From 7f2e0bfe0c205af512af716f4fcb7b8508269eb7 Mon Sep 17 00:00:00 2001 From: Simon McVittie Date: Mon, 11 Jun 2007 14:51:59 +0100 Subject: [PATCH] dbus.service: Allow objects to start off unexported, and become exported later. Also allow them to be exported on more than one object path or even connection. dbus.decorators: Allow connection_keyword on signals and methods, so we can tell which connection to use for any follow-up actions. --- dbus/decorators.py | 81 ++++++++++++++------ dbus/service.py | 209 +++++++++++++++++++++++++++++++++++++++++++-------- test/test-service.py | 8 +- 3 files changed, 239 insertions(+), 59 deletions(-) diff --git a/dbus/decorators.py b/dbus/decorators.py index 9cc0dbe..aab0b77 100644 --- a/dbus/decorators.py +++ b/dbus/decorators.py @@ -34,7 +34,7 @@ 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, + message_keyword=None, connection_keyword=None, utf8_strings=False, byte_arrays=False): """Factory for decorators used to mark methods of a `dbus.service.Object` to be exported on the D-Bus. @@ -96,6 +96,12 @@ def method(dbus_interface, in_signature=None, out_signature=None, the `dbus.lowlevel.MethodCallMessage` as a keyword argument with this name. + `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. + `utf8_strings` : bool If False (default), D-Bus strings are passed to the decorated method as objects of class dbus.String, a unicode subclass. @@ -138,6 +144,8 @@ def method(dbus_interface, in_signature=None, out_signature=None, args.remove(destination_keyword) if message_keyword: args.remove(message_keyword) + if connection_keyword: + args.remove(connection_keyword) if in_signature: in_sig = tuple(_dbus_bindings.Signature(in_signature)) @@ -156,6 +164,7 @@ def method(dbus_interface, in_signature=None, out_signature=None, func._dbus_path_keyword = 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} @@ -164,7 +173,8 @@ def method(dbus_interface, in_signature=None, out_signature=None, return decorator -def signal(dbus_interface, signature=None, path_keyword=None): +def signal(dbus_interface, signature=None, path_keyword=None, + connection_keyword=None): """Factory for decorators used to mark methods of a `dbus.service.Object` to emit signals on the D-Bus. @@ -187,6 +197,19 @@ def signal(dbus_interface, signature=None, path_keyword=None): Note that when calling the decorated method, you must always pass in the object path as a keyword argument, not as a positional argument. + + `connection_keyword` : str or None + Similar to `path_keyword`, but this gives the Connection on + which the signal should be emitted. If given, and the path_keyword + is also given, the signal will be emitted at that path on that + connection; if given, but the path_keyword is not, the signal + will be emitted from every path at which this object is available + on that connection. + + If not given, the signal is emitted on every Connection on which + the object is available: if the `path_keyword` is given, it will + be emitted at that path on each Connection, otherwise it will be + emitted once per (Connection, path) pair. """ _dbus_bindings.validate_interface_name(dbus_interface) def decorator(func): @@ -195,28 +218,42 @@ def signal(dbus_interface, signature=None, path_keyword=None): def emit_signal(self, *args, **keywords): func(self, *args, **keywords) - object_path = self.__dbus_object_path__ + + object_path = None if path_keyword: - kw = keywords.pop(path_keyword, None) - if kw is not None: - if not (kw == object_path - or object_path == '/' - or kw.startswith(object_path + '/')): - raise DBusException('Object path %s is not in the ' - 'subtree starting at %s' - % (kw, object_path)) - object_path = kw - - message = _dbus_bindings.SignalMessage(object_path, - dbus_interface, - member_name) - - if signature is not None: - message.append(signature=signature, *args) + object_path = keywords.pop(path_keyword, None) + connection = None + if connection_keyword: + connection = keywords.pop(connection_keyword, None) + + if connection is None: + if object_path is None: + # any conn, any path + locations = self.locations + else: + # any conn, specified path + connections = set() + for location in self.locations: + connections.add(connection) + locations = [(connection, object_path) + for connection in connections] + elif object_path is None: + # specified conn, any path + locations = [L for L in self.locations if L[0] is connection] else: - message.append(*args) - - self._connection.send_message(message) + # specified conn, specified path + locations = ((connection, object_path),) + + for location in locations: + message = _dbus_bindings.SignalMessage(location[1], + dbus_interface, + member_name) + if signature is not None: + message.append(signature=signature, *args) + else: + message.append(*args) + + location[0].send_message(message) args = inspect.getargspec(func)[0] args.pop(0) diff --git a/dbus/service.py b/dbus/service.py index ae50751..b9f1d6a 100644 --- a/dbus/service.py +++ b/dbus/service.py @@ -26,6 +26,10 @@ 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 @@ -342,6 +346,10 @@ class InterfaceType(type): 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. @@ -374,57 +382,171 @@ class Object(Interface): 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 + # the signature of __init__ is a bit mad, for backwards compatibility 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 + `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``, - or raise TypeError if there is no ``bus_name`` either. + 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 - The D-Bus object path at which to export this Object. + `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 + `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 None: - raise TypeError('The object_path argument is required') - _dbus_bindings.validate_object_path(object_path) - if object_path == LOCAL_PATH: - raise DBusException('Objects may not be exported on the reserved ' - 'path %s' % LOCAL_PATH) + if object_path is not None: + _dbus_bindings.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: - # someone's using the old API but naming arguments, probably - if bus_name is None: - raise TypeError('Either conn or bus_name is required') - conn = bus_name.get_bus() + 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() - self._object_path = object_path self._name = bus_name - self._connection = conn - self._connection._register_object_path(object_path, self._message_cb, self._unregister_cb) + 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. + """ + 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. + """ + 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. + """ + 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. + """ + if path == LOCAL_PATH: + raise ValueError('Objects may not be exported on the reserved ' + 'path %s' % LOCAL_PATH) - __dbus_object_path__ = property(lambda self: self._object_path, None, None, - "The D-Bus object path of this object") + 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) + + 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, False)) + finally: + self._locations_lock.release() def remove_from_connection(self, connection=None, path=None): """Make this object inaccessible via the given D-Bus connection @@ -446,21 +568,42 @@ class Object(Interface): if the object was not exported on the requested connection or path, or (if both are None) was not exported at all. """ - if self._object_path is None or self._connection is None: - raise LookupError('%r is not exported' % self) - if path is not None and self._object_path != path: - raise LookupError('%r is not exported at path %r' % (self, path)) - if connection is not None and self._connection != connection: - raise LookupError('%r is not exported on %r' % (self, connection)) - + self._locations_lock.acquire() try: - self._connection._unregister_object_path(self._object_path) + 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._connection = None - self._object_path = None + self._locations_lock.release() def _unregister_cb(self, connection): - _logger.info('Unregistering exported object %r', self) + # 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): try: @@ -493,6 +636,8 @@ class Object(Interface): 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) diff --git a/test/test-service.py b/test/test-service.py index fe24010..4372392 100755 --- a/test/test-service.py +++ b/test/test-service.py @@ -50,9 +50,6 @@ OBJECT = "/org/freedesktop/DBus/TestSuitePythonObject" class RemovableObject(dbus.service.Object): # Part of test for https://bugs.freedesktop.org/show_bug.cgi?id=10457 - def __init__(self, bus_name, object_path=OBJECT + '/RemovableObject'): - super(RemovableObject, self).__init__(bus_name, object_path) - @dbus.service.method(IFACE, in_signature='', out_signature='b') def IsThere(self): return True @@ -78,7 +75,7 @@ class TestInterface(dbus.service.Interface): class TestObject(dbus.service.Object, TestInterface): def __init__(self, bus_name, object_path=OBJECT): dbus.service.Object.__init__(self, bus_name, object_path) - self._removables = [] + self._removable = RemovableObject() """ Echo whatever is sent """ @@ -229,7 +226,8 @@ class TestObject(dbus.service.Object, TestInterface): def AddRemovableObject(self): # Part of test for https://bugs.freedesktop.org/show_bug.cgi?id=10457 # Keep the removable object reffed, since that's the use case for this - self._removables.append(RemovableObject(global_name)) + self._removable.add_to_connection(self._connection, + OBJECT + '/RemovableObject') return True @dbus.service.method(IFACE, in_signature='', out_signature='b') -- 2.11.4.GIT