841 lines
35 KiB
Python
841 lines
35 KiB
Python
# 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/>
|
|
#
|
|
# SPDX-License-Identifier: MIT
|
|
#
|
|
# 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', 'FallbackObject', 'method', 'signal')
|
|
__docformat__ = 'restructuredtext'
|
|
|
|
import sys
|
|
import logging
|
|
import threading
|
|
import traceback
|
|
try:
|
|
from collections.abc import Sequence
|
|
except ImportError:
|
|
# Python 2 (and 3.x < 3.3, but we don't support those)
|
|
from collections import Sequence
|
|
|
|
import _dbus_bindings
|
|
from dbus import (
|
|
INTROSPECTABLE_IFACE, ObjectPath, SessionBus, Signature, Struct,
|
|
validate_bus_name, validate_object_path)
|
|
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
|
|
from dbus._compat import is_py2
|
|
|
|
|
|
_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 as e:
|
|
logging.basicConfig()
|
|
if signature is None:
|
|
try:
|
|
signature = reply.guess_signature(retval) + ' (guessed)'
|
|
except Exception as 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
|
|
|
|
if not message.get_no_reply():
|
|
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)
|
|
|
|
if not message.get_no_reply():
|
|
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].items():
|
|
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
|
|
|
|
|
|
# Define Interface as an instance of the metaclass InterfaceType, in a way
|
|
# that is compatible across both Python 2 and Python 3.
|
|
Interface = InterfaceType('Interface', (object,), {})
|
|
|
|
|
|
#: 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 = threading.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 isinstance(retval, Sequence):
|
|
# 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 as 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.items():
|
|
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)
|