| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021 |
- # File: core.py
- # Library: DOPAL - DO Python Azureus Library
- #
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation; version 2 of the License.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details ( see the COPYING file ).
- #
- # You should have received a copy of the GNU General Public License
- # along with this program; if not, write to the Free Software
- # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
- '''
- Contains the main objects and functions required for DOPAL to be useful.
- '''
- # PyChecker seems to be complaining a lot about inconsistent return types
- # (especially returning different types of responses and returning different
- # types of exceptions), so we're switching it off.
- __pychecker__ = 'no-returnvalues'
- from dopal.aztypes import unwrap_value
- from dopal.errors import AzureusResponseXMLError, MissingObjectIDError, \
- RemoteInternalError, RemoteMethodError, LinkError, InvalidObjectIDError, \
- NoSuchMethodError, InvalidWrapTypeError, InvalidRemoteObjectError, \
- NoObjectIDGivenError, NoEstablishedConnectionError, \
- InvalidConnectionIDError, raise_as, DopalPendingDeprecationWarning
- #
- # Low-level class, representing a link to a remote Azureus instance.
- #
- class AzureusLink(object):
- def __init__(self): # AzureusLink
- # Default values, can be changed via set_connection_details.
- self.link_data = {
- 'host': '127.0.0.1', 'port': 6884, 'secure': False,
- 'user': None, 'password': ''}
- self.debug = None
- def get_cgi_path(self, auth_details=False, include_password=False): # AzureusLink
- path_template = "%(protocol)s://%(auth)s%(host)s:%(port)s/process.cgi"
- path_data = {}
- path_data['host'] = self.link_data['host']
- path_data['port'] = self.link_data['port']
- path_data['user'] = self.link_data['user']
- if self.link_data['secure']:
- path_data['protocol'] = 'https'
- else:
- path_data['protocol'] = 'http'
- if auth_details and self.link_data['user']:
- if include_password:
- #path_data['password'] = '*' * len(self.link_data['password'])
- path_data['password'] = '*' * 4
- else:
- path_data['password'] = self.link_data['password']
- path_data['auth'] = '%(user)s:%(password)s@' % path_data
- else:
- path_data['auth'] = ''
- return path_template % path_data
- def _send_data(self, request):
- import urllib2
- return urllib2.urlopen(request)
- def _send_method_exchange(self, xml_data): # AzureusLink
- from dopal.debug import ConnectionExchangeDebug, \
- ErrorLinkDebug, OutgoingExchangeDebug
- cgi_path = self.get_cgi_path(auth_details=False)
- printable_path = self.get_cgi_path(auth_details=True, include_password=False)
- if self.debug is not None:
- debug_data = OutgoingExchangeDebug(printable_path, xml_data)
- self.debug(debug_data)
- import socket, urllib2
- request = urllib2.Request(cgi_path, xml_data)
- # Add User-Agent string.
- from dopal import __user_agent__
- request.add_header("User-agent", __user_agent__)
- # Add authorisation data.
- if self.link_data['user']:
- auth_string = ("%(user)s:%(password)s" % self.link_data)
- base64_string = auth_string.encode('base64').strip()
- request.add_header("Authorization", "Basic " + base64_string)
- del auth_string, base64_string
- try:
- data = self._send_data(request).read()
- except (urllib2.URLError, socket.error, LinkError), error:
- # Log the error, if enabled.
- if self.debug is not None:
- debug_data = ErrorLinkDebug(printable_path, error)
- self.debug(debug_data)
- # Error raised here.
- raise_as(error, LinkError, obj=cgi_path)
- # Log the exchange, if enabled.
- if self.debug is not None:
- debug_data = ConnectionExchangeDebug(printable_path, xml_data, data)
- self.debug(debug_data)
- return data
- def send_method_exchange(self, xml_data): # AzureusLink
- retry_count = 0
- retry_namespace = None
- while True:
- try:
- result = self._send_method_exchange(xml_data)
- except LinkError, error:
- if retry_namespace is not None:
- retry_namespace = {}
- if self.handle_link_error(error, retry_count, retry_namespace):
- retry_count = 1
- else:
- raise error
- else:
- if retry_count:
- self.handle_link_repair(error, retry_count, retry_namespace)
- return result
- # This won't happen, but it keeps PyChecker happy.
- return None
- def handle_link_error(self, error, retry_count, saved):
- return False # Don't bother retrying.
- def handle_link_repair(self, error, retry_count, saved):
- pass
- def set_link_details(self, **kwargs): # AzureusLink
- """
- Sets the details of where the Azureus server to connect to is located.
- @rtype: None
- @keyword host: Host name of the machine to connect to (default is
- C{127.0.0.1}).
- @keyword port: Server port that Azureus is accepting XML/HTTP
- connections on (default is C{6884}).
- @keyword secure: Set to a true value if the Azureus is only
- accepting secure connections (default is C{False}).
- @keyword user: For authenticated connections - the user name to
- connect as (default is to use no authentication).
- @keyword password: For authenticated connections - the password to
- connect with (default is to use no authentication).
- """
- # Smart use of handle_kwargs, I think. :)
- from dopal.utils import handle_kwargs
- kwargs = handle_kwargs(kwargs, **self.link_data)
- #for key, value in kwargs.items():
- # if key not in self.link_data:
- # raise TypeError, "invalid keyword argument: %s" % key
- self.link_data.update(kwargs)
- def __str__(self): # AzureusLink
- return "%s for %s" % (self.__class__.__name__, self.link_data['host'])
- #
- # Method generation.
- #
- def remote_method_call_to_xml(method_name, method_args, request_id,
- object_id=None, connection_id=None):
- '''
- Generates an XML block which can be sent to Azureus to invoke a remote
- method.
- An example of the output generated by this method::
- >>> remote_method_call_to_xml('getDownloads', [True], request_id=123, object_id=456, connection_id=789)
- <REQUEST>
- <OBJECT>
- <_object_id>456</_object_id>
- </OBJECT>
- <METHOD>getDownloads[boolean]</METHOD>
- <PARAMS>
- <ENTRY index="0">true</ENTRY>
- </PARAMS>
- <CONNECTION_ID>789</CONNECTION_ID>
- <REQUEST_ID>123</REQUEST_ID>
- </REQUEST>
- The I{method_args} parameter needs to be a sequence of items representing
- the method you want to invoke. Each argument needs to be one of the
- following types:
- - C{boolean} (represented in Java as a boolean)
- - C{int} (represented in Java as an int)
- - C{long} (represented in Java as a long)
- - C{str} or C{unicode} (represented in Java as a String)
- - C{float} (represented in Java as a float)
- - An object with a I{get_xml_type} method, which returns a string
- representing the name of the Java data type that it represents. It
- needs to also have one other method on it:
- - I{get_object_id} - needs to return the remote ID of the Azureus
- object it is representing; or
- - I{as_xml} - returns an object which can be converted into a string
- containing XML representing the value of this object. Several other
- types are supported using this method, defined in the L{aztypes}
- module (such as C{java.net.URL}, C{byte[]} etc.)
- @attention: B{Deprecated:} This method is not unicode-safe, nor does it
- define what happens when dealing with unicode data. Use
- L{remote_method_call_as_xml} instead.
- @param method_name: A string representing the name of the method you want
- to invoke (which must either be a method available on the object with the
- given object ID, or a special global method which Azureus has some special
- case behaviour for).
- @type method_name: str
- @param method_args: A sequence of items representing the arguments you want
- to pass to the method (definition of what types are accepted are explained
- above).
- @param request_id: The unique ID to be given to this invocation request
- (each invocation on a connection must be unique).
- @type request_id: str / int / long
- @param object_id: The object on which to invoke the method on. There are
- some methods which are special cased which don't require an object ID - in
- these cases, this can be left as I{None}.
- @type object_id: str / int / long / None
- @param connection_id: The ID of the connection you are using - this value
- is given to you once you have initiated a connection with Azureus - this
- can be left blank if you haven't yet initiated the connection.
- @type connection_id: str / int / long / None
- @return: A string representing the XML block to send.
- @raise InvalidWrapTypeError: Raised if one of the items in the
- method_args sequence does not match one of the accepted types.
- @see: L{aztypes}
- @see: L{InvalidWrapTypeError}
- @summary: B{Deprecated:} Use L{remote_method_call_as_xml} instead.
- '''
- import warnings
- warnings.warn("remote_method_call_to_xml is deprecated, use remote_method_call_as_xml instead", DopalPendingDeprecationWarning)
- import types
- from dopal.xmlutils import XMLObject, make_xml_ref_for_az_object
- # We check the argument types here,
- arg_types = []
- arg_values = []
- for method_arg in method_args:
- # The value has methods on it to tell us how we should handle it.
- if hasattr(method_arg, 'get_xml_type'):
- arg_type = method_arg.get_xml_type()
- # Either the value generates the XML itself (like below), or we
- # are able to determine how to generate the XML for it.
- if hasattr(method_arg, 'as_xml'):
- arg_value = method_arg.as_xml()
- # The value represents a remote object...
- elif hasattr(method_arg, 'get_object_id'):
- arg_value = make_xml_ref_for_az_object(method_arg.get_object_id())
- # If we get here, we don't know how to handle this object.
- else:
- raise InvalidWrapTypeError(obj=method_arg)
- # We must check boolean types before integers, as booleans are
- # a type of integer.
- #
- # The first check is just to ensure that booleans exist on the
- # system (to retain compatibility with Python 2.2)
- elif hasattr(types, 'BooleanType') and isinstance(method_arg, bool):
- arg_type = 'boolean'
- # lower - the Java booleans are lower case.
- arg_value = str(method_arg).lower()
- elif isinstance(method_arg, int):
- arg_type = 'int'
- arg_value = str(method_arg)
- elif isinstance(method_arg, types.StringTypes):
- arg_type = 'String'
- arg_value = method_arg
- elif isinstance(method_arg, long):
- arg_type = 'long'
- arg_value = str(method_arg)
- elif isinstance(method_arg, float):
- arg_type = 'float'
- arg_value = str(method_arg)
- else:
- raise InvalidWrapTypeError(obj=method_arg)
- arg_types.append(arg_type)
- arg_values.append(arg_value)
- del arg_type, arg_value, method_arg
- # We don't need to refer to method_args again, as we have arg_types and
- # arg_values. This prevents the code below accessing method_args
- # accidently.
- del method_args
- # Now we start to generate the XML.
- request_block = XMLObject('REQUEST')
- # Add the object ID (if we have one).
- if object_id:
- # We are just using this object to generate the XML block, the name
- # we give the type is not used, so does not matter.
- object_block = make_xml_ref_for_az_object(object_id)
- request_block.add_content(object_block)
- del object_block
- # Add the method identifier.
- method_block = XMLObject('METHOD')
- method_content = method_name
- if arg_types:
- method_content += '[' + ','.join(arg_types) + ']'
- method_block.add_content(method_content)
- request_block.add_content(method_block)
- del method_block, method_content
- # Add method arguments.
- if arg_values:
- params_block = XMLObject('PARAMS')
- for index_pos, xml_value in zip(range(len(arg_values)), arg_values):
- entry_block = XMLObject('ENTRY')
- entry_block.add_attribute('index', str(index_pos))
- entry_block.add_content(xml_value)
- params_block.add_content(entry_block)
- request_block.add_content(params_block)
- del index_pos, xml_value, entry_block, params_block
- # Add the connection ID (if we have one).
- if connection_id:
- connection_id_block = XMLObject('CONNECTION_ID')
- connection_id_block.add_content(str(connection_id))
- request_block.add_content(connection_id_block)
- del connection_id_block
- # Add a "unique" request ID.
- request_id_block = XMLObject('REQUEST_ID')
- request_id_block.add_content(str(request_id))
- request_block.add_content(request_id_block)
- return request_block.to_string()
- def remote_method_call_as_xml(method_name, method_args, request_id,
- object_id=None, connection_id=None):
- '''
- Generates an XML block which can be sent to Azureus to invoke a remote
- method - this is returned as an object which can be turned into a unicode
- string.
- An example of the output generated by this method::
- >>> remote_method_call_as_xml('getDownloads', [True], request_id=123, object_id=456, connection_id=789).encode('UTF-8')
- <?xml version="1.0" encoding="UTF-8"?>
- <REQUEST>
- <OBJECT>
- <_object_id>456</_object_id>
- </OBJECT>
- <METHOD>getDownloads[boolean]</METHOD>
- <PARAMS>
- <ENTRY index="0">true</ENTRY>
- </PARAMS>
- <CONNECTION_ID>789</CONNECTION_ID>
- <REQUEST_ID>123</REQUEST_ID>
- </REQUEST>
- The I{method_args} parameter needs to be a sequence of items representing
- the method you want to invoke. Each argument needs to be one of the
- following types:
- - C{boolean} (represented in Java as a boolean)
- - C{int} (represented in Java as an int)
- - C{long} (represented in Java as a long)
- - C{str} or C{unicode} (represented in Java as a String)
- - C{float} (represented in Java as a float)
- - An object with a I{get_xml_type} method, which returns a string
- representing the name of the Java data type that it represents. It
- needs to also have one other method on it:
- - I{get_object_id} - needs to return the remote ID of the Azureus
- object it is representing; or
- - I{as_xml} - returns an object which can be converted into a string
- containing XML representing the value of this object. Several other
- types are supported using this method, defined in the L{aztypes}
- module (such as C{java.net.URL}, C{byte[]} etc.)
- @attention: Any byte strings passed to this function will be treated as if
- they are text strings, and they can be converted to unicode using the
- default system encoding. If the strings represented encoded content, you
- must decode them to unicode strings before passing to this function.
- @note: This function will return an object which has an C{encode} method
- (to convert the XML into the specified bytestring representation. The
- object can also be converted into a unicode string via the C{unicode}
- function. Currently, this object will be an instance of
- L{UXMLObject<dopal.xmlutils.UXMLObject>}, but this behaviour may change in
- future - the only guarantees this function makes is the fact that the
- resulting object can be converted into unicode,
- and that it will have an encode method on it.
- @param method_name: A string representing the name of the method you want
- to invoke (which must either be a method available on the object with the
- given object ID, or a special global method which Azureus has some special
- case behaviour for).
- @type method_name: str / unicode
- @param method_args: A sequence of items representing the arguments you want
- to pass to the method (definition of what types are accepted are explained
- above).
- @param request_id: The unique ID to be given to this invocation request
- (each invocation on a connection must be unique).
- @type request_id: str / unicode / int / long
- @param object_id: The object on which to invoke the method on. There are
- some methods which are special cased which don't require an object ID - in
- these cases, this can be left as I{None}.
- @type object_id: str / unicode / int / long / None
- @param connection_id: The ID of the connection you are using - this value
- is given to you once you have initiated a connection with Azureus - this
- can be left blank if you haven't yet initiated the connection.
- @type connection_id: str / unicode / int / long / None
- @return: An object which has an C{encode} method (to convert the XML into
- the specified bytestring representation. The object can also be converted
- into a unicode string via the C{unicode} function. Currently, this object
- will be an instance of L{UXMLObject<dopal.xmlutils.UXMLObject>}, but this
- behaviour may change in future - the only guarantees this function makes is
- the fact that the resulting object can be converted into unicode, and that
- it will have an encode method on it.
- @raise InvalidWrapTypeError: Raised if one of the items in the
- method_args sequence does not match one of the accepted types.
- @see: L{aztypes}
- @see: L{InvalidWrapTypeError}
- @see: L{UXMLObject<dopal.xmlutils.UXMLObject>}
- '''
- import types
- from dopal.xmlutils import UXMLObject, make_xml_ref_for_az_object
- # We check the argument types here,
- arg_types = []
- arg_values = []
- for method_arg in method_args:
- # The value has methods on it to tell us how we should handle it.
- if hasattr(method_arg, 'get_xml_type'):
- arg_type = method_arg.get_xml_type()
- # Either the value generates the XML itself (like below), or we
- # are able to determine how to generate the XML for it.
- if hasattr(method_arg, 'as_xml'):
- arg_value = method_arg.as_xml()
- # The value represents a remote object...
- elif hasattr(method_arg, 'get_object_id'):
- arg_value = make_xml_ref_for_az_object(method_arg.get_object_id())
- # If we get here, we don't know how to handle this object.
- else:
- raise InvalidWrapTypeError(obj=method_arg)
- # We must check boolean types before integers, as booleans are
- # a type of integer.
- #
- # The first check is just to ensure that booleans exist on the
- # system (to retain compatibility with Python 2.2)
- elif hasattr(types, 'BooleanType') and isinstance(method_arg, bool):
- arg_type = 'boolean'
- # lower - the Java booleans are lower case.
- arg_value = str(method_arg).lower()
- elif isinstance(method_arg, int):
- arg_type = 'int'
- arg_value = str(method_arg)
- elif isinstance(method_arg, types.StringTypes):
- arg_type = 'String'
- arg_value = method_arg
- elif isinstance(method_arg, long):
- arg_type = 'long'
- arg_value = str(method_arg)
- elif isinstance(method_arg, float):
- arg_type = 'float'
- arg_value = str(method_arg)
- else:
- raise InvalidWrapTypeError(obj=method_arg)
- arg_types.append(arg_type)
- arg_values.append(arg_value)
- del arg_type, arg_value, method_arg
- # We don't need to refer to method_args again, as we have arg_types and
- # arg_values. This prevents the code below accessing method_args
- # accidently.
- del method_args
- # Now we start to generate the XML.
- request_block = UXMLObject('REQUEST')
- # Add the object ID (if we have one).
- if object_id:
- # We are just using this object to generate the XML block, the name
- # we give the type is not used, so does not matter.
- object_block = make_xml_ref_for_az_object(object_id)
- request_block.add_content(object_block)
- del object_block
- # Add the method identifier.
- method_block = UXMLObject('METHOD')
- method_content = method_name
- if arg_types:
- method_content += '[' + ','.join(arg_types) + ']'
- method_block.add_content(method_content)
- request_block.add_content(method_block)
- # Make this easily accessible for the debugger.
- request_block.request_method = method_content
- del method_block, method_content
- # Add method arguments.
- if arg_values:
- params_block = UXMLObject('PARAMS')
- for index_pos, xml_value in zip(range(len(arg_values)), arg_values):
- entry_block = UXMLObject('ENTRY')
- entry_block.add_attribute('index', str(index_pos))
- entry_block.add_content(xml_value)
- params_block.add_content(entry_block)
- request_block.add_content(params_block)
- del index_pos, xml_value, entry_block, params_block
- # Add the connection ID (if we have one).
- if connection_id:
- connection_id_block = UXMLObject('CONNECTION_ID')
- connection_id_block.add_content(str(connection_id))
- request_block.add_content(connection_id_block)
- del connection_id_block
- # Add a "unique" request ID.
- request_id_block = UXMLObject('REQUEST_ID')
- request_id_block.add_content(str(request_id))
- request_block.add_content(request_id_block)
- return request_block
- #
- # Incoming method handling.
- #
- # Processes an XML response returned by Azureus, returning an AzureusResponse
- # instance.
- #
- # xml_node must be an instance of xml.dom.Node which has been normalised using
- # the normalise_xml_structure function.
- #
- # This function will raise a AzureusResponseXMLError if the XML is not in the
- # format expected.
- def process_xml_response(xml_node):
- if len(xml_node.childNodes) != 1:
- err = "expected one main block inside document, had %s"
- raise AzureusResponseXMLError, err % len(xml_node.childNodes)
- block_name = xml_node.firstChild.localName
- if block_name != 'RESPONSE':
- err = "expected a RESPONSE block, got %s block instead"
- raise AzureusResponseXMLError, err % block_name
- response_block = xml_node.firstChild
- az_dict = {}
- # We get an empty response block when the remote method doesn't return a
- # result (e.g. void), or returns a reponse which is effectively empty
- # (empty sequence, empty string) - or perhaps null itself.
- if not response_block.hasChildNodes():
- return NullResponse(az_dict)
- # If we detect any child block with the name ERROR, then we'll raise an
- # error and ignore the rest of the content (it is possible for the block
- # to be embedded alongside other values - normally if something has gone
- # wrong during processing.
- #
- # XXX: Perhaps this could occur anywhere in the tree structure, what should
- # we do?
- from xml.dom import Node
- from dopal.xmlutils import get_text_content
- for child_block in response_block.childNodes:
- if child_block.nodeType == Node.ELEMENT_NODE and \
- child_block.nodeName == 'ERROR':
- return ErrorResponse(az_dict, get_text_content(child_block))
- if len(response_block.childNodes) == 1:
- node = response_block.firstChild
- if node.nodeType == Node.TEXT_NODE:
- return AtomicResponse(az_dict, get_text_content(node))
- # We will have some "complex" XML structure. It may contain the definition
- # of one remote object, one remote object with other remote objects
- # branching off it, or no remote objects at all.
- #
- # We will return the XML as-is, but we will try to extract important data
- # so that it is more conveniently retrievable.
- #
- # Nodes which are categorised as being "important" are currently defined
- # as information about Azureus and any connection information.
- conn_node = None
- azureus_nodes = []
- for node in response_block.childNodes:
- if node.nodeName.startswith('azureus_'):
- azureus_nodes.append(node)
- elif node.nodeName == '_connection_id':
- conn_node = node
- else:
- pass
- # Extract the data from the Azureus nodes.
- az_dict = {}
- for az_node in azureus_nodes:
- name = az_node.nodeName[8:] # (remove "azureus_" prefix)
- value = get_text_content(az_node)
- az_dict[name] = value
- # Extract the connection ID.
- if conn_node:
- connection_id = long(get_text_content(conn_node))
- else:
- connection_id = None
- # We've got a structured definition.
- return StructuredResponse(az_dict, response_block, connection_id)
- #
- # Base class of all types of response which can be returned by Azureus.
- #
- # It will have at least the following attributes:
- #
- # azureus_data - dictionary containng information about the instance of
- # Azureus which is running.
- #
- # response_data - The value of the response object. The type of this value
- # will differ between different Response implementations.
- #
- # connection_id - ID of the connection given in the response. Will be None
- # if none was given.
- #
- class AzureusResponse(object):
- def __init__(self, azureus_data, response_data=None, connection_id=None):
- self.azureus_data = azureus_data
- self.response_data = response_data
- self.connection_id = connection_id
- class ErrorResponse(AzureusResponse):
- def raise_error(self):
- raise generate_remote_error(self.response_data)
- class StructuredResponse(AzureusResponse):
- def get_object_id(self):
- # Doesn't matter that this is an abstract class, the method still
- # works. :)
- from dopal.convert import XMLStructureReader
- return XMLStructureReader.get_object_id(self.response_data)
- class AtomicResponse(AzureusResponse):
- def get_value(self, value_type=None):
- if value_type is None:
- return self.response_data
- else:
- return unwrap_value(self.response_data, value_type)
- def as_string(self):
- return self.get_value("String")
- def as_int(self):
- return self.get_value("int")
- def as_long(self):
- return self.get_value("long")
- def as_float(self):
- return self.get_value("float")
- def as_bool(self):
- return self.get_value("boolean")
- def as_bytes(self):
- return self.get_value("byte[]")
- class NullResponse(AzureusResponse):
- '''
- A response class which is used when Azureus returns a response which
- contains no content at all.
- This is normally returned when:
- - C{null} is returned by the remote method.
- - An empty sequence.
- - An empty string.
- - The return type of the method is C{void}.
- '''
- def get_value(self, value_type=None):
- if value_type is None:
- return None
- elif value_type in ['byte[]', 'String']:
- return ''
- else:
- return InvalidUnwrapTypeError(obj=value_type)
- #
- # Error-handling.
- #
- #
- # This method takes a string returned in a response and generates an instance
- # of RemoteMethodError - it doesn't take into account of any reported class
- # type or any other data.
- #
- def generate_remote_error(message):
- # Bad method?
- bad_method_prefix = 'Unknown method: '
- if message.startswith(bad_method_prefix):
- return NoSuchMethodError(message[len(bad_method_prefix):])
- # Bad object ID?
- if message == 'Object no longer exists':
- return InvalidObjectIDError()
- # Missing object ID?
- if message == 'Object identifier missing from request':
- return MissingObjectIDError()
- # Perhaps a Java exception has occurred remotely. Not always easy to
- # detect - it'll mention the Java exception class though. For example,
- # passing an non-integer object ID got this error:
- #
- # u'java.lang.RuntimeException: java.lang.NumberFormatException: For
- # input string: "3536f63"'
- #
- # So we'll try and test to see if a Java exception occurred, by seeing
- # if there appears to be a Java-esque exception mentioned.
- parts = message.split(':', 1)
- if len(parts) == 2:
- exception_name = parts[0]
- # A Java-esque exception name: We'll take anything which is defined
- # in a package with java. at the start, and ends with Error or
- # Exception, then we'll take it.
- if exception_name.startswith('java.') and \
- (exception_name.endswith('Error') or \
- exception_name.endswith('Exception')):
- return RemoteInternalError(message)
- # Something went wrong - don't know what...
- return RemoteMethodError(message)
- #
- # Higher-level version of AzureusLink - this class maintains an active
- # connection with the remote server - it also utilises other components
- # defined by this module.
- #
- class AzureusConnection(AzureusLink):
- def __init__(self): # AzureusConnection
- AzureusLink.__init__(self)
- self.connection_id = None
- self.request_id = None # Will be initialised later.
- def update_connection_details(self, connection_id=None, connection_data={}): # AzureusConnection
- if connection_id is not None:
- self.connection_id = connection_id
- # Return true if the specified method can be called without passing an
- # object ID or connection ID.
- #
- # XXX: Would it be safe to have ExtendedAzureusConnection handle this?
- # I would say yes, but look at the invoke_remote_method method, I don't
- # think it would behave well if we either return True or False all the
- # time...
- def _is_global_method_call(self, method_name, method_args): # AzureusConnection
- return method_name in ['getDownloads', 'getSingleton'] and not method_args
- def invoke_remote_method(self, object_id, method_name, method_args, raise_errors=True): # AzureusConnection
- # We require a connection ID and an object ID, unless we are calling
- # a "global" method - methods which don't need either, and which will
- # actually return that data to you in the result.
- if self._is_global_method_call(method_name, method_args):
- connection_id = None
- else:
- connection_id = self.connection_id
- if object_id is None:
- raise NoObjectIDGivenError
- if connection_id is None:
- raise NoEstablishedConnectionError
- from xml.dom.minidom import parseString
- from dopal.xmlutils import normalise_xml_structure, get_text_content
- # First step, convert the method data to XML.
- xml_data = remote_method_call_as_xml(method_name, method_args,
- self.get_new_request_id(), object_id, connection_id)
- xml_data_as_string = xml_data.encode('UTF-8')
- from dopal.debug import MethodRequestDebug, MethodResponseDebug
- # Log a debug message, if appropriate.
- if self.debug is not None:
- self.debug(MethodRequestDebug(object_id, xml_data.request_method))
- # Second step, send this to Azureus and get a response back.
- xml_response_string = self.send_method_exchange(xml_data_as_string)
- # Third step, convert the string into a xml.dom.Node structure.
- xml_structure = parseString(xml_response_string)
- # Fourth step, sanitise the XML structure for easier parsing.
- normalise_xml_structure(xml_structure)
- # Fifth step, calculate the Azureus response instance represented by
- # this XML structure.
- response = process_xml_response(xml_structure)
- # Send another debug message with the response.
- if self.debug is not None:
- self.debug(MethodResponseDebug(object_id, xml_data.request_method, response))
- # Sixth step - update our own connection data given in this response.
- connection_id = response.connection_id
- azureus_data = response.azureus_data
- self.update_connection_details(connection_id, azureus_data)
- # Seventh step - return the response (or raise an error, if it's an
- # error response).
- if raise_errors and isinstance(response, ErrorResponse):
- response.raise_error()
- return response
- def get_new_request_id(self): # AzureusConnection
- if self.request_id is None:
- # We use long to force it to be an integer.
- import time
- self.request_id = long(time.time())
- self.request_id += 1
- return self.request_id
- class ExtendedAzureusConnection(AzureusConnection):
- def __init__(self): # ExtendedAzureusConnection
- AzureusConnection.__init__(self)
- self.connection_data = {}
- self._plugin_interface_id = None
- def invoke_remote_method(self, object_id, method_name, method_args, raise_errors=True): # ExtendedAzureusConnection
- try:
- response = AzureusConnection.invoke_remote_method(self, object_id, method_name, method_args, raise_errors)
- except InvalidObjectIDError:
- if self.is_connection_valid():
- raise InvalidRemoteObjectError
- else:
- raise InvalidConnectionIDError
- if object_id is None and method_name == 'getSingleton' and not method_args and isinstance(response, StructuredResponse):
- self._plugin_interface_id = response.get_object_id()
- return response
- def establish_connection(self, force=True): # ExtendedAzureusConnection
- '''
- Establishes a connection with the Azureus server.
- By invoking this method, this will ensure that other methods defined
- by this class work correctly, as it will have references both to a
- connection ID, and the ID for the plugin interface.
- The C{force} argument determines whether server communication must
- take place or not. If C{True}, then this object will communicate with
- the Azureus server - if C{False}, then this object will only
- communicate with the Azureus server if it has no recorded information
- about the plugin interface.
- This method has two uses, depending on the value of the C{force}
- argument - it can be used to ensure that there is a valid recorded
- connection in place (if force is C{True}), or it can be used just
- to ensure that other methods on this class will behave properly (if
- force is C{False}).
- If a new connection is established, then the L{_on_reconnect} method
- will be invoked.
- @param force: Boolean value indicating if communication with the server
- I{must} take place or not (default is C{True}).
- @return: None
- '''
- # If 'force' is not true - then we only make a call if we don't have
- # any stored reference to a plugin interface ID.
- if (not force) and (self._plugin_interface_id is not None):
- return
- # Our overridden implementation of this method will set
- # what we need.
- old_interface_id = self._plugin_interface_id
- response = self.invoke_remote_method(None, 'getSingleton', ())
- # If we had the ID for an old PluginInterface object, then
- # that probably signals a reconnection. So let's signal that.
- if old_interface_id is not None and \
- old_interface_id != self._plugin_interface_id:
- self._on_reconnect()
- return
- def _on_reconnect(self): # ExtendedAzureusConnection
- '''
- Hook for subclasses to be notified whenever a new connection has been
- made.
- '''
- pass
- def is_connection_valid(self): # ExtendedAzureusConnection
- '''
- Returns a boolean value indicating if the current connection is
- still valid.
- @invariant: This connection to have already been I{established}.
- @raise NoEstablishedConnectionError: If this connection has not been
- established.
- @return: C{True} if the current established connection is still valid,
- C{False} otherwise.
- '''
- if self._plugin_interface_id is None:
- raise NoEstablishedConnectionError
- # Try to invoke this method on the remote PluginInterface object.
- try:
- AzureusConnection.invoke_remote_method(self, self._plugin_interface_id, '_refresh', ())
- except InvalidObjectIDError:
- return False
- else:
- return True
- def __str__(self): # ExtendedAzureusConnection
- result = super(ExtendedAzureusConnection, self).__str__()
- if self.connection_data.has_key('name') and \
- self.connection_data.has_key('version'):
- result += " [%(name)s %(version)s]" % self.connection_data
- return result
- def update_connection_details(self, connection_id=None, connection_data={}): # ExtendedAzureusConnection
- super(ExtendedAzureusConnection, self).update_connection_details(connection_id)
- self.connection_data.update(connection_data)
- def get_azureus_version(self): # ExtendedAzureusConnection
- '''
- @since: DOPAL 0.56
- '''
- try:
- az_version = self.connection_data['version']
- except KeyError:
- raise NoEstablishedConnectionError
- else:
- import dopal.utils
- return dopal.utils.parse_azureus_version_string(az_version)
- # Use of this name is deprecated, and this alias will be removed in later
- # versions of DOPAL.
- ReusableAzureusConnection = ExtendedAzureusConnection
|