diff options
Diffstat (limited to 'dbus')
-rw-r--r-- | dbus/__init__.py | 105 | ||||
-rw-r--r-- | dbus/_dbus.py | 256 | ||||
-rw-r--r-- | dbus/_expat_introspect_parser.py | 85 | ||||
-rw-r--r-- | dbus/_version.py | 3 | ||||
-rw-r--r-- | dbus/_version.py.in | 3 | ||||
-rw-r--r-- | dbus/bus.py | 439 | ||||
-rw-r--r-- | dbus/connection.py | 646 | ||||
-rw-r--r-- | dbus/dbus_bindings.py | 37 | ||||
-rw-r--r-- | dbus/decorators.py | 340 | ||||
-rw-r--r-- | dbus/exceptions.py | 106 | ||||
-rw-r--r-- | dbus/glib.py | 41 | ||||
-rw-r--r-- | dbus/gobject_service.py | 71 | ||||
-rw-r--r-- | dbus/lowlevel.py | 40 | ||||
-rw-r--r-- | dbus/mainloop/__init__.py | 62 | ||||
-rw-r--r-- | dbus/mainloop/glib.py | 41 | ||||
-rw-r--r-- | dbus/proxies.py | 557 | ||||
-rw-r--r-- | dbus/server.py | 117 | ||||
-rw-r--r-- | dbus/service.py | 829 | ||||
-rw-r--r-- | dbus/types.py | 9 |
19 files changed, 3787 insertions, 0 deletions
diff --git a/dbus/__init__.py b/dbus/__init__.py new file mode 100644 index 0000000..e990ec3 --- /dev/null +++ b/dbus/__init__.py @@ -0,0 +1,105 @@ +"""\ +Implements the public API for a D-Bus client. See the dbus.service module +to export objects or claim well-known names. + +.. + for epydoc's benefit + +:NewField SupportedUsage: Supported usage +:NewField Constructor: Constructor +""" + +# Copyright (C) 2003, 2004, 2005, 2006 Red Hat Inc. <http://www.redhat.com/> +# Copyright (C) 2003 David Zeuthen +# Copyright (C) 2004 Rob Taylor +# Copyright (C) 2005, 2006 Collabora Ltd. <http://www.collabora.co.uk/> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import os + +__all__ = ( + # from _dbus + 'Bus', 'SystemBus', 'SessionBus', 'StarterBus', + + # from proxies + 'Interface', + + # from _dbus_bindings + 'get_default_main_loop', 'set_default_main_loop', + + 'validate_interface_name', 'validate_member_name', + 'validate_bus_name', 'validate_object_path', + 'validate_error_name', + + 'BUS_DAEMON_NAME', 'BUS_DAEMON_PATH', 'BUS_DAEMON_IFACE', + 'LOCAL_PATH', 'LOCAL_IFACE', 'PEER_IFACE', + 'INTROSPECTABLE_IFACE', 'PROPERTIES_IFACE', + + 'ObjectPath', 'ByteArray', 'Signature', 'Byte', 'Boolean', + 'Int16', 'UInt16', 'Int32', 'UInt32', 'Int64', 'UInt64', + 'Double', 'String', 'Array', 'Struct', 'Dictionary', 'UTF8String', + + # from exceptions + 'DBusException', + 'MissingErrorHandlerException', 'MissingReplyHandlerException', + 'ValidationException', 'IntrospectionParserException', + 'UnknownMethodException', 'NameExistsException', + + # submodules + 'service', 'mainloop', 'lowlevel' + ) +__docformat__ = 'restructuredtext' + +try: + from dbus._version import version, __version__ +except ImportError: + pass + +# OLPC Sugar compatibility +import dbus.exceptions as exceptions +import dbus.types as types + +from _dbus_bindings import get_default_main_loop, set_default_main_loop,\ + validate_interface_name, validate_member_name,\ + validate_bus_name, validate_object_path,\ + validate_error_name +from _dbus_bindings import BUS_DAEMON_NAME, BUS_DAEMON_PATH, BUS_DAEMON_IFACE,\ + LOCAL_PATH, LOCAL_IFACE, PEER_IFACE,\ + INTROSPECTABLE_IFACE, PROPERTIES_IFACE + +from dbus.exceptions import MissingErrorHandlerException, \ + MissingReplyHandlerException, \ + ValidationException, \ + IntrospectionParserException, \ + UnknownMethodException, \ + NameExistsException, \ + DBusException +from _dbus_bindings import ObjectPath, ByteArray, Signature, Byte, Boolean,\ + Int16, UInt16, Int32, UInt32, Int64, UInt64,\ + Double, String, Array, Struct, Dictionary, \ + UTF8String +from dbus._dbus import Bus, SystemBus, SessionBus, StarterBus +from dbus.proxies import Interface + + +if 'DBUS_PYTHON_NO_DEPRECATED' not in os.environ: + from dbus._dbus import dbus_bindings # for backwards compat diff --git a/dbus/_dbus.py b/dbus/_dbus.py new file mode 100644 index 0000000..60e7933 --- /dev/null +++ b/dbus/_dbus.py @@ -0,0 +1,256 @@ +"""Implementation for dbus.Bus. Not to be imported directly.""" + +# Copyright (C) 2003, 2004, 2005, 2006 Red Hat Inc. <http://www.redhat.com/> +# Copyright (C) 2003 David Zeuthen +# Copyright (C) 2004 Rob Taylor +# Copyright (C) 2005, 2006 Collabora Ltd. <http://www.collabora.co.uk/> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +from __future__ import generators + +__all__ = ('Bus', 'SystemBus', 'SessionBus', 'StarterBus') +__docformat__ = 'reStructuredText' + +import os +import sys +import weakref +from traceback import print_exc + +from dbus.exceptions import DBusException +from _dbus_bindings import BUS_DAEMON_NAME, BUS_DAEMON_PATH,\ + BUS_DAEMON_IFACE, UTF8String,\ + validate_member_name, validate_interface_name,\ + validate_bus_name, validate_object_path,\ + BUS_SESSION, BUS_SYSTEM, BUS_STARTER,\ + DBUS_START_REPLY_SUCCESS, \ + DBUS_START_REPLY_ALREADY_RUNNING +from dbus.bus import BusConnection +from dbus.lowlevel import SignalMessage + +try: + import thread +except ImportError: + import dummy_thread as thread + + +class Bus(BusConnection): + """A connection to one of three possible standard buses, the SESSION, + SYSTEM, or STARTER bus. This class manages shared connections to those + buses. + + If you're trying to subclass `Bus`, you may be better off subclassing + `BusConnection`, which doesn't have all this magic. + """ + + _shared_instances = {} + + def __new__(cls, bus_type=BusConnection.TYPE_SESSION, private=False, + mainloop=None): + """Constructor, returning an existing instance where appropriate. + + The returned instance is actually always an instance of `SessionBus`, + `SystemBus` or `StarterBus`. + + :Parameters: + `bus_type` : cls.TYPE_SESSION, cls.TYPE_SYSTEM or cls.TYPE_STARTER + Connect to the appropriate bus + `private` : bool + If true, never return an existing shared instance, but instead + return a private connection. + + :Deprecated: since 0.82.3. Use dbus.bus.BusConnection for + private connections. + + `mainloop` : dbus.mainloop.NativeMainLoop + The main loop to use. The default is to use the default + main loop if one has been set up, or raise an exception + if none has been. + :Changed: in dbus-python 0.80: + converted from a wrapper around a Connection to a Connection + subclass. + """ + if (not private and bus_type in cls._shared_instances): + return cls._shared_instances[bus_type] + + # this is a bit odd, but we create instances of the subtypes + # so we can return the shared instances if someone tries to + # construct one of them (otherwise we'd eg try and return an + # instance of Bus from __new__ in SessionBus). why are there + # three ways to construct this class? we just don't know. + if bus_type == BUS_SESSION: + subclass = SessionBus + elif bus_type == BUS_SYSTEM: + subclass = SystemBus + elif bus_type == BUS_STARTER: + subclass = StarterBus + else: + raise ValueError('invalid bus_type %s' % bus_type) + + bus = BusConnection.__new__(subclass, bus_type, mainloop=mainloop) + + bus._bus_type = bus_type + + if not private: + cls._shared_instances[bus_type] = bus + + return bus + + def close(self): + t = self._bus_type + if self.__class__._shared_instances.get(t) is self: + del self.__class__._shared_instances[t] + super(Bus, self).close() + + def get_connection(self): + """Return self, for backwards compatibility with earlier dbus-python + versions where Bus was not a subclass of Connection. + + :Deprecated: since 0.80.0 + """ + return self + _connection = property(get_connection, None, None, + """self._connection == self, for backwards + compatibility with earlier dbus-python versions + where Bus was not a subclass of Connection.""") + + def get_session(private=False): + """Static method that returns a connection to the session bus. + + :Parameters: + `private` : bool + If true, do not return a shared connection. + """ + return SessionBus(private=private) + + get_session = staticmethod(get_session) + + def get_system(private=False): + """Static method that returns a connection to the system bus. + + :Parameters: + `private` : bool + If true, do not return a shared connection. + """ + return SystemBus(private=private) + + get_system = staticmethod(get_system) + + + def get_starter(private=False): + """Static method that returns a connection to the starter bus. + + :Parameters: + `private` : bool + If true, do not return a shared connection. + """ + return StarterBus(private=private) + + get_starter = staticmethod(get_starter) + + def __repr__(self): + if self._bus_type == BUS_SESSION: + name = 'session' + elif self._bus_type == BUS_SYSTEM: + name = 'system' + elif self._bus_type == BUS_STARTER: + name = 'starter' + else: + name = 'unknown bus type' + + return '<%s.%s (%s) at %#x>' % (self.__class__.__module__, + self.__class__.__name__, + name, id(self)) + __str__ = __repr__ + + +# FIXME: Drop the subclasses here? I can't think why we'd ever want +# polymorphism +class SystemBus(Bus): + """The system-wide message bus.""" + def __new__(cls, private=False, mainloop=None): + """Return a connection to the system bus. + + :Parameters: + `private` : bool + If true, never return an existing shared instance, but instead + return a private connection. + `mainloop` : dbus.mainloop.NativeMainLoop + The main loop to use. The default is to use the default + main loop if one has been set up, or raise an exception + if none has been. + """ + return Bus.__new__(cls, Bus.TYPE_SYSTEM, mainloop=mainloop, + private=private) + +class SessionBus(Bus): + """The session (current login) message bus.""" + def __new__(cls, private=False, mainloop=None): + """Return a connection to the session bus. + + :Parameters: + `private` : bool + If true, never return an existing shared instance, but instead + return a private connection. + `mainloop` : dbus.mainloop.NativeMainLoop + The main loop to use. The default is to use the default + main loop if one has been set up, or raise an exception + if none has been. + """ + return Bus.__new__(cls, Bus.TYPE_SESSION, private=private, + mainloop=mainloop) + +class StarterBus(Bus): + """The bus that activated this process (only valid if + this process was launched by DBus activation). + """ + def __new__(cls, private=False, mainloop=None): + """Return a connection to the bus that activated this process. + + :Parameters: + `private` : bool + If true, never return an existing shared instance, but instead + return a private connection. + `mainloop` : dbus.mainloop.NativeMainLoop + The main loop to use. The default is to use the default + main loop if one has been set up, or raise an exception + if none has been. + """ + return Bus.__new__(cls, Bus.TYPE_STARTER, private=private, + mainloop=mainloop) + + +if 'DBUS_PYTHON_NO_DEPRECATED' not in os.environ: + + class _DBusBindingsEmulation: + """A partial emulation of the dbus_bindings module.""" + def __str__(self): + return '_DBusBindingsEmulation()' + def __repr__(self): + return '_DBusBindingsEmulation()' + def __getattr__(self, attr): + global dbus_bindings + import dbus.dbus_bindings as m + dbus_bindings = m + return getattr(m, attr) + + dbus_bindings = _DBusBindingsEmulation() + """Deprecated, don't use.""" diff --git a/dbus/_expat_introspect_parser.py b/dbus/_expat_introspect_parser.py new file mode 100644 index 0000000..96b27ad --- /dev/null +++ b/dbus/_expat_introspect_parser.py @@ -0,0 +1,85 @@ +# Copyright (C) 2003, 2004, 2005, 2006 Red Hat Inc. <http://www.redhat.com/> +# Copyright (C) 2003 David Zeuthen +# Copyright (C) 2004 Rob Taylor +# Copyright (C) 2005, 2006 Collabora Ltd. <http://www.collabora.co.uk/> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +from xml.parsers.expat import ExpatError, ParserCreate +from dbus.exceptions import IntrospectionParserException + +class _Parser(object): + __slots__ = ('map', 'in_iface', 'in_method', 'sig') + def __init__(self): + self.map = {} + self.in_iface = '' + self.in_method = '' + self.sig = '' + + def parse(self, data): + parser = ParserCreate('UTF-8', ' ') + parser.buffer_text = True + parser.StartElementHandler = self.StartElementHandler + parser.EndElementHandler = self.EndElementHandler + parser.Parse(data) + return self.map + + def StartElementHandler(self, name, attributes): + if not self.in_iface: + if (not self.in_method and name == 'interface'): + self.in_iface = attributes['name'] + else: + if (not self.in_method and name == 'method'): + self.in_method = attributes['name'] + elif (self.in_method and name == 'arg'): + if attributes.get('direction', 'in') == 'in': + self.sig += attributes['type'] + + def EndElementHandler(self, name): + if self.in_iface: + if (not self.in_method and name == 'interface'): + self.in_iface = '' + elif (self.in_method and name == 'method'): + self.map[self.in_iface + '.' + self.in_method] = self.sig + self.in_method = '' + self.sig = '' + +def process_introspection_data(data): + """Return a dict mapping ``interface.method`` strings to the + concatenation of all their 'in' parameters, and mapping + ``interface.signal`` strings to the concatenation of all their + parameters. + + Example output:: + + { + 'com.example.SignalEmitter.OneString': 's', + 'com.example.MethodImplementor.OneInt32Argument': 'i', + } + + :Parameters: + `data` : str + The introspection XML. Must be an 8-bit string of UTF-8. + """ + try: + return _Parser().parse(data) + except Exception, e: + raise IntrospectionParserException('%s: %s' % (e.__class__, e)) diff --git a/dbus/_version.py b/dbus/_version.py new file mode 100644 index 0000000..a2dca5f --- /dev/null +++ b/dbus/_version.py @@ -0,0 +1,3 @@ +# dbus/_version.py. Generated from _version.py.in by configure. +version = (0, 83, 1) +__version__ = "0.83.1" diff --git a/dbus/_version.py.in b/dbus/_version.py.in new file mode 100644 index 0000000..42c9dc7 --- /dev/null +++ b/dbus/_version.py.in @@ -0,0 +1,3 @@ +# @configure_input@ +version = (@DBUS_PYTHON_MAJOR_VERSION@, @DBUS_PYTHON_MINOR_VERSION@, @DBUS_PYTHON_MICRO_VERSION@) +__version__ = "@DBUS_PYTHON_MAJOR_VERSION@.@DBUS_PYTHON_MINOR_VERSION@.@DBUS_PYTHON_MICRO_VERSION@" diff --git a/dbus/bus.py b/dbus/bus.py new file mode 100644 index 0000000..edd5ef7 --- /dev/null +++ b/dbus/bus.py @@ -0,0 +1,439 @@ +# Copyright (C) 2007 Collabora Ltd. <http://www.collabora.co.uk/> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +__all__ = ('BusConnection',) +__docformat__ = 'reStructuredText' + +import logging +import weakref + +from _dbus_bindings import validate_interface_name, validate_member_name,\ + validate_bus_name, validate_object_path,\ + validate_error_name,\ + BUS_SESSION, BUS_STARTER, BUS_SYSTEM, \ + DBUS_START_REPLY_SUCCESS, \ + DBUS_START_REPLY_ALREADY_RUNNING, \ + BUS_DAEMON_NAME, BUS_DAEMON_PATH, BUS_DAEMON_IFACE,\ + NAME_FLAG_ALLOW_REPLACEMENT, \ + NAME_FLAG_DO_NOT_QUEUE, \ + NAME_FLAG_REPLACE_EXISTING, \ + RELEASE_NAME_REPLY_NON_EXISTENT, \ + RELEASE_NAME_REPLY_NOT_OWNER, \ + RELEASE_NAME_REPLY_RELEASED, \ + REQUEST_NAME_REPLY_ALREADY_OWNER, \ + REQUEST_NAME_REPLY_EXISTS, \ + REQUEST_NAME_REPLY_IN_QUEUE, \ + REQUEST_NAME_REPLY_PRIMARY_OWNER +from dbus.connection import Connection +from dbus.exceptions import DBusException +from dbus.lowlevel import HANDLER_RESULT_NOT_YET_HANDLED + + +_NAME_OWNER_CHANGE_MATCH = ("type='signal',sender='%s'," + "interface='%s',member='NameOwnerChanged'," + "path='%s',arg0='%%s'" + % (BUS_DAEMON_NAME, BUS_DAEMON_IFACE, + BUS_DAEMON_PATH)) +"""(_NAME_OWNER_CHANGE_MATCH % sender) matches relevant NameOwnerChange +messages""" + +_NAME_HAS_NO_OWNER = 'org.freedesktop.DBus.Error.NameHasNoOwner' + +_logger = logging.getLogger('dbus.bus') + + +class NameOwnerWatch(object): + __slots__ = ('_match', '_pending_call') + + def __init__(self, bus_conn, bus_name, callback): + validate_bus_name(bus_name) + + def signal_cb(owned, old_owner, new_owner): + callback(new_owner) + + def error_cb(e): + if e.get_dbus_name() == _NAME_HAS_NO_OWNER: + callback('') + else: + logging.basicConfig() + _logger.debug('GetNameOwner(%s) failed:', bus_name, + exc_info=(e.__class__, e, None)) + + self._match = bus_conn.add_signal_receiver(signal_cb, + 'NameOwnerChanged', + BUS_DAEMON_IFACE, + BUS_DAEMON_NAME, + BUS_DAEMON_PATH, + arg0=bus_name) + self._pending_call = bus_conn.call_async(BUS_DAEMON_NAME, + BUS_DAEMON_PATH, + BUS_DAEMON_IFACE, + 'GetNameOwner', + 's', (bus_name,), + callback, error_cb, + utf8_strings=True) + + def cancel(self): + if self._match is not None: + self._match.remove() + if self._pending_call is not None: + self._pending_call.cancel() + self._match = None + self._pending_call = None + + +class BusConnection(Connection): + """A connection to a D-Bus daemon that implements the + ``org.freedesktop.DBus`` pseudo-service. + + :Since: 0.81.0 + """ + + TYPE_SESSION = BUS_SESSION + """Represents a session bus (same as the global dbus.BUS_SESSION)""" + + TYPE_SYSTEM = BUS_SYSTEM + """Represents the system bus (same as the global dbus.BUS_SYSTEM)""" + + TYPE_STARTER = BUS_STARTER + """Represents the bus that started this service by activation (same as + the global dbus.BUS_STARTER)""" + + START_REPLY_SUCCESS = DBUS_START_REPLY_SUCCESS + START_REPLY_ALREADY_RUNNING = DBUS_START_REPLY_ALREADY_RUNNING + + def __new__(cls, address_or_type=TYPE_SESSION, mainloop=None): + bus = cls._new_for_bus(address_or_type, mainloop=mainloop) + + # _bus_names is used by dbus.service.BusName! + bus._bus_names = weakref.WeakValueDictionary() + + bus._signal_sender_matches = {} + """Map from SignalMatch to NameOwnerWatch.""" + + return bus + + def add_signal_receiver(self, handler_function, signal_name=None, + dbus_interface=None, bus_name=None, + path=None, **keywords): + named_service = keywords.pop('named_service', None) + if named_service is not None: + if bus_name is not None: + raise TypeError('bus_name and named_service cannot both be ' + 'specified') + bus_name = named_service + from warnings import warn + warn('Passing the named_service parameter to add_signal_receiver ' + 'by name is deprecated: please use positional parameters', + DeprecationWarning, stacklevel=2) + + match = super(BusConnection, self).add_signal_receiver( + handler_function, signal_name, dbus_interface, bus_name, + path, **keywords) + + if (bus_name is not None and bus_name != BUS_DAEMON_NAME): + if bus_name[:1] == ':': + def callback(new_owner): + if new_owner == '': + match.remove() + else: + callback = match.set_sender_name_owner + watch = self.watch_name_owner(bus_name, callback) + self._signal_sender_matches[match] = watch + + self.add_match_string(str(match)) + + return match + + def _clean_up_signal_match(self, match): + # The signals lock is no longer held here (it was in <= 0.81.0) + self.remove_match_string_non_blocking(str(match)) + watch = self._signal_sender_matches.pop(match, None) + if watch is not None: + watch.cancel() + + def activate_name_owner(self, bus_name): + if (bus_name is not None and bus_name[:1] != ':' + and bus_name != BUS_DAEMON_NAME): + try: + return self.get_name_owner(bus_name) + except DBusException, e: + if e.get_dbus_name() != _NAME_HAS_NO_OWNER: + raise + # else it doesn't exist: try to start it + self.start_service_by_name(bus_name) + return self.get_name_owner(bus_name) + else: + # already unique + return bus_name + + def get_object(self, bus_name, object_path, introspect=True, + follow_name_owner_changes=False, **kwargs): + """Return a local proxy for the given remote object. + + Method calls on the proxy are translated into method calls on the + remote object. + + :Parameters: + `bus_name` : str + A bus name (either the unique name or a well-known name) + of the application owning the object. The keyword argument + named_service is a deprecated alias for this. + `object_path` : str + The object path of the desired object + `introspect` : bool + If true (default), attempt to introspect the remote + object to find out supported methods and their signatures + `follow_name_owner_changes` : bool + If the object path is a well-known name and this parameter + is false (default), resolve the well-known name to the unique + name of its current owner and bind to that instead; if the + ownership of the well-known name changes in future, + keep communicating with the original owner. + This is necessary if the D-Bus API used is stateful. + + If the object path is a well-known name and this parameter + is true, whenever the well-known name changes ownership in + future, bind to the new owner, if any. + + If the given object path is a unique name, this parameter + has no effect. + + :Returns: a `dbus.proxies.ProxyObject` + :Raises `DBusException`: if resolving the well-known name to a + unique name fails + """ + if follow_name_owner_changes: + self._require_main_loop() # we don't get the signals otherwise + + named_service = kwargs.pop('named_service', None) + if named_service is not None: + if bus_name is not None: + raise TypeError('bus_name and named_service cannot both ' + 'be specified') + from warnings import warn + warn('Passing the named_service parameter to get_object by name ' + 'is deprecated: please use positional parameters', + DeprecationWarning, stacklevel=2) + bus_name = named_service + if kwargs: + raise TypeError('get_object does not take these keyword ' + 'arguments: %s' % ', '.join(kwargs.iterkeys())) + + return self.ProxyObjectClass(self, bus_name, object_path, + introspect=introspect, + follow_name_owner_changes=follow_name_owner_changes) + + def get_unix_user(self, bus_name): + """Get the numeric uid of the process owning the given bus name. + + :Parameters: + `bus_name` : str + A bus name, either unique or well-known + :Returns: a `dbus.UInt32` + :Since: 0.80.0 + """ + validate_bus_name(bus_name) + return self.call_blocking(BUS_DAEMON_NAME, BUS_DAEMON_PATH, + BUS_DAEMON_IFACE, 'GetConnectionUnixUser', + 's', (bus_name,)) + + def start_service_by_name(self, bus_name, flags=0): + """Start a service which will implement the given bus name on this Bus. + + :Parameters: + `bus_name` : str + The well-known bus name to be activated. + `flags` : dbus.UInt32 + Flags to pass to StartServiceByName (currently none are + defined) + + :Returns: A tuple of 2 elements. The first is always True, the + second is either START_REPLY_SUCCESS or + START_REPLY_ALREADY_RUNNING. + + :Raises `DBusException`: if the service could not be started. + :Since: 0.80.0 + """ + validate_bus_name(bus_name) + return (True, self.call_blocking(BUS_DAEMON_NAME, BUS_DAEMON_PATH, + BUS_DAEMON_IFACE, + 'StartServiceByName', + 'su', (bus_name, flags))) + + # XXX: it might be nice to signal IN_QUEUE, EXISTS by exception, + # but this would not be backwards-compatible + def request_name(self, name, flags=0): + """Request a bus name. + + :Parameters: + `name` : str + The well-known name to be requested + `flags` : dbus.UInt32 + A bitwise-OR of 0 or more of the flags + `NAME_FLAG_ALLOW_REPLACEMENT`, + `NAME_FLAG_REPLACE_EXISTING` + and `NAME_FLAG_DO_NOT_QUEUE` + :Returns: `REQUEST_NAME_REPLY_PRIMARY_OWNER`, + `REQUEST_NAME_REPLY_IN_QUEUE`, + `REQUEST_NAME_REPLY_EXISTS` or + `REQUEST_NAME_REPLY_ALREADY_OWNER` + :Raises `DBusException`: if the bus daemon cannot be contacted or + returns an error. + """ + validate_bus_name(name, allow_unique=False) + return self.call_blocking(BUS_DAEMON_NAME, BUS_DAEMON_PATH, + BUS_DAEMON_IFACE, 'RequestName', + 'su', (name, flags)) + + def release_name(self, name): + """Release a bus name. + + :Parameters: + `name` : str + The well-known name to be released + :Returns: `RELEASE_NAME_REPLY_RELEASED`, + `RELEASE_NAME_REPLY_NON_EXISTENT` + or `RELEASE_NAME_REPLY_NOT_OWNER` + :Raises `DBusException`: if the bus daemon cannot be contacted or + returns an error. + """ + validate_bus_name(name, allow_unique=False) + return self.call_blocking(BUS_DAEMON_NAME, BUS_DAEMON_PATH, + BUS_DAEMON_IFACE, 'ReleaseName', + 's', (name,)) + + def list_names(self): + """Return a list of all currently-owned names on the bus. + + :Returns: a dbus.Array of dbus.UTF8String + :Since: 0.81.0 + """ + return self.call_blocking(BUS_DAEMON_NAME, BUS_DAEMON_PATH, + BUS_DAEMON_IFACE, 'ListNames', + '', (), utf8_strings=True) + + def list_activatable_names(self): + """Return a list of all names that can be activated on the bus. + + :Returns: a dbus.Array of dbus.UTF8String + :Since: 0.81.0 + """ + return self.call_blocking(BUS_DAEMON_NAME, BUS_DAEMON_PATH, + BUS_DAEMON_IFACE, 'ListNames', + '', (), utf8_strings=True) + + def get_name_owner(self, bus_name): + """Return the unique connection name of the primary owner of the + given name. + + :Raises `DBusException`: if the `bus_name` has no owner + :Since: 0.81.0 + """ + validate_bus_name(bus_name, allow_unique=False) + return self.call_blocking(BUS_DAEMON_NAME, BUS_DAEMON_PATH, + BUS_DAEMON_IFACE, 'GetNameOwner', + 's', (bus_name,), utf8_strings=True) + + def watch_name_owner(self, bus_name, callback): + """Watch the unique connection name of the primary owner of the + given name. + + `callback` will be called with one argument, which is either the + unique connection name, or the empty string (meaning the name is + not owned). + + :Since: 0.81.0 + """ + return NameOwnerWatch(self, bus_name, callback) + + def name_has_owner(self, bus_name): + """Return True iff the given bus name has an owner on this bus. + + :Parameters: + `bus_name` : str + The bus name to look up + :Returns: a `bool` + """ + return bool(self.call_blocking(BUS_DAEMON_NAME, BUS_DAEMON_PATH, + BUS_DAEMON_IFACE, 'NameHasOwner', + 's', (bus_name,))) + + def add_match_string(self, rule): + """Arrange for this application to receive messages on the bus that + match the given rule. This version will block. + + :Parameters: + `rule` : str + The match rule + :Raises `DBusException`: on error. + :Since: 0.80.0 + """ + self.call_blocking(BUS_DAEMON_NAME, BUS_DAEMON_PATH, + BUS_DAEMON_IFACE, 'AddMatch', 's', (rule,)) + + # FIXME: add an async success/error handler capability? + # (and the same for remove_...) + def add_match_string_non_blocking(self, rule): + """Arrange for this application to receive messages on the bus that + match the given rule. This version will not block, but any errors + will be ignored. + + + :Parameters: + `rule` : str + The match rule + :Raises `DBusException`: on error. + :Since: 0.80.0 + """ + self.call_async(BUS_DAEMON_NAME, BUS_DAEMON_PATH, + BUS_DAEMON_IFACE, 'AddMatch', 's', (rule,), + None, None) + + def remove_match_string(self, rule): + """Arrange for this application to receive messages on the bus that + match the given rule. This version will block. + + :Parameters: + `rule` : str + The match rule + :Raises `DBusException`: on error. + :Since: 0.80.0 + """ + self.call_blocking(BUS_DAEMON_NAME, BUS_DAEMON_PATH, + BUS_DAEMON_IFACE, 'RemoveMatch', 's', (rule,)) + + def remove_match_string_non_blocking(self, rule): + """Arrange for this application to receive messages on the bus that + match the given rule. This version will not block, but any errors + will be ignored. + + + :Parameters: + `rule` : str + The match rule + :Raises `DBusException`: on error. + :Since: 0.80.0 + """ + self.call_async(BUS_DAEMON_NAME, BUS_DAEMON_PATH, + BUS_DAEMON_IFACE, 'RemoveMatch', 's', (rule,), + None, None) diff --git a/dbus/connection.py b/dbus/connection.py new file mode 100644 index 0000000..d76aaf2 --- /dev/null +++ b/dbus/connection.py @@ -0,0 +1,646 @@ +# Copyright (C) 2007 Collabora Ltd. <http://www.collabora.co.uk/> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +__all__ = ('Connection', 'SignalMatch') +__docformat__ = 'reStructuredText' + +import logging +try: + import thread +except ImportError: + import dummy_thread as thread +import weakref + +from _dbus_bindings import Connection as _Connection, \ + LOCAL_PATH, LOCAL_IFACE, \ + validate_interface_name, validate_member_name,\ + validate_bus_name, validate_object_path,\ + validate_error_name, \ + UTF8String +from dbus.exceptions import DBusException +from dbus.lowlevel import ErrorMessage, MethodCallMessage, SignalMessage, \ + MethodReturnMessage, HANDLER_RESULT_NOT_YET_HANDLED +from dbus.proxies import ProxyObject + + +_logger = logging.getLogger('dbus.connection') + + +def _noop(*args, **kwargs): + pass + + +class SignalMatch(object): + __slots__ = ('_sender_name_owner', '_member', '_interface', '_sender', + '_path', '_handler', '_args_match', '_rule', + '_utf8_strings', '_byte_arrays', '_conn_weakref', + '_destination_keyword', '_interface_keyword', + '_message_keyword', '_member_keyword', + '_sender_keyword', '_path_keyword', '_int_args_match') + + def __init__(self, conn, sender, object_path, dbus_interface, + member, handler, utf8_strings=False, byte_arrays=False, + sender_keyword=None, path_keyword=None, + interface_keyword=None, member_keyword=None, + message_keyword=None, destination_keyword=None, + **kwargs): + if member is not None: + validate_member_name(member) + if dbus_interface is not None: + validate_interface_name(dbus_interface) + if sender is not None: + validate_bus_name(sender) + if object_path is not None: + validate_object_path(object_path) + + self._rule = None + self._conn_weakref = weakref.ref(conn) + self._sender = sender + self._interface = dbus_interface + self._member = member + self._path = object_path + self._handler = handler + + # if the connection is actually a bus, it's responsible for changing + # this later + self._sender_name_owner = sender + + self._utf8_strings = utf8_strings + self._byte_arrays = byte_arrays + self._sender_keyword = sender_keyword + self._path_keyword = path_keyword + self._member_keyword = member_keyword + self._interface_keyword = interface_keyword + self._message_keyword = message_keyword + self._destination_keyword = destination_keyword + + self._args_match = kwargs + if not kwargs: + self._int_args_match = None + else: + self._int_args_match = {} + for kwarg in kwargs: + if not kwarg.startswith('arg'): + raise TypeError('SignalMatch: unknown keyword argument %s' + % kwarg) + try: + index = int(kwarg[3:]) + except ValueError: + raise TypeError('SignalMatch: unknown keyword argument %s' + % kwarg) + if index < 0 or index > 63: + raise TypeError('SignalMatch: arg match index must be in ' + 'range(64), not %d' % index) + self._int_args_match[index] = kwargs[kwarg] + + def __hash__(self): + """SignalMatch objects are compared by identity.""" + return hash(id(self)) + + def __eq__(self, other): + """SignalMatch objects are compared by identity.""" + return self is other + + def __ne__(self, other): + """SignalMatch objects are compared by identity.""" + return self is not other + + sender = property(lambda self: self._sender) + + def __str__(self): + if self._rule is None: + rule = ["type='signal'"] + if self._sender is not None: + rule.append("sender='%s'" % self._sender) + if self._path is not None: + rule.append("path='%s'" % self._path) + if self._interface is not None: + rule.append("interface='%s'" % self._interface) + if self._member is not None: + rule.append("member='%s'" % self._member) + if self._int_args_match is not None: + for index, value in self._int_args_match.iteritems(): + rule.append("arg%d='%s'" % (index, value)) + + self._rule = ','.join(rule) + + return self._rule + + def __repr__(self): + return ('<%s at %x "%s" on conn %r>' + % (self.__class__, id(self), self._rule, self._conn_weakref())) + + def set_sender_name_owner(self, new_name): + self._sender_name_owner = new_name + + def matches_removal_spec(self, sender, object_path, + dbus_interface, member, handler, **kwargs): + if handler not in (None, self._handler): + return False + if sender != self._sender: + return False + if object_path != self._path: + return False + if dbus_interface != self._interface: + return False + if member != self._member: + return False + if kwargs != self._args_match: + return False + return True + + def maybe_handle_message(self, message): + args = None + + # these haven't been checked yet by the match tree + if self._sender_name_owner not in (None, message.get_sender()): + return False + if self._int_args_match is not None: + # extracting args with utf8_strings and byte_arrays is less work + args = message.get_args_list(utf8_strings=True, byte_arrays=True) + for index, value in self._int_args_match.iteritems(): + if (index >= len(args) + or not isinstance(args[index], UTF8String) + or args[index] != value): + return False + + # these have likely already been checked by the match tree + if self._member not in (None, message.get_member()): + return False + if self._interface not in (None, message.get_interface()): + return False + if self._path not in (None, message.get_path()): + return False + + try: + # minor optimization: if we already extracted the args with the + # right calling convention to do the args match, don't bother + # doing so again + if args is None or not self._utf8_strings or not self._byte_arrays: + args = message.get_args_list(utf8_strings=self._utf8_strings, + byte_arrays=self._byte_arrays) + kwargs = {} + if self._sender_keyword is not None: + kwargs[self._sender_keyword] = message.get_sender() + if self._destination_keyword is not None: + kwargs[self._destination_keyword] = message.get_destination() + if self._path_keyword is not None: + kwargs[self._path_keyword] = message.get_path() + if self._member_keyword is not None: + kwargs[self._member_keyword] = message.get_member() + if self._interface_keyword is not None: + kwargs[self._interface_keyword] = message.get_interface() + if self._message_keyword is not None: + kwargs[self._message_keyword] = message + self._handler(*args, **kwargs) + except: + # basicConfig is a no-op if logging is already configured + logging.basicConfig() + _logger.error('Exception in handler for D-Bus signal:', exc_info=1) + + return True + + def remove(self): + conn = self._conn_weakref() + # do nothing if the connection has already vanished + if conn is not None: + conn.remove_signal_receiver(self, self._member, + self._interface, self._sender, + self._path, + **self._args_match) + + +class Connection(_Connection): + """A connection to another application. In this base class there is + assumed to be no bus daemon. + + :Since: 0.81.0 + """ + + ProxyObjectClass = ProxyObject + + def __init__(self, *args, **kwargs): + super(Connection, self).__init__(*args, **kwargs) + + # this if-block is needed because shared bus connections can be + # __init__'ed more than once + if not hasattr(self, '_dbus_Connection_initialized'): + self._dbus_Connection_initialized = 1 + + self.__call_on_disconnection = [] + + self._signal_recipients_by_object_path = {} + """Map from object path to dict mapping dbus_interface to dict + mapping member to list of SignalMatch objects.""" + + self._signals_lock = thread.allocate_lock() + """Lock used to protect signal data structures""" + + self.add_message_filter(self.__class__._signal_func) + + def activate_name_owner(self, bus_name): + """Return the unique name for the given bus name, activating it + if necessary and possible. + + If the name is already unique or this connection is not to a + bus daemon, just return it. + + :Returns: a bus name. If the given `bus_name` exists, the returned + name identifies its current owner; otherwise the returned name + does not exist. + :Raises DBusException: if the implementation has failed + to activate the given bus name. + :Since: 0.81.0 + """ + return bus_name + + def get_object(self, bus_name=None, object_path=None, introspect=True, + **kwargs): + """Return a local proxy for the given remote object. + + Method calls on the proxy are translated into method calls on the + remote object. + + :Parameters: + `bus_name` : str + A bus name (either the unique name or a well-known name) + of the application owning the object. The keyword argument + named_service is a deprecated alias for this. + `object_path` : str + The object path of the desired object + `introspect` : bool + If true (default), attempt to introspect the remote + object to find out supported methods and their signatures + + :Returns: a `dbus.proxies.ProxyObject` + """ + named_service = kwargs.pop('named_service', None) + if named_service is not None: + if bus_name is not None: + raise TypeError('bus_name and named_service cannot both ' + 'be specified') + from warnings import warn + warn('Passing the named_service parameter to get_object by name ' + 'is deprecated: please use positional parameters', + DeprecationWarning, stacklevel=2) + bus_name = named_service + if kwargs: + raise TypeError('get_object does not take these keyword ' + 'arguments: %s' % ', '.join(kwargs.iterkeys())) + + return self.ProxyObjectClass(self, bus_name, object_path, + introspect=introspect) + + def add_signal_receiver(self, handler_function, + signal_name=None, + dbus_interface=None, + bus_name=None, + path=None, + **keywords): + """Arrange for the given function to be called when a signal matching + the parameters is received. + + :Parameters: + `handler_function` : callable + The function to be called. Its positional arguments will + be the arguments of the signal. By default it will receive + no keyword arguments, but see the description of + the optional keyword arguments below. + `signal_name` : str + The signal name; None (the default) matches all names + `dbus_interface` : str + The D-Bus interface name with which to qualify the signal; + None (the default) matches all interface names + `bus_name` : str + A bus name for the sender, which will be resolved to a + unique name if it is not already; None (the default) matches + any sender. + `path` : str + The object path of the object which must have emitted the + signal; None (the default) matches any object path + :Keywords: + `utf8_strings` : bool + If True, the handler function will receive any string + arguments as dbus.UTF8String objects (a subclass of str + guaranteed to be UTF-8). If False (default) it will receive + any string arguments as dbus.String objects (a subclass of + unicode). + `byte_arrays` : bool + If True, the handler function will receive any byte-array + arguments as dbus.ByteArray objects (a subclass of str). + If False (default) it will receive any byte-array + arguments as a dbus.Array of dbus.Byte (subclasses of: + a list of ints). + `sender_keyword` : str + If not None (the default), the handler function will receive + the unique name of the sending endpoint as a keyword + argument with this name. + `destination_keyword` : str + If not None (the default), the handler function will receive + the bus name of the destination (or None if the signal is a + broadcast, as is usual) as a keyword argument with this name. + `interface_keyword` : str + If not None (the default), the handler function will receive + the signal interface as a keyword argument with this name. + `member_keyword` : str + If not None (the default), the handler function will receive + the signal name as a keyword argument with this name. + `path_keyword` : str + If not None (the default), the handler function will receive + the object-path of the sending object as a keyword argument + with this name. + `message_keyword` : str + If not None (the default), the handler function will receive + the `dbus.lowlevel.SignalMessage` as a keyword argument with + this name. + `arg...` : unicode or UTF-8 str + If there are additional keyword parameters of the form + ``arg``\ *n*, match only signals where the *n*\ th argument + is the value given for that keyword parameter. As of this + time only string arguments can be matched (in particular, + object paths and signatures can't). + `named_service` : str + A deprecated alias for `bus_name`. + """ + self._require_main_loop() + + named_service = keywords.pop('named_service', None) + if named_service is not None: + if bus_name is not None: + raise TypeError('bus_name and named_service cannot both be ' + 'specified') + bus_name = named_service + from warnings import warn + warn('Passing the named_service parameter to add_signal_receiver ' + 'by name is deprecated: please use positional parameters', + DeprecationWarning, stacklevel=2) + + match = SignalMatch(self, bus_name, path, dbus_interface, + signal_name, handler_function, **keywords) + + self._signals_lock.acquire() + try: + by_interface = self._signal_recipients_by_object_path.setdefault( + path, {}) + by_member = by_interface.setdefault(dbus_interface, {}) + matches = by_member.setdefault(signal_name, []) + + matches.append(match) + finally: + self._signals_lock.release() + + return match + + def _iter_easy_matches(self, path, dbus_interface, member): + if path is not None: + path_keys = (None, path) + else: + path_keys = (None,) + if dbus_interface is not None: + interface_keys = (None, dbus_interface) + else: + interface_keys = (None,) + if member is not None: + member_keys = (None, member) + else: + member_keys = (None,) + + for path in path_keys: + by_interface = self._signal_recipients_by_object_path.get(path, + None) + if by_interface is None: + continue + for dbus_interface in interface_keys: + by_member = by_interface.get(dbus_interface, None) + if by_member is None: + continue + for member in member_keys: + matches = by_member.get(member, None) + if matches is None: + continue + for m in matches: + yield m + + def remove_signal_receiver(self, handler_or_match, + signal_name=None, + dbus_interface=None, + bus_name=None, + path=None, + **keywords): + named_service = keywords.pop('named_service', None) + if named_service is not None: + if bus_name is not None: + raise TypeError('bus_name and named_service cannot both be ' + 'specified') + bus_name = named_service + from warnings import warn + warn('Passing the named_service parameter to ' + 'remove_signal_receiver by name is deprecated: please use ' + 'positional parameters', + DeprecationWarning, stacklevel=2) + + new = [] + deletions = [] + self._signals_lock.acquire() + try: + by_interface = self._signal_recipients_by_object_path.get(path, + None) + if by_interface is None: + return + by_member = by_interface.get(dbus_interface, None) + if by_member is None: + return + matches = by_member.get(signal_name, None) + if matches is None: + return + + for match in matches: + if (handler_or_match is match + or match.matches_removal_spec(bus_name, + path, + dbus_interface, + signal_name, + handler_or_match, + **keywords)): + deletions.append(match) + else: + new.append(match) + + if new: + by_member[signal_name] = new + else: + del by_member[signal_name] + if not by_member: + del by_interface[dbus_interface] + if not by_interface: + del self._signal_recipients_by_object_path[path] + finally: + self._signals_lock.release() + + for match in deletions: + self._clean_up_signal_match(match) + + def _clean_up_signal_match(self, match): + # Now called without the signals lock held (it was held in <= 0.81.0) + pass + + def _signal_func(self, message): + """D-Bus filter function. Handle signals by dispatching to Python + callbacks kept in the match-rule tree. + """ + + if not isinstance(message, SignalMessage): + return HANDLER_RESULT_NOT_YET_HANDLED + + dbus_interface = message.get_interface() + path = message.get_path() + signal_name = message.get_member() + + for match in self._iter_easy_matches(path, dbus_interface, + signal_name): + match.maybe_handle_message(message) + + if (dbus_interface == LOCAL_IFACE and + path == LOCAL_PATH and + signal_name == 'Disconnected'): + for cb in self.__call_on_disconnection: + try: + cb(self) + except Exception, e: + # basicConfig is a no-op if logging is already configured + logging.basicConfig() + _logger.error('Exception in handler for Disconnected ' + 'signal:', exc_info=1) + + return HANDLER_RESULT_NOT_YET_HANDLED + + def call_async(self, bus_name, object_path, dbus_interface, method, + signature, args, reply_handler, error_handler, + timeout=-1.0, utf8_strings=False, byte_arrays=False, + require_main_loop=True): + """Call the given method, asynchronously. + + If the reply_handler is None, successful replies will be ignored. + If the error_handler is None, failures will be ignored. If both + are None, the implementation may request that no reply is sent. + + :Returns: The dbus.lowlevel.PendingCall. + :Since: 0.81.0 + """ + if object_path == LOCAL_PATH: + raise DBusException('Methods may not be called on the reserved ' + 'path %s' % LOCAL_PATH) + if dbus_interface == LOCAL_IFACE: + raise DBusException('Methods may not be called on the reserved ' + 'interface %s' % LOCAL_IFACE) + # no need to validate other args - MethodCallMessage ctor will do + + get_args_opts = {'utf8_strings': utf8_strings, + 'byte_arrays': byte_arrays} + + message = MethodCallMessage(destination=bus_name, + path=object_path, + interface=dbus_interface, + method=method) + # Add the arguments to the function + try: + message.append(signature=signature, *args) + except Exception, e: + logging.basicConfig() + _logger.error('Unable to set arguments %r according to ' + 'signature %r: %s: %s', + args, signature, e.__class__, e) + raise + + if reply_handler is None and error_handler is None: + # we don't care what happens, so just send it + self.send_message(message) + return + + if reply_handler is None: + reply_handler = _noop + if error_handler is None: + error_handler = _noop + + def msg_reply_handler(message): + if isinstance(message, MethodReturnMessage): + reply_handler(*message.get_args_list(**get_args_opts)) + elif isinstance(message, ErrorMessage): + error_handler(DBusException(name=message.get_error_name(), + *message.get_args_list())) + else: + error_handler(TypeError('Unexpected type for reply ' + 'message: %r' % message)) + return self.send_message_with_reply(message, msg_reply_handler, + timeout, + require_main_loop=require_main_loop) + + def call_blocking(self, bus_name, object_path, dbus_interface, method, + signature, args, timeout=-1.0, utf8_strings=False, + byte_arrays=False): + """Call the given method, synchronously. + :Since: 0.81.0 + """ + if object_path == LOCAL_PATH: + raise DBusException('Methods may not be called on the reserved ' + 'path %s' % LOCAL_PATH) + if dbus_interface == LOCAL_IFACE: + raise DBusException('Methods may not be called on the reserved ' + 'interface %s' % LOCAL_IFACE) + # no need to validate other args - MethodCallMessage ctor will do + + get_args_opts = {'utf8_strings': utf8_strings, + 'byte_arrays': byte_arrays} + + message = MethodCallMessage(destination=bus_name, + path=object_path, + interface=dbus_interface, + method=method) + # Add the arguments to the function + try: + message.append(signature=signature, *args) + except Exception, e: + logging.basicConfig() + _logger.error('Unable to set arguments %r according to ' + 'signature %r: %s: %s', + args, signature, e.__class__, e) + raise + + # make a blocking call + reply_message = self.send_message_with_reply_and_block( + message, timeout) + args_list = reply_message.get_args_list(**get_args_opts) + if len(args_list) == 0: + return None + elif len(args_list) == 1: + return args_list[0] + else: + return tuple(args_list) + + def call_on_disconnection(self, callable): + """Arrange for `callable` to be called with one argument (this + Connection object) when the Connection becomes + disconnected. + + :Since: 0.83.0 + """ + self.__call_on_disconnection.append(callable) diff --git a/dbus/dbus_bindings.py b/dbus/dbus_bindings.py new file mode 100644 index 0000000..a45ca9f --- /dev/null +++ b/dbus/dbus_bindings.py @@ -0,0 +1,37 @@ +# Backwards-compatibility with the old dbus_bindings. + +from warnings import warn as _warn + +_dbus_bindings_warning = DeprecationWarning("""\ +The dbus_bindings module is not public API and will go away soon. + +Most uses of dbus_bindings are applications catching the exception +dbus.dbus_bindings.DBusException. You should use dbus.DBusException +instead (this is compatible with all dbus-python versions since 0.40.2). + +If you need additional public API, please contact the maintainers via +<dbus@lists.freedesktop.org>. +""") + +_warn(_dbus_bindings_warning, DeprecationWarning, stacklevel=2) + +# Exceptions +from dbus.exceptions import DBusException +class ConnectionError(Exception): pass + +# Types +from dbus.types import * + +# Messages +from _dbus_bindings import Message, SignalMessage as Signal,\ + MethodCallMessage as MethodCall,\ + MethodReturnMessage as MethodReturn,\ + ErrorMessage as Error +# MessageIter has gone away, thankfully + +# Connection +from _dbus_bindings import Connection + +from dbus import Bus +bus_request_name = Bus.request_name +bus_release_name = Bus.release_name diff --git a/dbus/decorators.py b/dbus/decorators.py new file mode 100644 index 0000000..a5ca281 --- /dev/null +++ b/dbus/decorators.py @@ -0,0 +1,340 @@ +"""Service-side D-Bus decorators.""" + +# Copyright (C) 2003, 2004, 2005, 2006 Red Hat Inc. <http://www.redhat.com/> +# Copyright (C) 2003 David Zeuthen +# Copyright (C) 2004 Rob Taylor +# Copyright (C) 2005, 2006 Collabora Ltd. <http://www.collabora.co.uk/> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +__all__ = ('method', 'signal') +__docformat__ = 'restructuredtext' + +import inspect + +from dbus import validate_interface_name, Signature, validate_member_name +from dbus.lowlevel import SignalMessage +from dbus.exceptions import DBusException + + +def method(dbus_interface, in_signature=None, out_signature=None, + async_callbacks=None, + sender_keyword=None, path_keyword=None, destination_keyword=None, + message_keyword=None, connection_keyword=None, + utf8_strings=False, byte_arrays=False, + rel_path_keyword=None): + """Factory for decorators used to mark methods of a `dbus.service.Object` + to be exported on the D-Bus. + + The decorated method will be exported over D-Bus as the method of the + same name on the given D-Bus interface. + + :Parameters: + `dbus_interface` : str + Name of a D-Bus interface + `in_signature` : str or None + If not None, the signature of the method parameters in the usual + D-Bus notation + `out_signature` : str or None + If not None, the signature of the return value in the usual + D-Bus notation + `async_callbacks` : tuple containing (str,str), or None + If None (default) the decorated method is expected to return + values matching the `out_signature` as usual, or raise + an exception on error. If not None, the following applies: + + `async_callbacks` contains the names of two keyword arguments to + the decorated function, which will be used to provide a success + callback and an error callback (in that order). + + When the decorated method is called via the D-Bus, its normal + return value will be ignored; instead, a pair of callbacks are + passed as keyword arguments, and the decorated method is + expected to arrange for one of them to be called. + + On success the success callback must be called, passing the + results of this method as positional parameters in the format + given by the `out_signature`. + + On error the decorated method may either raise an exception + before it returns, or arrange for the error callback to be + called with an Exception instance as parameter. + + `sender_keyword` : str or None + If not None, contains the name of a keyword argument to the + decorated function, conventionally ``'sender'``. When the + method is called, the sender's unique name will be passed as + this keyword argument. + + `path_keyword` : str or None + If not None (the default), the decorated method will receive + the destination object path as a keyword argument with this + name. Normally you already know the object path, but in the + case of "fallback paths" you'll usually want to use the object + path in the method's implementation. + + For fallback objects, `rel_path_keyword` (new in 0.82.2) is + likely to be more useful. + + :Since: 0.80.0? + + `rel_path_keyword` : str or None + If not None (the default), the decorated method will receive + the destination object path, relative to the path at which the + object was exported, as a keyword argument with this + name. For non-fallback objects the relative path will always be + '/'. + + :Since: 0.82.2 + + `destination_keyword` : str or None + If not None (the default), the decorated method will receive + the destination bus name as a keyword argument with this name. + Included for completeness - you shouldn't need this. + + :Since: 0.80.0? + + `message_keyword` : str or None + If not None (the default), the decorated method will receive + the `dbus.lowlevel.MethodCallMessage` as a keyword argument + with this name. + + :Since: 0.80.0? + + `connection_keyword` : str or None + If not None (the default), the decorated method will receive + the `dbus.connection.Connection` as a keyword argument + with this name. This is generally only useful for objects + that are available on more than one connection. + + :Since: 0.82.0 + + `utf8_strings` : bool + If False (default), D-Bus strings are passed to the decorated + method as objects of class dbus.String, a unicode subclass. + + If True, D-Bus strings are passed to the decorated method + as objects of class dbus.UTF8String, a str subclass guaranteed + to be encoded in UTF-8. + + This option does not affect object-paths and signatures, which + are always 8-bit strings (str subclass) encoded in ASCII. + + :Since: 0.80.0 + + `byte_arrays` : bool + If False (default), a byte array will be passed to the decorated + method as an `Array` (a list subclass) of `Byte` objects. + + If True, a byte array will be passed to the decorated method as + a `ByteArray`, a str subclass. This is usually what you want, + but is switched off by default to keep dbus-python's API + consistent. + + :Since: 0.80.0 + """ + validate_interface_name(dbus_interface) + + def decorator(func): + args = inspect.getargspec(func)[0] + args.pop(0) + + if async_callbacks: + if type(async_callbacks) != tuple: + raise TypeError('async_callbacks must be a tuple of (keyword for return callback, keyword for error callback)') + if len(async_callbacks) != 2: + raise ValueError('async_callbacks must be a tuple of (keyword for return callback, keyword for error callback)') + args.remove(async_callbacks[0]) + args.remove(async_callbacks[1]) + + if sender_keyword: + args.remove(sender_keyword) + if rel_path_keyword: + args.remove(rel_path_keyword) + if path_keyword: + args.remove(path_keyword) + if destination_keyword: + args.remove(destination_keyword) + if message_keyword: + args.remove(message_keyword) + if connection_keyword: + args.remove(connection_keyword) + + if in_signature: + in_sig = tuple(Signature(in_signature)) + + if len(in_sig) > len(args): + raise ValueError, 'input signature is longer than the number of arguments taken' + elif len(in_sig) < len(args): + raise ValueError, 'input signature is shorter than the number of arguments taken' + + func._dbus_is_method = True + func._dbus_async_callbacks = async_callbacks + func._dbus_interface = dbus_interface + func._dbus_in_signature = in_signature + func._dbus_out_signature = out_signature + func._dbus_sender_keyword = sender_keyword + func._dbus_path_keyword = path_keyword + func._dbus_rel_path_keyword = rel_path_keyword + func._dbus_destination_keyword = destination_keyword + func._dbus_message_keyword = message_keyword + func._dbus_connection_keyword = connection_keyword + func._dbus_args = args + func._dbus_get_args_options = {'byte_arrays': byte_arrays, + 'utf8_strings': utf8_strings} + return func + + return decorator + + +def signal(dbus_interface, signature=None, path_keyword=None, + rel_path_keyword=None): + """Factory for decorators used to mark methods of a `dbus.service.Object` + to emit signals on the D-Bus. + + Whenever the decorated method is called in Python, after the method + body is executed, a signal with the same name as the decorated method, + with the given D-Bus interface, will be emitted from this object. + + :Parameters: + `dbus_interface` : str + The D-Bus interface whose signal is emitted + `signature` : str + The signature of the signal in the usual D-Bus notation + + `path_keyword` : str or None + A keyword argument to the decorated method. If not None, + that argument will not be emitted as an argument of + the signal, and when the signal is emitted, it will appear + to come from the object path given by the keyword argument. + + Note that when calling the decorated method, you must always + pass in the object path as a keyword argument, not as a + positional argument. + + This keyword argument cannot be used on objects where + the class attribute ``SUPPORTS_MULTIPLE_OBJECT_PATHS`` is true. + + :Deprecated: since 0.82.0. Use `rel_path_keyword` instead. + + `rel_path_keyword` : str or None + A keyword argument to the decorated method. If not None, + that argument will not be emitted as an argument of + the signal. + + When the signal is emitted, if the named keyword argument is given, + the signal will appear to come from the object path obtained by + appending the keyword argument to the object's object path. + This is useful to implement "fallback objects" (objects which + own an entire subtree of the object-path tree). + + If the object is available at more than one object-path on the + same or different connections, the signal will be emitted at + an appropriate object-path on each connection - for instance, + if the object is exported at /abc on connection 1 and at + /def and /x/y/z on connection 2, and the keyword argument is + /foo, then signals will be emitted from /abc/foo and /def/foo + on connection 1, and /x/y/z/foo on connection 2. + + :Since: 0.82.0 + """ + validate_interface_name(dbus_interface) + + if path_keyword is not None: + from warnings import warn + warn(DeprecationWarning('dbus.service.signal::path_keyword has been ' + 'deprecated since dbus-python 0.82.0, and ' + 'will not work on objects that support ' + 'multiple object paths'), + DeprecationWarning, stacklevel=2) + if rel_path_keyword is not None: + raise TypeError('dbus.service.signal::path_keyword and ' + 'rel_path_keyword cannot both be used') + + def decorator(func): + member_name = func.__name__ + validate_member_name(member_name) + + def emit_signal(self, *args, **keywords): + abs_path = None + if path_keyword is not None: + if self.SUPPORTS_MULTIPLE_OBJECT_PATHS: + raise TypeError('path_keyword cannot be used on the ' + 'signals of an object that supports ' + 'multiple object paths') + abs_path = keywords.pop(path_keyword, None) + if (abs_path != self.__dbus_object_path__ and + not self.__dbus_object_path__.startswith(abs_path + '/')): + raise ValueError('Path %r is not below %r', abs_path, + self.__dbus_object_path__) + + rel_path = None + if rel_path_keyword is not None: + rel_path = keywords.pop(rel_path_keyword, None) + + func(self, *args, **keywords) + + for location in self.locations: + if abs_path is None: + # non-deprecated case + if rel_path is None or rel_path in ('/', ''): + object_path = location[1] + else: + # will be validated by SignalMessage ctor in a moment + object_path = location[1] + rel_path + else: + object_path = abs_path + + message = SignalMessage(object_path, + dbus_interface, + member_name) + message.append(signature=signature, *args) + + location[0].send_message(message) + # end emit_signal + + args = inspect.getargspec(func)[0] + args.pop(0) + + for keyword in rel_path_keyword, path_keyword: + if keyword is not None: + try: + args.remove(keyword) + except ValueError: + raise ValueError('function has no argument "%s"' % keyword) + + if signature: + sig = tuple(Signature(signature)) + + if len(sig) > len(args): + raise ValueError, 'signal signature is longer than the number of arguments provided' + elif len(sig) < len(args): + raise ValueError, 'signal signature is shorter than the number of arguments provided' + + emit_signal.__name__ = func.__name__ + emit_signal.__doc__ = func.__doc__ + emit_signal._dbus_is_signal = True + emit_signal._dbus_interface = dbus_interface + emit_signal._dbus_signature = signature + emit_signal._dbus_args = args + return emit_signal + + return decorator diff --git a/dbus/exceptions.py b/dbus/exceptions.py new file mode 100644 index 0000000..8d84a29 --- /dev/null +++ b/dbus/exceptions.py @@ -0,0 +1,106 @@ +"""D-Bus exceptions.""" + +# Copyright (C) 2007 Collabora Ltd. <http://www.collabora.co.uk/> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +__all__ = ('DBusException', 'MissingErrorHandlerException', + 'MissingReplyHandlerException', 'ValidationException', + 'IntrospectionParserException', 'UnknownMethodException', + 'NameExistsException') + +class DBusException(Exception): + + include_traceback = False + """If True, tracebacks will be included in the exception message sent to + D-Bus clients. + + Exceptions that are not DBusException subclasses always behave + as though this is True. Set this to True on DBusException subclasses + that represent a programming error, and leave it False on subclasses that + represent an expected failure condition (e.g. a network server not + responding).""" + + def __init__(self, *args, **kwargs): + name = kwargs.pop('name', None) + if name is not None or getattr(self, '_dbus_error_name', None) is None: + self._dbus_error_name = name + if kwargs: + raise TypeError('DBusException does not take keyword arguments: %s' + % ', '.join(kwargs.keys())) + Exception.__init__(self, *args) + + def __str__(self): + s = Exception.__str__(self) + if self._dbus_error_name is not None: + return '%s: %s' % (self._dbus_error_name, s) + else: + return s + + def get_dbus_message(self): + s = Exception.__str__(self) + return s.decode('utf-8', 'replace') + + def get_dbus_name(self): + return self._dbus_error_name + +class MissingErrorHandlerException(DBusException): + + include_traceback = True + + def __init__(self): + DBusException.__init__(self, "error_handler not defined: if you define a reply_handler you must also define an error_handler") + +class MissingReplyHandlerException(DBusException): + + include_traceback = True + + def __init__(self): + DBusException.__init__(self, "reply_handler not defined: if you define an error_handler you must also define a reply_handler") + +class ValidationException(DBusException): + + include_traceback = True + + def __init__(self, msg=''): + DBusException.__init__(self, "Error validating string: %s"%msg) + +class IntrospectionParserException(DBusException): + + include_traceback = True + + def __init__(self, msg=''): + DBusException.__init__(self, "Error parsing introspect data: %s"%msg) + +class UnknownMethodException(DBusException): + + include_traceback = True + _dbus_error_name = 'org.freedesktop.DBus.Error.UnknownMethod' + + def __init__(self, method): + DBusException.__init__(self, "Unknown method: %s"%method) + +class NameExistsException(DBusException): + + include_traceback = True + + def __init__(self, name): + DBusException.__init__(self, "Bus name already exists: %s"%name) diff --git a/dbus/glib.py b/dbus/glib.py new file mode 100644 index 0000000..610de53 --- /dev/null +++ b/dbus/glib.py @@ -0,0 +1,41 @@ +# Copyright (C) 2004 Anders Carlsson +# Copyright (C) 2004, 2005, 2006 Red Hat Inc. <http://www.redhat.com/> +# Copyright (C) 2005, 2006 Collabora Ltd. <http://www.collabora.co.uk/> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +"""Deprecated module which sets the default GLib main context as the mainloop +implementation within D-Bus, as a side-effect of being imported! + +This API is highly non-obvious, so instead of importing this module, +new programs which don't need pre-0.80 compatibility should use this +equivalent code:: + + from dbus.mainloop.glib import DBusGMainLoop + DBusGMainLoop(set_as_default=True) +""" +__docformat__ = 'restructuredtext' + +from dbus.mainloop.glib import DBusGMainLoop, threads_init + +init_threads = threads_init + +DBusGMainLoop(set_as_default=True) diff --git a/dbus/gobject_service.py b/dbus/gobject_service.py new file mode 100644 index 0000000..61a7749 --- /dev/null +++ b/dbus/gobject_service.py @@ -0,0 +1,71 @@ +"""Support code for implementing D-Bus services via GObjects.""" + +# Copyright (C) 2007 Collabora Ltd. <http://www.collabora.co.uk/> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import gobject +import dbus.service + +class ExportedGObjectType(gobject.GObjectMeta, dbus.service.InterfaceType): + """A metaclass which inherits from both GObjectMeta and + `dbus.service.InterfaceType`. Used as the metaclass for `ExportedGObject`. + """ + def __init__(cls, name, bases, dct): + gobject.GObjectMeta.__init__(cls, name, bases, dct) + dbus.service.InterfaceType.__init__(cls, name, bases, dct) + +class ExportedGObject(gobject.GObject, dbus.service.Object): + """A GObject which is exported on the D-Bus. + + Because GObject and `dbus.service.Object` both have custom metaclasses, + the naive approach using simple multiple inheritance won't work. This + class has `ExportedGObjectType` as its metaclass, which is sufficient + to make it work correctly. + """ + __metaclass__ = ExportedGObjectType + + def __init__(self, conn=None, object_path=None, **kwargs): + """Initialize an exported GObject. + + :Parameters: + `conn` : dbus.connection.Connection + The D-Bus connection or bus + `object_path` : str + The object path at which to register this object. + :Keywords: + `bus_name` : dbus.service.BusName + A bus name to be held on behalf of this object, or None. + `gobject_properties` : dict + GObject properties to be set on the constructed object. + + Any unrecognised keyword arguments will also be interpreted + as GObject properties. + """ + bus_name = kwargs.pop('bus_name', None) + gobject_properties = kwargs.pop('gobject_properties', None) + + if gobject_properties is not None: + kwargs.update(gobject_properties) + gobject.GObject.__init__(self, **kwargs) + dbus.service.Object.__init__(self, conn=conn, + object_path=object_path, + bus_name=bus_name) diff --git a/dbus/lowlevel.py b/dbus/lowlevel.py new file mode 100644 index 0000000..2754344 --- /dev/null +++ b/dbus/lowlevel.py @@ -0,0 +1,40 @@ +# Copyright (C) 2006 Collabora Ltd. <http://www.collabora.co.uk/> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +"""Low-level interface to D-Bus.""" + +__all__ = ('PendingCall', 'Message', 'MethodCallMessage', + 'MethodReturnMessage', 'ErrorMessage', 'SignalMessage', + 'HANDLER_RESULT_HANDLED', 'HANDLER_RESULT_NOT_YET_HANDLED', + 'MESSAGE_TYPE_INVALID', 'MESSAGE_TYPE_METHOD_CALL', + 'MESSAGE_TYPE_METHOD_RETURN', 'MESSAGE_TYPE_ERROR', + 'MESSAGE_TYPE_SIGNAL') + +from _dbus_bindings import PendingCall, Message, MethodCallMessage, \ + MethodReturnMessage, ErrorMessage, SignalMessage, \ + HANDLER_RESULT_HANDLED, \ + HANDLER_RESULT_NOT_YET_HANDLED, \ + MESSAGE_TYPE_INVALID, \ + MESSAGE_TYPE_METHOD_CALL, \ + MESSAGE_TYPE_METHOD_RETURN, \ + MESSAGE_TYPE_ERROR, \ + MESSAGE_TYPE_SIGNAL diff --git a/dbus/mainloop/__init__.py b/dbus/mainloop/__init__.py new file mode 100644 index 0000000..dfaeefb --- /dev/null +++ b/dbus/mainloop/__init__.py @@ -0,0 +1,62 @@ +# Copyright (C) 2006 Collabora Ltd. <http://www.collabora.co.uk/> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +"""Base definitions, etc. for main loop integration. + +""" + +import _dbus_bindings + +NativeMainLoop = _dbus_bindings.NativeMainLoop + +NULL_MAIN_LOOP = _dbus_bindings.NULL_MAIN_LOOP +"""A null mainloop which doesn't actually do anything. + +For advanced users who want to dispatch events by hand. This is almost +certainly a bad idea - if in doubt, use the GLib main loop found in +`dbus.mainloop.glib`. +""" + +WATCH_READABLE = _dbus_bindings.WATCH_READABLE +"""Represents a file descriptor becoming readable. +Used to implement file descriptor watches.""" + +WATCH_WRITABLE = _dbus_bindings.WATCH_WRITABLE +"""Represents a file descriptor becoming readable. +Used to implement file descriptor watches.""" + +WATCH_HANGUP = _dbus_bindings.WATCH_HANGUP +"""Represents a file descriptor reaching end-of-file. +Used to implement file descriptor watches.""" + +WATCH_ERROR = _dbus_bindings.WATCH_ERROR +"""Represents an error condition on a file descriptor. +Used to implement file descriptor watches.""" + +__all__ = ( + # Imported into this module + 'NativeMainLoop', 'WATCH_READABLE', 'WATCH_WRITABLE', + 'WATCH_HANGUP', 'WATCH_ERROR', 'NULL_MAIN_LOOP', + + # Submodules + 'glib' + ) diff --git a/dbus/mainloop/glib.py b/dbus/mainloop/glib.py new file mode 100644 index 0000000..39fbb1b --- /dev/null +++ b/dbus/mainloop/glib.py @@ -0,0 +1,41 @@ +# Copyright (C) 2004 Anders Carlsson +# Copyright (C) 2004-2006 Red Hat Inc. <http://www.redhat.com/> +# Copyright (C) 2005-2006 Collabora Ltd. <http://www.collabora.co.uk/> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +"""GLib main loop integration using libdbus-glib.""" + +__all__ = ('DBusGMainLoop', 'threads_init') + +from _dbus_glib_bindings import DBusGMainLoop, gthreads_init + +_dbus_gthreads_initialized = False +def threads_init(): + """Initialize threads in dbus-glib, if this has not already been done. + + This must be called before creating a second thread in a program that + uses this module. + """ + global _dbus_gthreads_initialized + if not _dbus_gthreads_initialized: + gthreads_init() + _dbus_gthreads_initialized = True diff --git a/dbus/proxies.py b/dbus/proxies.py new file mode 100644 index 0000000..cbb5d53 --- /dev/null +++ b/dbus/proxies.py @@ -0,0 +1,557 @@ +# Copyright (C) 2003-2007 Red Hat Inc. <http://www.redhat.com/> +# Copyright (C) 2003 David Zeuthen +# Copyright (C) 2004 Rob Taylor +# Copyright (C) 2005-2007 Collabora Ltd. <http://www.collabora.co.uk/> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import sys +import logging + +try: + from threading import RLock +except ImportError: + from dummy_threading import RLock + +import _dbus_bindings +from dbus._expat_introspect_parser import process_introspection_data +from dbus.exceptions import MissingReplyHandlerException, MissingErrorHandlerException, IntrospectionParserException, DBusException + +__docformat__ = 'restructuredtext' + + +_logger = logging.getLogger('dbus.proxies') + +from _dbus_bindings import LOCAL_PATH, \ + BUS_DAEMON_NAME, BUS_DAEMON_PATH, BUS_DAEMON_IFACE,\ + INTROSPECTABLE_IFACE + + +class _DeferredMethod: + """A proxy method which will only get called once we have its + introspection reply. + """ + def __init__(self, proxy_method, append, block): + self._proxy_method = proxy_method + # the test suite relies on the existence of this property + self._method_name = proxy_method._method_name + self._append = append + self._block = block + + def __call__(self, *args, **keywords): + if (keywords.has_key('reply_handler') or + keywords.get('ignore_reply', False)): + # defer the async call til introspection finishes + self._append(self._proxy_method, args, keywords) + return None + else: + # we're being synchronous, so block + self._block() + return self._proxy_method(*args, **keywords) + + def call_async(self, *args, **keywords): + self._append(self._proxy_method, args, keywords) + + +class _ProxyMethod: + """A proxy method. + + Typically a member of a ProxyObject. Calls to the + method produce messages that travel over the Bus and are routed + to a specific named Service. + """ + def __init__(self, proxy, connection, bus_name, object_path, method_name, + iface): + if object_path == LOCAL_PATH: + raise DBusException('Methods may not be called on the reserved ' + 'path %s' % LOCAL_PATH) + + # trust that the proxy, and the properties it had, are OK + self._proxy = proxy + self._connection = connection + self._named_service = bus_name + self._object_path = object_path + # fail early if the method name is bad + _dbus_bindings.validate_member_name(method_name) + # the test suite relies on the existence of this property + self._method_name = method_name + # fail early if the interface name is bad + if iface is not None: + _dbus_bindings.validate_interface_name(iface) + self._dbus_interface = iface + + def __call__(self, *args, **keywords): + reply_handler = keywords.pop('reply_handler', None) + error_handler = keywords.pop('error_handler', None) + ignore_reply = keywords.pop('ignore_reply', False) + + if reply_handler is not None or error_handler is not None: + if reply_handler is None: + raise MissingReplyHandlerException() + elif error_handler is None: + raise MissingErrorHandlerException() + elif ignore_reply: + raise TypeError('ignore_reply and reply_handler cannot be ' + 'used together') + + dbus_interface = keywords.pop('dbus_interface', self._dbus_interface) + + if dbus_interface is None: + key = self._method_name + else: + key = dbus_interface + '.' + self._method_name + introspect_sig = self._proxy._introspect_method_map.get(key, None) + + if ignore_reply or reply_handler is not None: + self._connection.call_async(self._named_service, + self._object_path, + dbus_interface, + self._method_name, + introspect_sig, + args, + reply_handler, + error_handler, + **keywords) + else: + return self._connection.call_blocking(self._named_service, + self._object_path, + dbus_interface, + self._method_name, + introspect_sig, + args, + **keywords) + + def call_async(self, *args, **keywords): + reply_handler = keywords.pop('reply_handler', None) + error_handler = keywords.pop('error_handler', None) + + dbus_interface = keywords.pop('dbus_interface', self._dbus_interface) + + if dbus_interface: + key = dbus_interface + '.' + self._method_name + else: + key = self._method_name + introspect_sig = self._proxy._introspect_method_map.get(key, None) + + self._connection.call_async(self._named_service, + self._object_path, + dbus_interface, + self._method_name, + introspect_sig, + args, + reply_handler, + error_handler, + **keywords) + + +class ProxyObject(object): + """A proxy to the remote Object. + + A ProxyObject is provided by the Bus. ProxyObjects + have member functions, and can be called like normal Python objects. + """ + ProxyMethodClass = _ProxyMethod + DeferredMethodClass = _DeferredMethod + + INTROSPECT_STATE_DONT_INTROSPECT = 0 + INTROSPECT_STATE_INTROSPECT_IN_PROGRESS = 1 + INTROSPECT_STATE_INTROSPECT_DONE = 2 + + def __init__(self, conn=None, bus_name=None, object_path=None, + introspect=True, follow_name_owner_changes=False, **kwargs): + """Initialize the proxy object. + + :Parameters: + `conn` : `dbus.connection.Connection` + The bus or connection on which to find this object. + The keyword argument `bus` is a deprecated alias for this. + `bus_name` : str + A bus name for the application owning the object, to be used + as the destination for method calls and the sender for + signal matches. The keyword argument ``named_service`` is a + deprecated alias for this. + `object_path` : str + The object path at which the application exports the object + `introspect` : bool + If true (default), attempt to introspect the remote + object to find out supported methods and their signatures + `follow_name_owner_changes` : bool + If true (default is false) and the `bus_name` is a + well-known name, follow ownership changes for that name + """ + bus = kwargs.pop('bus', None) + if bus is not None: + if conn is not None: + raise TypeError('conn and bus cannot both be specified') + conn = bus + from warnings import warn + warn('Passing the bus parameter to ProxyObject by name is ' + 'deprecated: please use positional parameters', + DeprecationWarning, stacklevel=2) + named_service = kwargs.pop('named_service', None) + if named_service is not None: + if bus_name is not None: + raise TypeError('bus_name and named_service cannot both be ' + 'specified') + bus_name = named_service + from warnings import warn + warn('Passing the named_service parameter to ProxyObject by name ' + 'is deprecated: please use positional parameters', + DeprecationWarning, stacklevel=2) + if kwargs: + raise TypeError('ProxyObject.__init__ does not take these ' + 'keyword arguments: %s' + % ', '.join(kwargs.iterkeys())) + + if follow_name_owner_changes: + # we don't get the signals unless the Bus has a main loop + # XXX: using Bus internals + conn._require_main_loop() + + self._bus = conn + + if bus_name is not None: + _dbus_bindings.validate_bus_name(bus_name) + # the attribute is still called _named_service for the moment, + # for the benefit of telepathy-python + self._named_service = self._requested_bus_name = bus_name + + _dbus_bindings.validate_object_path(object_path) + self.__dbus_object_path__ = object_path + + if not follow_name_owner_changes: + self._named_service = conn.activate_name_owner(bus_name) + + #PendingCall object for Introspect call + self._pending_introspect = None + #queue of async calls waiting on the Introspect to return + self._pending_introspect_queue = [] + #dictionary mapping method names to their input signatures + self._introspect_method_map = {} + + # must be a recursive lock because block() is called while locked, + # and calls the callback which re-takes the lock + self._introspect_lock = RLock() + + if not introspect or self.__dbus_object_path__ == LOCAL_PATH: + self._introspect_state = self.INTROSPECT_STATE_DONT_INTROSPECT + else: + self._introspect_state = self.INTROSPECT_STATE_INTROSPECT_IN_PROGRESS + + self._pending_introspect = self._Introspect() + + bus_name = property(lambda self: self._named_service, None, None, + """The bus name to which this proxy is bound. (Read-only, + may change.) + + If the proxy was instantiated using a unique name, this property + is that unique name. + + If the proxy was instantiated with a well-known name and with + ``follow_name_owner_changes`` set false (the default), this + property is the unique name of the connection that owned that + well-known name when the proxy was instantiated, which might + not actually own the requested well-known name any more. + + If the proxy was instantiated with a well-known name and with + ``follow_name_owner_changes`` set true, this property is that + well-known name. + """) + + requested_bus_name = property(lambda self: self._requested_bus_name, + None, None, + """The bus name which was requested when this proxy was + instantiated. + """) + + object_path = property(lambda self: self.__dbus_object_path__, + None, None, + """The object-path of this proxy.""") + + # XXX: We don't currently support this because it's the signal receiver + # that's responsible for tracking name owner changes, but it + # seems a natural thing to add in future. + #unique_bus_name = property(lambda self: something, None, None, + # """The unique name of the connection to which this proxy is + # currently bound. (Read-only, may change.) + # """) + + def connect_to_signal(self, signal_name, handler_function, dbus_interface=None, **keywords): + """Arrange for the given function to be called when the given signal + is received. + + :Parameters: + `signal_name` : str + The name of the signal + `handler_function` : callable + A function to be called when the signal is emitted by + the remote object. Its positional arguments will be the + arguments of the signal; optionally, it may be given + keyword arguments as described below. + `dbus_interface` : str + Optional interface with which to qualify the signal name. + If None (the default) the handler will be called whenever a + signal of the given member name is received, whatever + its interface. + :Keywords: + `utf8_strings` : bool + If True, the handler function will receive any string + arguments as dbus.UTF8String objects (a subclass of str + guaranteed to be UTF-8). If False (default) it will receive + any string arguments as dbus.String objects (a subclass of + unicode). + `byte_arrays` : bool + If True, the handler function will receive any byte-array + arguments as dbus.ByteArray objects (a subclass of str). + If False (default) it will receive any byte-array + arguments as a dbus.Array of dbus.Byte (subclasses of: + a list of ints). + `sender_keyword` : str + If not None (the default), the handler function will receive + the unique name of the sending endpoint as a keyword + argument with this name + `destination_keyword` : str + If not None (the default), the handler function will receive + the bus name of the destination (or None if the signal is a + broadcast, as is usual) as a keyword argument with this name. + `interface_keyword` : str + If not None (the default), the handler function will receive + the signal interface as a keyword argument with this name. + `member_keyword` : str + If not None (the default), the handler function will receive + the signal name as a keyword argument with this name. + `path_keyword` : str + If not None (the default), the handler function will receive + the object-path of the sending object as a keyword argument + with this name + `message_keyword` : str + If not None (the default), the handler function will receive + the `dbus.lowlevel.SignalMessage` as a keyword argument with + this name. + `arg...` : unicode or UTF-8 str + If there are additional keyword parameters of the form + ``arg``\ *n*, match only signals where the *n*\ th argument + is the value given for that keyword parameter. As of this time + only string arguments can be matched (in particular, + object paths and signatures can't). + """ + return \ + self._bus.add_signal_receiver(handler_function, + signal_name=signal_name, + dbus_interface=dbus_interface, + bus_name=self._named_service, + path=self.__dbus_object_path__, + **keywords) + + def _Introspect(self): + return self._bus.call_async(self._named_service, + self.__dbus_object_path__, + INTROSPECTABLE_IFACE, 'Introspect', '', (), + self._introspect_reply_handler, + self._introspect_error_handler, + utf8_strings=True, + require_main_loop=False) + + def _introspect_execute_queue(self): + # FIXME: potential to flood the bus + # We should make sure mainloops all have idle handlers + # and do one message per idle + for (proxy_method, args, keywords) in self._pending_introspect_queue: + proxy_method(*args, **keywords) + + def _introspect_reply_handler(self, data): + self._introspect_lock.acquire() + try: + try: + self._introspect_method_map = process_introspection_data(data) + except IntrospectionParserException, e: + self._introspect_error_handler(e) + return + + self._introspect_state = self.INTROSPECT_STATE_INTROSPECT_DONE + self._pending_introspect = None + self._introspect_execute_queue() + finally: + self._introspect_lock.release() + + def _introspect_error_handler(self, error): + logging.basicConfig() + _logger.error("Introspect error on %s:%s: %s.%s: %s", + self._named_service, self.__dbus_object_path__, + error.__class__.__module__, error.__class__.__name__, + error) + self._introspect_lock.acquire() + try: + _logger.debug('Executing introspect queue due to error') + self._introspect_state = self.INTROSPECT_STATE_DONT_INTROSPECT + self._pending_introspect = None + self._introspect_execute_queue() + finally: + self._introspect_lock.release() + + def _introspect_block(self): + self._introspect_lock.acquire() + try: + if self._pending_introspect is not None: + self._pending_introspect.block() + # else someone still has a _DeferredMethod from before we + # finished introspection: no need to do anything special any more + finally: + self._introspect_lock.release() + + def _introspect_add_to_queue(self, callback, args, kwargs): + self._introspect_lock.acquire() + try: + if self._introspect_state == self.INTROSPECT_STATE_INTROSPECT_IN_PROGRESS: + self._pending_introspect_queue.append((callback, args, kwargs)) + else: + # someone still has a _DeferredMethod from before we + # finished introspection + callback(*args, **kwargs) + finally: + self._introspect_lock.release() + + def __getattr__(self, member): + if member.startswith('__') and member.endswith('__'): + raise AttributeError(member) + else: + return self.get_dbus_method(member) + + def get_dbus_method(self, member, dbus_interface=None): + """Return a proxy method representing the given D-Bus method. The + returned proxy method can be called in the usual way. For instance, :: + + proxy.get_dbus_method("Foo", dbus_interface='com.example.Bar')(123) + + is equivalent to:: + + proxy.Foo(123, dbus_interface='com.example.Bar') + + or even:: + + getattr(proxy, "Foo")(123, dbus_interface='com.example.Bar') + + However, using `get_dbus_method` is the only way to call D-Bus + methods with certain awkward names - if the author of a service + implements a method called ``connect_to_signal`` or even + ``__getattr__``, you'll need to use `get_dbus_method` to call them. + + For services which follow the D-Bus convention of CamelCaseMethodNames + this won't be a problem. + """ + + ret = self.ProxyMethodClass(self, self._bus, + self._named_service, + self.__dbus_object_path__, member, + dbus_interface) + + # this can be done without taking the lock - the worst that can + # happen is that we accidentally return a _DeferredMethod just after + # finishing introspection, in which case _introspect_add_to_queue and + # _introspect_block will do the right thing anyway + if self._introspect_state == self.INTROSPECT_STATE_INTROSPECT_IN_PROGRESS: + ret = self.DeferredMethodClass(ret, self._introspect_add_to_queue, + self._introspect_block) + + return ret + + def __repr__(self): + return '<ProxyObject wrapping %s %s %s at %#x>'%( + self._bus, self._named_service, self.__dbus_object_path__, id(self)) + __str__ = __repr__ + + +class Interface(object): + """An interface into a remote object. + + An Interface can be used to wrap ProxyObjects + so that calls can be routed to their correct + D-Bus interface. + """ + + def __init__(self, object, dbus_interface): + """Construct a proxy for the given interface on the given object. + + :Parameters: + `object` : `dbus.proxies.ProxyObject` or `dbus.Interface` + The remote object or another of its interfaces + `dbus_interface` : str + An interface the `object` implements + """ + if isinstance(object, Interface): + self._obj = object.proxy_object + else: + self._obj = object + self._dbus_interface = dbus_interface + + object_path = property (lambda self: self._obj.object_path, None, None, + "The D-Bus object path of the underlying object") + __dbus_object_path__ = object_path + bus_name = property (lambda self: self._obj.bus_name, None, None, + "The bus name to which the underlying proxy object " + "is bound") + requested_bus_name = property (lambda self: self._obj.requested_bus_name, + None, None, + "The bus name which was requested when the " + "underlying object was created") + proxy_object = property (lambda self: self._obj, None, None, + """The underlying proxy object""") + dbus_interface = property (lambda self: self._dbus_interface, None, None, + """The D-Bus interface represented""") + + def connect_to_signal(self, signal_name, handler_function, + dbus_interface=None, **keywords): + """Arrange for a function to be called when the given signal is + emitted. + + The parameters and keyword arguments are the same as for + `dbus.proxies.ProxyObject.connect_to_signal`, except that if + `dbus_interface` is None (the default), the D-Bus interface that + was passed to the `Interface` constructor is used. + """ + if not dbus_interface: + dbus_interface = self._dbus_interface + + return self._obj.connect_to_signal(signal_name, handler_function, + dbus_interface, **keywords) + + def __getattr__(self, member): + if member.startswith('__') and member.endswith('__'): + raise AttributeError(member) + else: + return self._obj.get_dbus_method(member, self._dbus_interface) + + def get_dbus_method(self, member, dbus_interface=None): + """Return a proxy method representing the given D-Bus method. + + This is the same as `dbus.proxies.ProxyObject.get_dbus_method` + except that if `dbus_interface` is None (the default), + the D-Bus interface that was passed to the `Interface` constructor + is used. + """ + if dbus_interface is None: + dbus_interface = self._dbus_interface + return self._obj.get_dbus_method(member, dbus_interface) + + def __repr__(self): + return '<Interface %r implementing %r at %#x>'%( + self._obj, self._dbus_interface, id(self)) + __str__ = __repr__ diff --git a/dbus/server.py b/dbus/server.py new file mode 100644 index 0000000..1988101 --- /dev/null +++ b/dbus/server.py @@ -0,0 +1,117 @@ +# Copyright (C) 2008 Openismus GmbH <http://openismus.com/> +# Copyright (C) 2008 Collabora Ltd. <http://www.collabora.co.uk/> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +__all__ = ('Server', ) +__docformat__ = 'reStructuredText' + +from _dbus_bindings import _Server +from dbus.connection import Connection + +class Server(_Server): + """An opaque object representing a server that listens for connections from + other applications. + + This class is not useful to instantiate directly: you must subclass it and + either extend the method connection_added, or append to the + list on_connection_added. + + :Since: 0.83 + """ + + def __new__(cls, address, connection_class=Connection, + mainloop=None, auth_mechanisms=None): + """Construct a new Server. + + :Parameters: + `address` : str + Listen on this address. + `connection_class` : type + When new connections come in, instantiate this subclass + of dbus.connection.Connection to represent them. + The default is Connection. + `mainloop` : dbus.mainloop.NativeMainLoop or None + The main loop with which to associate the new connections. + `auth_mechanisms` : sequence of str + Authentication mechanisms to allow. The default is to allow + any authentication mechanism supported by ``libdbus``. + """ + return super(Server, cls).__new__(cls, address, connection_class, + mainloop, auth_mechanisms) + + def __init__(self, *args, **kwargs): + + self.__connections = {} + + self.on_connection_added = [] + """A list of callbacks to invoke when a connection is added. + They receive two arguments: this Server and the new Connection.""" + + self.on_connection_removed = [] + """A list of callbacks to invoke when a connection becomes + disconnected. They receive two arguments: this Server and the removed + Connection.""" + + # This method name is hard-coded in _dbus_bindings._Server. + # This is not public API. + def _on_new_connection(self, conn): + conn.call_on_disconnection(self.connection_removed) + self.connection_added(conn) + + def connection_added(self, conn): + """Respond to the creation of a new Connection. + + This base-class implementation just invokes the callbacks in + the on_connection_added attribute. + + :Parameters: + `conn` : dbus.connection.Connection + A D-Bus connection which has just been added. + + The type of this parameter is whatever was passed + to the Server constructor as the ``connection_class``. + """ + if self.on_connection_added: + for cb in self.on_connection_added: + cb(conn) + + def connection_removed(self, conn): + """Respond to the disconnection of a Connection. + + This base-class implementation just invokes the callbacks in + the on_connection_removed attribute. + + :Parameters: + `conn` : dbus.connection.Connection + A D-Bus connection which has just become disconnected. + + The type of this parameter is whatever was passed + to the Server constructor as the ``connection_class``. + """ + if self.on_connection_removed: + for cb in self.on_connection_removed: + cb(conn) + + address = property(_Server.get_address) + id = property(_Server.get_id) + is_connected = property(_Server.get_is_connected) + diff --git a/dbus/service.py b/dbus/service.py new file mode 100644 index 0000000..b92d840 --- /dev/null +++ b/dbus/service.py @@ -0,0 +1,829 @@ +# Copyright (C) 2003-2006 Red Hat Inc. <http://www.redhat.com/> +# Copyright (C) 2003 David Zeuthen +# Copyright (C) 2004 Rob Taylor +# Copyright (C) 2005-2006 Collabora Ltd. <http://www.collabora.co.uk/> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +__all__ = ('BusName', 'Object', 'method', 'signal') +__docformat__ = 'restructuredtext' + +import sys +import logging +import operator +import traceback +try: + import thread +except ImportError: + import dummy_thread as thread + +import _dbus_bindings +from dbus import SessionBus, Signature, Struct, validate_bus_name, \ + validate_object_path, INTROSPECTABLE_IFACE, ObjectPath +from dbus.decorators import method, signal +from dbus.exceptions import DBusException, \ + NameExistsException, \ + UnknownMethodException +from dbus.lowlevel import ErrorMessage, MethodReturnMessage, MethodCallMessage +from dbus.proxies import LOCAL_PATH + + +_logger = logging.getLogger('dbus.service') + + +class _VariantSignature(object): + """A fake method signature which, when iterated, yields an endless stream + of 'v' characters representing variants (handy with zip()). + + It has no string representation. + """ + def __iter__(self): + """Return self.""" + return self + + def next(self): + """Return 'v' whenever called.""" + return 'v' + +class BusName(object): + """A base class for exporting your own Named Services across the Bus. + + When instantiated, objects of this class attempt to claim the given + well-known name on the given bus for the current process. The name is + released when the BusName object becomes unreferenced. + + If a well-known name is requested multiple times, multiple references + to the same BusName object will be returned. + + Caveats + ------- + - Assumes that named services are only ever requested using this class - + if you request names from the bus directly, confusion may occur. + - Does not handle queueing. + """ + def __new__(cls, name, bus=None, allow_replacement=False , replace_existing=False, do_not_queue=False): + """Constructor, which may either return an existing cached object + or a new object. + + :Parameters: + `name` : str + The well-known name to be advertised + `bus` : dbus.Bus + A Bus on which this service will be advertised. + + Omitting this parameter or setting it to None has been + deprecated since version 0.82.1. For backwards compatibility, + if this is done, the global shared connection to the session + bus will be used. + + `allow_replacement` : bool + If True, other processes trying to claim the same well-known + name will take precedence over this one. + `replace_existing` : bool + If True, this process can take over the well-known name + from other processes already holding it. + `do_not_queue` : bool + If True, this service will not be placed in the queue of + services waiting for the requested name if another service + already holds it. + """ + validate_bus_name(name, allow_well_known=True, allow_unique=False) + + # if necessary, get default bus (deprecated) + if bus is None: + import warnings + warnings.warn('Omitting the "bus" parameter to ' + 'dbus.service.BusName.__init__ is deprecated', + DeprecationWarning, stacklevel=2) + bus = SessionBus() + + # see if this name is already defined, return it if so + # FIXME: accessing internals of Bus + if name in bus._bus_names: + return bus._bus_names[name] + + # otherwise register the name + name_flags = ( + (allow_replacement and _dbus_bindings.NAME_FLAG_ALLOW_REPLACEMENT or 0) | + (replace_existing and _dbus_bindings.NAME_FLAG_REPLACE_EXISTING or 0) | + (do_not_queue and _dbus_bindings.NAME_FLAG_DO_NOT_QUEUE or 0)) + + retval = bus.request_name(name, name_flags) + + # TODO: more intelligent tracking of bus name states? + if retval == _dbus_bindings.REQUEST_NAME_REPLY_PRIMARY_OWNER: + pass + elif retval == _dbus_bindings.REQUEST_NAME_REPLY_IN_QUEUE: + # queueing can happen by default, maybe we should + # track this better or let the user know if they're + # queued or not? + pass + elif retval == _dbus_bindings.REQUEST_NAME_REPLY_EXISTS: + raise NameExistsException(name) + elif retval == _dbus_bindings.REQUEST_NAME_REPLY_ALREADY_OWNER: + # if this is a shared bus which is being used by someone + # else in this process, this can happen legitimately + pass + else: + raise RuntimeError('requesting bus name %s returned unexpected value %s' % (name, retval)) + + # and create the object + bus_name = object.__new__(cls) + bus_name._bus = bus + bus_name._name = name + + # cache instance (weak ref only) + # FIXME: accessing Bus internals again + bus._bus_names[name] = bus_name + + return bus_name + + # do nothing because this is called whether or not the bus name + # object was retrieved from the cache or created new + def __init__(self, *args, **keywords): + pass + + # we can delete the low-level name here because these objects + # are guaranteed to exist only once for each bus name + def __del__(self): + self._bus.release_name(self._name) + pass + + def get_bus(self): + """Get the Bus this Service is on""" + return self._bus + + def get_name(self): + """Get the name of this service""" + return self._name + + def __repr__(self): + return '<dbus.service.BusName %s on %r at %#x>' % (self._name, self._bus, id(self)) + __str__ = __repr__ + + +def _method_lookup(self, method_name, dbus_interface): + """Walks the Python MRO of the given class to find the method to invoke. + + Returns two methods, the one to call, and the one it inherits from which + defines its D-Bus interface name, signature, and attributes. + """ + parent_method = None + candidate_class = None + successful = False + + # split up the cases when we do and don't have an interface because the + # latter is much simpler + if dbus_interface: + # search through the class hierarchy in python MRO order + for cls in self.__class__.__mro__: + # if we haven't got a candidate class yet, and we find a class with a + # suitably named member, save this as a candidate class + if (not candidate_class and method_name in cls.__dict__): + if ("_dbus_is_method" in cls.__dict__[method_name].__dict__ + and "_dbus_interface" in cls.__dict__[method_name].__dict__): + # however if it is annotated for a different interface + # than we are looking for, it cannot be a candidate + if cls.__dict__[method_name]._dbus_interface == dbus_interface: + candidate_class = cls + parent_method = cls.__dict__[method_name] + successful = True + break + else: + pass + else: + candidate_class = cls + + # if we have a candidate class, carry on checking this and all + # superclasses for a method annoated as a dbus method + # on the correct interface + if (candidate_class and method_name in cls.__dict__ + and "_dbus_is_method" in cls.__dict__[method_name].__dict__ + and "_dbus_interface" in cls.__dict__[method_name].__dict__ + and cls.__dict__[method_name]._dbus_interface == dbus_interface): + # the candidate class has a dbus method on the correct interface, + # or overrides a method that is, success! + parent_method = cls.__dict__[method_name] + successful = True + break + + else: + # simpler version of above + for cls in self.__class__.__mro__: + if (not candidate_class and method_name in cls.__dict__): + candidate_class = cls + + if (candidate_class and method_name in cls.__dict__ + and "_dbus_is_method" in cls.__dict__[method_name].__dict__): + parent_method = cls.__dict__[method_name] + successful = True + break + + if successful: + return (candidate_class.__dict__[method_name], parent_method) + else: + if dbus_interface: + raise UnknownMethodException('%s is not a valid method of interface %s' % (method_name, dbus_interface)) + else: + raise UnknownMethodException('%s is not a valid method' % method_name) + + +def _method_reply_return(connection, message, method_name, signature, *retval): + reply = MethodReturnMessage(message) + try: + reply.append(signature=signature, *retval) + except Exception, e: + logging.basicConfig() + if signature is None: + try: + signature = reply.guess_signature(retval) + ' (guessed)' + except Exception, e: + _logger.error('Unable to guess signature for arguments %r: ' + '%s: %s', retval, e.__class__, e) + raise + _logger.error('Unable to append %r to message with signature %s: ' + '%s: %s', retval, signature, e.__class__, e) + raise + + connection.send_message(reply) + + +def _method_reply_error(connection, message, exception): + name = getattr(exception, '_dbus_error_name', None) + + if name is not None: + pass + elif getattr(exception, '__module__', '') in ('', '__main__'): + name = 'org.freedesktop.DBus.Python.%s' % exception.__class__.__name__ + else: + name = 'org.freedesktop.DBus.Python.%s.%s' % (exception.__module__, exception.__class__.__name__) + + et, ev, etb = sys.exc_info() + if isinstance(exception, DBusException) and not exception.include_traceback: + # We don't actually want the traceback anyway + contents = exception.get_dbus_message() + elif ev is exception: + # The exception was actually thrown, so we can get a traceback + contents = ''.join(traceback.format_exception(et, ev, etb)) + else: + # We don't have any traceback for it, e.g. + # async_err_cb(MyException('Failed to badger the mushroom')) + # see also https://bugs.freedesktop.org/show_bug.cgi?id=12403 + contents = ''.join(traceback.format_exception_only(exception.__class__, + exception)) + reply = ErrorMessage(message, name, contents) + + connection.send_message(reply) + + +class InterfaceType(type): + def __init__(cls, name, bases, dct): + # these attributes are shared between all instances of the Interface + # object, so this has to be a dictionary that maps class names to + # the per-class introspection/interface data + class_table = getattr(cls, '_dbus_class_table', {}) + cls._dbus_class_table = class_table + interface_table = class_table[cls.__module__ + '.' + name] = {} + + # merge all the name -> method tables for all the interfaces + # implemented by our base classes into our own + for b in bases: + base_name = b.__module__ + '.' + b.__name__ + if getattr(b, '_dbus_class_table', False): + for (interface, method_table) in class_table[base_name].iteritems(): + our_method_table = interface_table.setdefault(interface, {}) + our_method_table.update(method_table) + + # add in all the name -> method entries for our own methods/signals + for func in dct.values(): + if getattr(func, '_dbus_interface', False): + method_table = interface_table.setdefault(func._dbus_interface, {}) + method_table[func.__name__] = func + + super(InterfaceType, cls).__init__(name, bases, dct) + + # methods are different to signals, so we have two functions... :) + def _reflect_on_method(cls, func): + args = func._dbus_args + + if func._dbus_in_signature: + # convert signature into a tuple so length refers to number of + # types, not number of characters. the length is checked by + # the decorator to make sure it matches the length of args. + in_sig = tuple(Signature(func._dbus_in_signature)) + else: + # magic iterator which returns as many v's as we need + in_sig = _VariantSignature() + + if func._dbus_out_signature: + out_sig = Signature(func._dbus_out_signature) + else: + # its tempting to default to Signature('v'), but + # for methods that return nothing, providing incorrect + # introspection data is worse than providing none at all + out_sig = [] + + reflection_data = ' <method name="%s">\n' % (func.__name__) + for pair in zip(in_sig, args): + reflection_data += ' <arg direction="in" type="%s" name="%s" />\n' % pair + for type in out_sig: + reflection_data += ' <arg direction="out" type="%s" />\n' % type + reflection_data += ' </method>\n' + + return reflection_data + + def _reflect_on_signal(cls, func): + args = func._dbus_args + + if func._dbus_signature: + # convert signature into a tuple so length refers to number of + # types, not number of characters + sig = tuple(Signature(func._dbus_signature)) + else: + # magic iterator which returns as many v's as we need + sig = _VariantSignature() + + reflection_data = ' <signal name="%s">\n' % (func.__name__) + for pair in zip(sig, args): + reflection_data = reflection_data + ' <arg type="%s" name="%s" />\n' % pair + reflection_data = reflection_data + ' </signal>\n' + + return reflection_data + +class Interface(object): + __metaclass__ = InterfaceType + +#: A unique object used as the value of Object._object_path and +#: Object._connection if it's actually in more than one place +_MANY = object() + +class Object(Interface): + r"""A base class for exporting your own Objects across the Bus. + + Just inherit from Object and mark exported methods with the + @\ `dbus.service.method` or @\ `dbus.service.signal` decorator. + + Example:: + + class Example(dbus.service.object): + def __init__(self, object_path): + dbus.service.Object.__init__(self, dbus.SessionBus(), path) + self._last_input = None + + @dbus.service.method(interface='com.example.Sample', + in_signature='v', out_signature='s') + def StringifyVariant(self, var): + self.LastInputChanged(var) # emits the signal + return str(var) + + @dbus.service.signal(interface='com.example.Sample', + signature='v') + def LastInputChanged(self, var): + # run just before the signal is actually emitted + # just put "pass" if nothing should happen + self._last_input = var + + @dbus.service.method(interface='com.example.Sample', + in_signature='', out_signature='v') + def GetLastInput(self): + return self._last_input + """ + + #: If True, this object can be made available at more than one object path. + #: If True but `SUPPORTS_MULTIPLE_CONNECTIONS` is False, the object may + #: handle more than one object path, but they must all be on the same + #: connection. + SUPPORTS_MULTIPLE_OBJECT_PATHS = False + + #: If True, this object can be made available on more than one connection. + #: If True but `SUPPORTS_MULTIPLE_OBJECT_PATHS` is False, the object must + #: have the same object path on all its connections. + SUPPORTS_MULTIPLE_CONNECTIONS = False + + def __init__(self, conn=None, object_path=None, bus_name=None): + """Constructor. Either conn or bus_name is required; object_path + is also required. + + :Parameters: + `conn` : dbus.connection.Connection or None + The connection on which to export this object. + + If None, use the Bus associated with the given ``bus_name``. + If there is no ``bus_name`` either, the object is not + initially available on any Connection. + + For backwards compatibility, if an instance of + dbus.service.BusName is passed as the first parameter, + this is equivalent to passing its associated Bus as + ``conn``, and passing the BusName itself as ``bus_name``. + + `object_path` : str or None + A D-Bus object path at which to make this Object available + immediately. If this is not None, a `conn` or `bus_name` must + also be provided. + + `bus_name` : dbus.service.BusName or None + Represents a well-known name claimed by this process. A + reference to the BusName object will be held by this + Object, preventing the name from being released during this + Object's lifetime (unless it's released manually). + """ + if object_path is not None: + validate_object_path(object_path) + + if isinstance(conn, BusName): + # someone's using the old API; don't gratuitously break them + bus_name = conn + conn = bus_name.get_bus() + elif conn is None: + if bus_name is not None: + # someone's using the old API but naming arguments, probably + conn = bus_name.get_bus() + + #: Either an object path, None or _MANY + self._object_path = None + #: Either a dbus.connection.Connection, None or _MANY + self._connection = None + #: A list of tuples (Connection, object path, False) where the False + #: is for future expansion (to support fallback paths) + self._locations = [] + #: Lock protecting `_locations`, `_connection` and `_object_path` + self._locations_lock = thread.allocate_lock() + + #: True if this is a fallback object handling a whole subtree. + self._fallback = False + + self._name = bus_name + + if conn is None and object_path is not None: + raise TypeError('If object_path is given, either conn or bus_name ' + 'is required') + if conn is not None and object_path is not None: + self.add_to_connection(conn, object_path) + + @property + def __dbus_object_path__(self): + """The object-path at which this object is available. + Access raises AttributeError if there is no object path, or more than + one object path. + + Changed in 0.82.0: AttributeError can be raised. + """ + if self._object_path is _MANY: + raise AttributeError('Object %r has more than one object path: ' + 'use Object.locations instead' % self) + elif self._object_path is None: + raise AttributeError('Object %r has no object path yet' % self) + else: + return self._object_path + + @property + def connection(self): + """The Connection on which this object is available. + Access raises AttributeError if there is no Connection, or more than + one Connection. + + Changed in 0.82.0: AttributeError can be raised. + """ + if self._connection is _MANY: + raise AttributeError('Object %r is on more than one Connection: ' + 'use Object.locations instead' % self) + elif self._connection is None: + raise AttributeError('Object %r has no Connection yet' % self) + else: + return self._connection + + @property + def locations(self): + """An iterable over tuples representing locations at which this + object is available. + + Each tuple has at least two items, but may have more in future + versions of dbus-python, so do not rely on their exact length. + The first two items are the dbus.connection.Connection and the object + path. + + :Since: 0.82.0 + """ + return iter(self._locations) + + def add_to_connection(self, connection, path): + """Make this object accessible via the given D-Bus connection and + object path. + + :Parameters: + `connection` : dbus.connection.Connection + Export the object on this connection. If the class attribute + SUPPORTS_MULTIPLE_CONNECTIONS is False (default), this object + can only be made available on one connection; if the class + attribute is set True by a subclass, the object can be made + available on more than one connection. + + `path` : dbus.ObjectPath or other str + Place the object at this object path. If the class attribute + SUPPORTS_MULTIPLE_OBJECT_PATHS is False (default), this object + can only be made available at one object path; if the class + attribute is set True by a subclass, the object can be made + available with more than one object path. + + :Raises ValueError: if the object's class attributes do not allow the + object to be exported in the desired way. + :Since: 0.82.0 + """ + if path == LOCAL_PATH: + raise ValueError('Objects may not be exported on the reserved ' + 'path %s' % LOCAL_PATH) + + self._locations_lock.acquire() + try: + if (self._connection is not None and + self._connection is not connection and + not self.SUPPORTS_MULTIPLE_CONNECTIONS): + raise ValueError('%r is already exported on ' + 'connection %r' % (self, self._connection)) + + if (self._object_path is not None and + not self.SUPPORTS_MULTIPLE_OBJECT_PATHS and + self._object_path != path): + raise ValueError('%r is already exported at object ' + 'path %s' % (self, self._object_path)) + + connection._register_object_path(path, self._message_cb, + self._unregister_cb, + self._fallback) + + if self._connection is None: + self._connection = connection + elif self._connection is not connection: + self._connection = _MANY + + if self._object_path is None: + self._object_path = path + elif self._object_path != path: + self._object_path = _MANY + + self._locations.append((connection, path, self._fallback)) + finally: + self._locations_lock.release() + + def remove_from_connection(self, connection=None, path=None): + """Make this object inaccessible via the given D-Bus connection + and object path. If no connection or path is specified, + the object ceases to be accessible via any connection or path. + + :Parameters: + `connection` : dbus.connection.Connection or None + Only remove the object from this Connection. If None, + remove from all Connections on which it's exported. + `path` : dbus.ObjectPath or other str, or None + Only remove the object from this object path. If None, + remove from all object paths. + :Raises LookupError: + if the object was not exported on the requested connection + or path, or (if both are None) was not exported at all. + :Since: 0.81.1 + """ + self._locations_lock.acquire() + try: + if self._object_path is None or self._connection is None: + raise LookupError('%r is not exported' % self) + + if connection is not None or path is not None: + dropped = [] + for location in self._locations: + if ((connection is None or location[0] is connection) and + (path is None or location[1] == path)): + dropped.append(location) + else: + dropped = self._locations + self._locations = [] + + if not dropped: + raise LookupError('%r is not exported at a location matching ' + '(%r,%r)' % (self, connection, path)) + + for location in dropped: + try: + location[0]._unregister_object_path(location[1]) + except LookupError: + pass + if self._locations: + try: + self._locations.remove(location) + except ValueError: + pass + finally: + self._locations_lock.release() + + def _unregister_cb(self, connection): + # there's not really enough information to do anything useful here + _logger.info('Unregistering exported object %r from some path ' + 'on %r', self, connection) + + def _message_cb(self, connection, message): + if not isinstance(message, MethodCallMessage): + return + + try: + # lookup candidate method and parent method + method_name = message.get_member() + interface_name = message.get_interface() + (candidate_method, parent_method) = _method_lookup(self, method_name, interface_name) + + # set up method call parameters + args = message.get_args_list(**parent_method._dbus_get_args_options) + keywords = {} + + if parent_method._dbus_out_signature is not None: + signature = Signature(parent_method._dbus_out_signature) + else: + signature = None + + # set up async callback functions + if parent_method._dbus_async_callbacks: + (return_callback, error_callback) = parent_method._dbus_async_callbacks + keywords[return_callback] = lambda *retval: _method_reply_return(connection, message, method_name, signature, *retval) + keywords[error_callback] = lambda exception: _method_reply_error(connection, message, exception) + + # include the sender etc. if desired + if parent_method._dbus_sender_keyword: + keywords[parent_method._dbus_sender_keyword] = message.get_sender() + if parent_method._dbus_path_keyword: + keywords[parent_method._dbus_path_keyword] = message.get_path() + if parent_method._dbus_rel_path_keyword: + path = message.get_path() + rel_path = path + for exp in self._locations: + # pathological case: if we're exported in two places, + # one of which is a subtree of the other, then pick the + # subtree by preference (i.e. minimize the length of + # rel_path) + if exp[0] is connection: + if path == exp[1]: + rel_path = '/' + break + if exp[1] == '/': + # we already have rel_path == path at the beginning + continue + if path.startswith(exp[1] + '/'): + # yes we're in this exported subtree + suffix = path[len(exp[1]):] + if len(suffix) < len(rel_path): + rel_path = suffix + rel_path = ObjectPath(rel_path) + keywords[parent_method._dbus_rel_path_keyword] = rel_path + + if parent_method._dbus_destination_keyword: + keywords[parent_method._dbus_destination_keyword] = message.get_destination() + if parent_method._dbus_message_keyword: + keywords[parent_method._dbus_message_keyword] = message + if parent_method._dbus_connection_keyword: + keywords[parent_method._dbus_connection_keyword] = connection + + # call method + retval = candidate_method(self, *args, **keywords) + + # we're done - the method has got callback functions to reply with + if parent_method._dbus_async_callbacks: + return + + # otherwise we send the return values in a reply. if we have a + # signature, use it to turn the return value into a tuple as + # appropriate + if signature is not None: + signature_tuple = tuple(signature) + # if we have zero or one return values we want make a tuple + # for the _method_reply_return function, otherwise we need + # to check we're passing it a sequence + if len(signature_tuple) == 0: + if retval == None: + retval = () + else: + raise TypeError('%s has an empty output signature but did not return None' % + method_name) + elif len(signature_tuple) == 1: + retval = (retval,) + else: + if operator.isSequenceType(retval): + # multi-value signature, multi-value return... proceed unchanged + pass + else: + raise TypeError('%s has multiple output values in signature %s but did not return a sequence' % + (method_name, signature)) + + # no signature, so just turn the return into a tuple and send it as normal + else: + if retval is None: + retval = () + elif (isinstance(retval, tuple) + and not isinstance(retval, Struct)): + # If the return is a tuple that is not a Struct, we use it + # as-is on the assumption that there are multiple return + # values - this is the usual Python idiom. (fd.o #10174) + pass + else: + retval = (retval,) + + _method_reply_return(connection, message, method_name, signature, *retval) + except Exception, exception: + # send error reply + _method_reply_error(connection, message, exception) + + @method(INTROSPECTABLE_IFACE, in_signature='', out_signature='s', + path_keyword='object_path', connection_keyword='connection') + def Introspect(self, object_path, connection): + """Return a string of XML encoding this object's supported interfaces, + methods and signals. + """ + reflection_data = _dbus_bindings.DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE + reflection_data += '<node name="%s">\n' % object_path + + interfaces = self._dbus_class_table[self.__class__.__module__ + '.' + self.__class__.__name__] + for (name, funcs) in interfaces.iteritems(): + reflection_data += ' <interface name="%s">\n' % (name) + + for func in funcs.values(): + if getattr(func, '_dbus_is_method', False): + reflection_data += self.__class__._reflect_on_method(func) + elif getattr(func, '_dbus_is_signal', False): + reflection_data += self.__class__._reflect_on_signal(func) + + reflection_data += ' </interface>\n' + + for name in connection.list_exported_child_objects(object_path): + reflection_data += ' <node name="%s"/>\n' % name + + reflection_data += '</node>\n' + + return reflection_data + + def __repr__(self): + where = '' + if (self._object_path is not _MANY + and self._object_path is not None): + where = ' at %s' % self._object_path + return '<%s.%s%s at %#x>' % (self.__class__.__module__, + self.__class__.__name__, where, + id(self)) + __str__ = __repr__ + +class FallbackObject(Object): + """An object that implements an entire subtree of the object-path + tree. + + :Since: 0.82.0 + """ + + SUPPORTS_MULTIPLE_OBJECT_PATHS = True + + def __init__(self, conn=None, object_path=None): + """Constructor. + + Note that the superclass' ``bus_name`` __init__ argument is not + supported here. + + :Parameters: + `conn` : dbus.connection.Connection or None + The connection on which to export this object. If this is not + None, an `object_path` must also be provided. + + If None, the object is not initially available on any + Connection. + + `object_path` : str or None + A D-Bus object path at which to make this Object available + immediately. If this is not None, a `conn` must also be + provided. + + This object will implements all object-paths in the subtree + starting at this object-path, except where a more specific + object has been added. + """ + super(FallbackObject, self).__init__() + self._fallback = True + + if conn is None: + if object_path is not None: + raise TypeError('If object_path is given, conn is required') + elif object_path is None: + raise TypeError('If conn is given, object_path is required') + else: + self.add_to_connection(conn, object_path) diff --git a/dbus/types.py b/dbus/types.py new file mode 100644 index 0000000..cc4a678 --- /dev/null +++ b/dbus/types.py @@ -0,0 +1,9 @@ +__all__ = ('ObjectPath', 'ByteArray', 'Signature', 'Byte', 'Boolean', + 'Int16', 'UInt16', 'Int32', 'UInt32', 'Int64', 'UInt64', + 'Double', 'String', 'Array', 'Struct', 'Dictionary', + 'UTF8String') + +from _dbus_bindings import ObjectPath, ByteArray, Signature, Byte,\ + Int16, UInt16, Int32, UInt32,\ + Int64, UInt64, Dictionary, Array, \ + String, Boolean, Double, Struct, UTF8String |