""" Simplified higher-level Python binding for D-Bus on top of dbussy. Provides a framework for dispatching method and signal calls, and also for on-the-fly invocation of method calls in the server from the client using proxy objects, all with the option of running via an asyncio event loop. """ #+ # Copyright 2017-2020 Lawrence D'Oliveiro . # Licensed under the GNU Lesser General Public License v2.1 or later. #- import enum from weakref import \ ref as weak_ref, \ WeakValueDictionary import asyncio import atexit import dbussy as dbus from dbussy import \ DBUS, \ DBUSX, \ Introspection #+ # High-level bus connection #- class ErrorReturn(Exception) : "Dispatch handlers can raise this to report an error that will be returned" \ " in a message back to the other end of the connection." def __init__(self, name, message) : self.args = (name, message) #end __init__ def as_error(self) : "fills in and returns an Error object that reports the specified error name and message." result = dbus.Error.init() result.set(self.args[0], self.args[1]) return \ result #end as_error #end ErrorReturn def _signal_key(fallback, interface, name) : # constructs a key for the signal-listener dictionary from the # given args. return \ (fallback, interface, name) #end _signal_key def _signal_rule(path, fallback, interface, name) : # constructs a D-Bus match rule from the given args. return \ dbus.format_rule \ ( { "type" : "signal", ("path", "path_namespace")[fallback] : dbus.unsplit_path(path), "interface" : interface, "member" : name, } ) #end _signal_rule class _DispatchNode : __slots__ = ("children", "signal_listeners") class _Interface : __slots__ = ("interface", "fallback", "listening") def __init__(self, interface, fallback) : self.interface = interface self.fallback = fallback self.listening = set() # of match rule strings #end __init #end _Interface def __init__(self) : self.children = {} # dict of path component => _DispatchNode self.signal_listeners = {} # dict of _signal_key() => list of functions #end __init__ @property def is_empty(self) : return \ ( len(self.children) == 0 and len(self.signal_listeners) == 0 ) #end _is_empty #end _DispatchNode class _ClientDispatchNode(_DispatchNode) : __slots__ = () def __init__(self, bus) : # bus arg ignored, only accepted for compatibility with _ServerDispatchNode super().__init__() #end __init__ #end _ClientDispatchNode class _ServerDispatchNode(_DispatchNode) : __slots__ = ("interfaces", "user_data") class _UserDataDict(dict) : # for holding user data, does automatic cleaning up of object # tree as items are removed. __slots__ = ("_ravel_bus",) def __init__(self, bus) : super().__init__() self._ravel_bus = weak_ref(bus) #end __init def __delitem__(self, key) : super().__delitem__(key) if len(self) == 0 : bus = self._ravel_bus() assert bus != None, "parent Connection has gone" bus._trim_dispatch(True) #end if #end __delitem__ #end _UserDataDict def __init__(self, bus) : super().__init__() self.interfaces = {} # dict of interface name => _Interface self.user_data = self._UserDataDict(bus) # for caller use #end __init__ @property def is_empty(self) : return \ ( super().is_empty and len(self.interfaces) == 0 and len(self.user_data) == 0 ) #end is_empty #end _ServerDispatchNode class _UserData : __slots__ = ("_w_conn",) def __init__(self, conn) : self._w_conn = weak_ref(conn) #end __init__ @property def conn(self) : result = self._w_conn() assert result != None return \ result #end conn def __getitem__(self, path) : node = self.conn._get_dispatch_node(path, True, True) return \ node.user_data #end __getitem__ #end _UserData class Connection(dbus.TaskKeeper) : "higher-level wrapper around dbussy.Connection. Do not instantiate directly: use" \ " the session_bus() and system_bus() calls in this module, or obtain from accepting" \ " connections on a Server().\n" \ "\n" \ "This class provides various functions, some more suited to client-side use and" \ " some more suitable to the server side. Allows for registering of @interface()" \ " classes for automatic dispatching of method calls at appropriate points in" \ " the object hierarchy." __slots__ = \ ( "connection", "notify_delay", "user_data", "_direct_connect", "bus_names_acquired", "bus_names_pending", "_client_dispatch", "_server_dispatch", "_managed_objects", "_registered_bus_names_listeners", "_bus_name_acquired_action", "_bus_name_acquired_action_arg", "_bus_name_lost_action", "_bus_name_lost_action_arg", "_props_changed", "_objects_added", "_objects_removed", ) # to forestall typos _instances = WeakValueDictionary() def __new__(celf, connection, direct_connect) : # always return the same Connection for the same dbus.Connection. if not isinstance(connection, dbus.Connection) : raise TypeError("connection must be a Connection") #end if self = celf._instances.get(connection) if self == None : self = super().__new__(celf) super()._init(self) self.connection = connection self.loop = connection.loop self.notify_delay = 0 self._client_dispatch = None # for signal listeners self._server_dispatch = None # for registered classes that field method calls self._managed_objects = None self._direct_connect = direct_connect unique_name = connection.bus_unique_name if direct_connect : assert unique_name == None, "connection already registered" self.bus_names_acquired = None self.bus_names_pending = None else : assert unique_name != None, "connection not yet registered" self.bus_names_acquired = {unique_name} self.bus_names_pending = set() #end if self._registered_bus_names_listeners = False self._props_changed = None self._objects_added = None self._objects_removed = None self._bus_name_acquired_action = None self._bus_name_acquired_action_arg = None self._bus_name_lost_action = None self._bus_name_lost_action_arg = None self.user_data = _UserData(self) celf._instances[connection] = self for interface in \ ( PeerStub, IntrospectionHandler, PropertyHandler, ) \ : self.register \ ( path = "/", interface = interface(), fallback = True ) #end for else : assert self._direct_connect == direct_connect #end if return \ self #end __new__ def __del__(self) : # Note: if remove_listeners refers directly to outer “self", # then Connection object is not disposed immediately. Passing # reference as explicit arg seems to fix this. def remove_listeners(self, is_server, level, path) : for node, child in level.children.items() : remove_listeners(self, is_server, child, path + [node]) #end for if not self._direct_connect : if is_server : for interface in level.interfaces.values() : for rulestr in interface.listening : ignore = dbus.Error.init() self.connection.bus_remove_match(rulestr, ignore) #end for #end for #end if for rulekey in level.signal_listeners : fallback, interface, name = rulekey ignore = dbus.Error.init() self.connection.bus_remove_match \ ( _signal_rule(path, fallback, interface, name), ignore ) #end for #end if #end remove_listeners #begin __del__ if self.connection != None : if self._server_dispatch != None : remove_listeners(self, True, self._server_dispatch, []) #end if if self._client_dispatch != None : remove_listeners(self, False, self._client_dispatch, []) #end if self.connection = None #end if #end __del__ def attach_asyncio(self, loop = None) : "attaches this Connection object to an asyncio event loop. If none is" \ " specified, the default event loop (as returned from asyncio.get_event_loop()" \ " is used." self.connection.attach_asyncio(loop) self.loop = self.connection.loop return \ self #end attach_asyncio @staticmethod def _bus_name_acquired(conn, msg, w_self) : # internal callback which keeps track of bus names and dispatches # to user-specified action. self = w_self() assert self != None assert not self._direct_connect, "shouldn’t be acquiring bus names on direct server connection" bus_name = msg.expect_objects("s")[0] self.bus_names_pending.discard(bus_name) if bus_name not in self.bus_names_acquired : self.bus_names_acquired.add(bus_name) if self._bus_name_acquired_action != None : result = self._bus_name_acquired_action(self, bus_name, self._bus_name_acquired_action_arg) if asyncio.iscoroutine(result) : self.create_task(result) #end if #end if #end if #end _bus_name_acquired @staticmethod def _bus_name_lost(conn, msg, w_self) : # internal callback which keeps track of bus names and dispatches # to user-specified action. self = w_self() assert self != None assert not self._direct_connect, "shouldn’t be losing bus names on direct server connection" bus_name = msg.expect_objects("s")[0] self.bus_names_pending.discard(bus_name) if bus_name in self.bus_names_acquired : self.bus_names_acquired.remove(bus_name) if self._bus_name_lost_action != None : result = self._bus_name_lost_action(self, bus_name, self._bus_name_lost_action_arg) if asyncio.iscoroutine(result) : self.create_task(result) #end if #end if #end if #end _bus_name_lost def set_bus_name_acquired_action(self, action, action_arg) : "sets the action (if not None) to be called on receiving a bus-name-acquired" \ " signal. action is invoked as\n" \ "\n" \ " action(conn, bus_name, action_arg)\n" \ "\n" \ "where conn is the Connection object and bus_name is the name." assert not self._direct_connect, "cannot acquire bus names on direct server connection" self._bus_name_acquired_action = action self._bus_name_acquired_action_arg = action_arg #end set_bus_name_acquired_action def set_bus_name_lost_action(self, action, action_arg) : "sets the action (if not None) to be called on receiving a bus-name-lost" \ " signal. action is invoked as\n" \ "\n" \ " action(conn, bus_name, action_arg)\n" \ "\n" \ "where conn is the Connection object and bus_name is the name." assert not self._direct_connect, "cannot acquire bus names on direct server connection" self._bus_name_lost_action = action self._bus_name_lost_action_arg = action_arg #end set_bus_name_lost_action def request_name(self, bus_name, flags) : "registers a bus name." assert not self._direct_connect, "cannot register bus names on direct server connection" if not self._registered_bus_names_listeners : self.connection.bus_add_match_action \ ( rule = "type=signal,interface=org.freedesktop.DBus,member=NameAcquired", func = self._bus_name_acquired, user_data = weak_ref(self) ) self.connection.bus_add_match_action \ ( rule = "type=signal,interface=org.freedesktop.DBus,member=NameLost", func = self._bus_name_lost, user_data = weak_ref(self) ) self._registered_bus_names_listeners = True #end if return \ self.connection.bus_request_name(bus_name, flags) #end request_name async def request_name_async(self, bus_name, flags, error = None, timeout = DBUS.TIMEOUT_USE_DEFAULT) : "registers a bus name." assert not self._direct_connect, "cannot register bus names on direct server connection" assert self.loop != None, "no event loop to attach coroutine to" if not self._registered_bus_names_listeners : self._registered_bus_names_listeners = True # do first in case of reentrant call await self.connection.bus_add_match_action_async \ ( rule = "type=signal,interface=org.freedesktop.DBus,member=NameAcquired", func = self._bus_name_acquired, user_data = weak_ref(self) ) await self.connection.bus_add_match_action_async \ ( rule = "type=signal,interface=org.freedesktop.DBus,member=NameLost", func = self._bus_name_lost, user_data = weak_ref(self) ) #end if is_acquired = bus_name in self.bus_names_acquired is_pending = bus_name in self.bus_names_pending if not (is_acquired or is_pending) : self.bus_names_pending.add(bus_name) result = await self.connection.bus_request_name_async(bus_name, flags, error = error, timeout = timeout) if error != None and error.is_set or result != DBUS.REQUEST_NAME_REPLY_IN_QUEUE : self.bus_names_pending.discard(bus_name) #end if elif is_pending : result = DBUS.REQUEST_NAME_REPLY_IN_QUEUE else : result = DBUS.REQUEST_NAME_REPLY_ALREADY_OWNER #end if return \ result #end request_name_async def release_name(self, bus_name) : "releases a registered bus name." assert not self._direct_connect, "cannot register bus names on direct server connection" return \ self.connection.bus_release_name(bus_name) #end release_name async def release_name_async(self, bus_name, error = None, timeout = DBUS.TIMEOUT_USE_DEFAULT) : "releases a registered bus name." assert not self._direct_connect, "cannot register bus names on direct server connection" assert self.loop != None, "no event loop to attach coroutine to" return \ await self.connection.bus_release_name_async(bus_name, error = error, timeout = timeout) #end release_name_async def _trim_dispatch(self, is_server) : # removes empty subtrees from the object tree. def trim_dispatch_node(level) : to_delete = set() for node, child in level.children.items() : trim_dispatch_node(child) if child.is_empty : to_delete.add(node) #end if #end for for node in to_delete : del level.children[node] #end for #end trim_dispatch_node #begin _trim_dispatch dispatch = (self._client_dispatch, self._server_dispatch)[is_server] if dispatch != None : trim_dispatch_node(dispatch) if dispatch.is_empty : if is_server : self._server_dispatch = None else : self._client_dispatch = None #end if #end if #end if #end _trim_dispatch def _get_dispatch_node(self, path, is_server, create_if) : # returns the appropriate _DispatchNode entry in the # client or server dispatch tree (depending on is_server) for # the specified path, or None if no such and not create_if. if create_if : if is_server and self._server_dispatch == None : self._server_dispatch = _ServerDispatchNode(self) elif not is_server and self._client_dispatch == None : self._client_dispatch = _ClientDispatchNode(self) #end if #end if level = (self._client_dispatch, self._server_dispatch)[is_server] DispatchNode = (_ClientDispatchNode, _ServerDispatchNode)[is_server] if level != None : levels = iter(dbus.split_path(path)) while True : component = next(levels, None) if component == None : break # found if level != None if component not in level.children : if not create_if : level = None break #end if level.children[component] = DispatchNode(self) #end if level = level.children[component] # search another step down the path #end while #end if return \ level #end _get_dispatch_node def _remove_matches(self, dispatch) : if not self._direct_connect : for rulestr in dispatch.listening : ignore = dbus.Error.init() self.connection.bus_remove_match(rulestr, ignore) #end for #end if #end _remove_matches def register_additional_standard(self, **kwargs) : "registers additional standard interfaces that are not automatically" \ " installed at Connection creation time. Currently the only one is" \ " the object-manager interface, registered with\n" \ "\n" \ " «conn».register_additional_standard(managed_objects = True)\n" for key in kwargs : if kwargs[key] : if key == "managed_objects" : if self._managed_objects != None : raise asyncio.InvalidStateError \ ( "object manager interface already registered" ) #end if self.register \ ( path = "/", interface = ManagedObjectsHandler(), fallback = True ) self._managed_objects = {} else : raise TypeError("unrecognized argument keyword “%s”" % key) #end if #end if #end for return \ self #end register_additional_standard def register(self, path, fallback, interface, replace = True) : "for server-side use; registers the specified instance of an @interface()" \ " class for handling method calls on the specified path, and also on subpaths" \ " if fallback." if is_interface_instance(interface) : iface_type = type(interface) elif is_interface(interface) : # assume can instantiate without arguments iface_type = interface interface = iface_type() else : raise TypeError("interface must be an @interface() class or instance thereof") #end if if self._server_dispatch == None : self._server_dispatch = _ServerDispatchNode(self) self.connection.add_filter(_message_interface_dispatch, weak_ref(self)) #end if level = self._server_dispatch for component in dbus.split_path(path) : if component not in level.children : level.children[component] = _ServerDispatchNode(self) #end if level = level.children[component] #end for interface_name = iface_type._interface_name if interface_name in level.interfaces : entry = level.interfaces[interface_name] existing_kind = type(entry.interface)._interface_kind if not replace or existing_kind != iface_type._interface_kind : raise KeyError \ ( "already registered an interface named “%s” of kind %s" % (interface_name, existing_kind) ) #end if self._remove_matches(entry) #end if entry = _ServerDispatchNode._Interface(interface, fallback) if iface_type._interface_kind != INTERFACE.SERVER and not self._direct_connect : signals = iface_type._interface_signals for name in signals : if not iface_type._interface_signals[name]._signal_info["stub"] : rulestr = _signal_rule(path, fallback, interface_name, name) self.connection.bus_add_match(rulestr) entry.listening.add(rulestr) #end if #end for #end for level.interfaces[interface_name] = entry #end register def unregister(self, path, interface = None) : "for server-side use; unregisters the specified interface class (or all" \ " registered interface classes, if None) from handling method calls on path." if interface != None : if is_interface_instance(interface) : interface = type(interface) elif not is_interface(interface) : raise TypeError("interface must be None or an @interface() class or instance thereof") #end if #end if if self._server_dispatch != None : level = self._server_dispatch levels = iter(dbus.split_path(path)) while True : component = next(levels, None) if component == None : if interface != None : interfaces = {interface._interface_name} else : interfaces = set(level.interfaces.keys()) #end if for iface_name in interfaces : self._remove_matches(level.interfaces[iface_name]) del level.interfaces[iface_name] #end for break #end if if component not in level.children : break level = level.children[component] #end while self._trim_dispatch(True) #end if #end unregister def listen_signal(self, path, fallback, interface, name, func) : "for client-side use; registers a callback which will be invoked when a" \ " signal is received for the specified path, interface and name." if not hasattr(func, "_signal_info") : raise TypeError("callback must have @signal() decorator applied") #end if signal_info = func._signal_info entry = self._get_dispatch_node(path, False, True) # Should I bother to check it matches a registered interface and # defined signal therein? # Also, should I pay any attention to signal_info["name"]? Perhaps # default if name arg is None? listeners = entry.signal_listeners rulekey = _signal_key(fallback, interface, name) if rulekey not in listeners : if not self._direct_connect : self.connection.bus_add_match(_signal_rule(path, fallback, interface, name)) #end if listeners[rulekey] = [] #end if listeners[rulekey].append(func) #end listen_signal def unlisten_signal(self, path, fallback, interface, name, func) : "for client-side use; unregisters a previously-registered callback" \ " which would have been invoked when a signal is received for the" \ " specified path, interface and name." entry = self._get_dispatch_node(path, False, False) if entry != None : signal_listeners = entry.signal_listeners rulekey = _signal_key(fallback, interface, name) if rulekey in signal_listeners : listeners = signal_listeners[rulekey] try : listeners.pop(listeners.index(func)) except ValueError : pass #end try if len(listeners) == 0 : if not self._direct_connect : ignore = dbus.Error.init() self.connection.bus_remove_match \ ( _signal_rule(path, fallback, interface, name), ignore ) #end if del signal_listeners[rulekey] # as a note to myself that I will need to call bus_add_match # if a new listener is added #end if #end if self._trim_dispatch(False) #end if #end unlisten_signal def listen_propchanged(self, path, fallback, interface, func) : "special case of Connection.listen_signal specifically for listening" \ " for properties-changed signals. The interface is ignored for now;" \ " your listener will have to check for matches on this itself." self.listen_signal \ ( path = path, fallback = fallback, interface = DBUS.INTERFACE_PROPERTIES, name = "PropertiesChanged", func = func, ) #end listen_propchanged def unlisten_propchanged(self, path, fallback, interface, func) : "special case of Connection.unlisten_signal specifically for listening" \ " for properties-changed signals. The interface is ignored for now;" \ " your listener has to check for matches on this itself." self.unlisten_signal \ ( path = path, fallback = fallback, interface = DBUS.INTERFACE_PROPERTIES, name = "PropertiesChanged", func = func, ) #end unlisten_propchanged def listen_objects_added(self, func) : self.listen_signal \ ( path = "/", fallback = True, interface = DBUSX.INTERFACE_OBJECT_MANAGER, name = "InterfacesAdded", func = func, ) #end listen_objects_added def unlisten_objects_added(self, func) : self.unlisten_signal \ ( path = "/", fallback = True, interface = DBUSX.INTERFACE_OBJECT_MANAGER, name = "InterfacesAdded", func = func, ) #end unlisten_objects_added def listen_objects_removed(self, func) : self.listen_signal \ ( path = "/", fallback = True, interface = DBUSX.INTERFACE_OBJECT_MANAGER, name = "InterfacesRemoved", func = func, ) #end listen_objects_removed def unlisten_objects_removed(self, func) : self.unlisten_signal \ ( path = "/", fallback = True, interface = DBUSX.INTERFACE_OBJECT_MANAGER, name = "InterfacesRemoved", func = func, ) #end unlisten_objects_removed def get_dispatch_interface(self, path, interface_name) : "returns the appropriate instance of a previously-registered interface" \ " class for handling calls to the specified interface name for the" \ " specified object path, or None if no such." fallback = None # to begin with level = self._server_dispatch if level != None : levels = iter(dbus.split_path(path)) while True : component = next(levels, None) if ( interface_name in level.interfaces and (level.interfaces[interface_name].fallback or component == None) ) : iface = level.interfaces[interface_name].interface else : iface = fallback #end if if ( component == None # reached bottom of path or component not in level.children # no handlers to be found further down path ) : break #end if fallback = iface level = level.children[component] # search another step down the path #end while else : iface = None #end if return \ iface #end get_dispatch_interface def _get_iface_entry(self, path, interface, name, namekind) : iface = self.get_dispatch_interface(path, interface) if iface == None : raise TypeError \ ( "no suitable interface %s for object %s" % (interface, dbus.unsplit_path(path)) ) #end if iface_type = type(iface) if iface_type._interface_kind == (INTERFACE.SERVER, INTERFACE.CLIENT)[namekind == "signal"] : raise TypeError \ ( "cannot send %s call from %s side" % (("method", "server"), ("signal", "client"))[namekind == "signal"] ) #end if lookup = getattr \ ( iface_type, { "method" : "_interface_methods", "signal" : "_interface_signals", "property" : "_interface_props", }[namekind] ) if name not in lookup : raise KeyError \ ( "name “%s” is not a %s of interface “%s”" % (name, namekind, interface) ) #end if return \ lookup[name] #end _get_iface_entry def _notify_props_changed(self) : # callback that is queued on the event loop to actually send the # properties-changed notification signals. if self._props_changed != None : done = set() now = self.loop.time() for key in self._props_changed : entry = self._props_changed[key] path, interface = key if entry["at"] <= now : self.send_signal \ ( path = path, interface = DBUS.INTERFACE_PROPERTIES, name = "PropertiesChanged", args = (interface, entry["changed"], sorted(entry["invalidated"])) ) done.add(key) #end if #end for for key in done : del self._props_changed[key] #end for if len(self._props_changed) == 0 : # all done for now self._props_changed = None # indicates I am not pending to be called any more else : # another notification waiting to be sent later next_time = min(entry["at"] for entry in self._props_changed.values()) self.loop.call_at(next_time, self._notify_props_changed) #end if #end if #end _notify_props_changed def _get_all_my_props(self, message, path, interface_name) : # utility wrapper that retrieves all property values for the specified # object path defined by the specified interface. Returns two results: # property values, and list of (propname, coroutine) tuples for async # propgetters. Could raise ErrorReturn if a propgetter does so. dispatch = self.get_dispatch_interface(path, interface_name) props = type(dispatch)._interface_props propnames = iter(props.keys()) properror = None propvalues = {} to_await = [] while True : propname = next(propnames, None) if propname == None : break propentry = props[propname] if "getter" in propentry : getter = getattr(dispatch, propentry["getter"].__name__) kwargs = {} for keyword_keyword, value in \ ( ("name_keyword", lambda : propname), ("connection_keyword", lambda : self.connection), ("message_keyword", lambda : message), ("path_keyword", lambda : path), ("bus_keyword", lambda : bus), ) \ : if getter._propgetter_info[keyword_keyword] != None : value = value() if value == None : raise ValueError \ ( "getter for prop “%s” expects a %s arg but" " no value supplied for that" % (propname, keyword_keyword) ) #end if kwargs[getter._propgetter_info[keyword_keyword]] = value #end if #end for propvalue = getter(**kwargs) # could raise ErrorReturn if asyncio.iscoroutine(propvalue) : if self.loop == None : raise TypeError \ ( "not expecting getter for prop “%s” to be coroutine" % propname ) #end if to_await.append((propname, propvalue)) #end if propvalues[propname] = (propentry["type"], propvalue) #end if #end for return \ propvalues, to_await #end _get_all_my_props def prop_changed(self, path, interface, propname, proptype, propvalue) : "indicates that a signal should be sent notifying of a change to the specified" \ " property of the specified object path in the specified interface. propvalue" \ " is either the new value to be included in the signal, or None to indicate" \ " that the property has merely become invalidated, and its new value needs" \ " to be obtained explicitly.\n" \ "\n" \ "If there is an event loop attached, then multiple calls to this with different" \ " properties on the same path and interface can be batched up into a single" \ " signal notification." assert (proptype != None) == (propvalue != None), \ "either specify both of proptype and propvalue, or neither" if self.loop != None : queue_task = False if self._props_changed == None : self._props_changed = {} queue_task = True #end if key = (dbus.unsplit_path(path), interface) if key not in self._props_changed : self._props_changed[key] = \ { "at" : self.loop.time() + self.notify_delay, "changed" : {}, "invalidated" : set(), } #end if if propvalue != None : self._props_changed[key]["changed"][propname] = (proptype, propvalue) else : self._props_changed[key]["invalidated"].add(propname) #end if if queue_task : if self.notify_delay != 0 : self.loop.call_later(self.notify_delay, self._notify_props_changed) else : self.loop.call_soon(self._notify_props_changed) #end if #end if else : # cannot batch them up--send message immediately changed = {} invalidated = [] if propvalue != None : changed[propname] = (proptype, propvalue) else : invalidated.append(propname) #end if self.send_signal \ ( path = path, interface = DBUS.INTERFACE_PROPERTIES, name = "PropertiesChanged", args = (interface, changed, invalidated) ) #end if #end prop_changed def _notify_objects_added(self) : # callback that is queued on the event loop to actually send the # objects-added notification signals. if self._objects_added != None : notify_again = None if self.loop != None : now = self.loop.time() else : now = None #end if paths_to_delete = set() for path in sorted(self._objects_added.keys()) : entry = self._objects_added[path] added = {} for interface, iface_entry in entry.items() : when = iface_entry["at"] if when == None or when <= now : added[interface] = iface_entry["props"] else : if notify_again == None : notify_again = when else : notify_again = min(notify_again, when) #end if #end if #end for if len(added) != 0 : self.send_signal \ ( path = path, interface = DBUSX.INTERFACE_OBJECT_MANAGER, name = "InterfacesAdded", args = (path, added) ) for interface in added : del entry[interface] #end for #end if if len(entry) == 0 : paths_to_delete.add(path) #end if #end for for path in paths_to_delete : del self._objects_added[path] #end for if len(self._objects_added) == 0 : self._objects_added = None #end if if notify_again != None : self.loop.call_later(notify_again - now, self._notify_objects_added) #end if #end if #end _notify_objects_added def _notify_objects_removed(self) : # callback that is queued on the event loop to actually send the # objects-removed notification signals. if self._objects_removed != None : notify_again = None if self.loop != None : now = self.loop.time() else : now = None #end if paths_to_delete = set() for path in sorted(self._objects_removed.keys()) : entry = self._objects_removed[path] removed = set() for interface in entry : when = entry[interface]["at"] if when == None or when <= now : removed.add(interface) else : if notify_again == None : notify_again = when else : notify_again = min(notify_again, when) #end if #end if #end for if len(removed) != 0 : self.send_signal \ ( path = path, interface = DBUSX.INTERFACE_OBJECT_MANAGER, name = "InterfacesRemoved", args = (path, sorted(removed)) ) for interface in removed : del entry[interface] #end for #end if if len(entry) == 0 : paths_to_delete.add(path) #end if #end for for path in paths_to_delete : del self._objects_removed[path] #end for if len(self._objects_removed) == 0 : self._objects_removed = None #end if if notify_again != None : self.loop.call_later(notify_again - now, self._notify_objects_removed) #end if #end if #end _notify_objects_removed def find_interfaces_for_object(self, path) : "returns a dict of interfaces, keyed by name, applicable to the" \ " given object path." level = self._server_dispatch result = {} if level != None : levels = iter(dbus.split_path(path)) while True : component = next(levels, None) for interface_name, interface in level.interfaces.items() : if component == None or interface.fallback : result[interface_name] = interface.interface # Note that a fallback entry might be replaced # by a more specific one further down the path. #end if #end for if ( component == None # reached bottom of path or component not in level.children # no handlers to be found further down path ) : break #end if level = level.children[component] # search another step down the path #end while #end if return \ result #end find_interfaces_for_object def object_added(self, path, interfaces_and_props = None) : "Call this to send an ObjectManager notification about the addition of" \ " the specified interfaces and property values to the specified object" \ " path. The ObjectManager interface must already have been registered on" \ " this Connection, by calling" \ " «conn».register_additional_standard(managed_objects = True)." added_entry = None to_await = [] queue_task = False notify_when = None def queue_notify(deferred) : if queue_task : if deferred : delay = notify_when - self.loop.time() else : delay = self.notify_delay #end if if delay > 0 : self.loop.call_later(delay, self._notify_objects_added) else : self.loop.call_soon(self._notify_objects_added) #end if #end if #end queue_notify async def await_propvalues() : nonlocal queue_task for path, interface_name, propname, fute in to_await : propvalue = await fute # don’t trap ErrorReturn propvalues = added_entry[interface_name]["props"] propvalues[propname] = (propvalues[propname][0], propvalue) #end for if self._objects_added == None : # might have happened in meantime self._objects_added = {} queue_task = True #end if self._objects_added[path] = added_entry # all prop values now complete queue_notify(True) #end await_propvalues #begin object_added path = dbus.unsplit_path(path) if self._managed_objects == None : raise RuntimeError("ObjectManager interface needs to be registered on this Connection") #end if if interfaces_and_props == None : # get all applicable interface names, props will be retrieved below intfs = self.find_interfaces_for_object(path) interfaces_and_props = dict \ ( (iface_name, None) for iface_name in intfs if len(intfs[iface_name]._interface_props) != 0 ) #end if if path in self._managed_objects : obj_entry = self._managed_objects[path] else : obj_entry = set() self._managed_objects[path] = obj_entry #end if if self._objects_added == None : self._objects_added = {} queue_task = True #end if if path in self._objects_added : added_entry = self._objects_added[path] else : added_entry = {} #end if if self.loop != None : notify_when = self.loop.time() + self.notify_delay else : notify_when = None #end if for interface, props in interfaces_and_props.items() : if props != None : added_props = {} for propname, propvalue in props.items() : if not isinstance(propvalue, (list, tuple)) or len(propvalue) != 2 : raise TypeError \ ( "value for property “%s” must be (type, value) pair" % propname ) #end if proptype = dbus.parse_single_signature(propvalue[0]) propvalue = proptype.validate(propvalue[1]) proptype = dbus.unparse_signature(proptype) added_props[propname] = (proptype, propvalue) #end for else : added_props, await_props = self._get_all_my_props(None, path, interface) # propgetters should not expect a message arg for propname, propvalue in await_props : to_await.append((path, interface, propname, propvalue)) #end for #end if added_entry[interface] = \ { "at" : notify_when, "props" : added_props, } obj_entry.add(interface) #end for if len(to_await) == 0 : self._objects_added[path] = added_entry # all prop values complete #end if if self.loop != None : if len(to_await) != 0 : self.create_task(await_propvalues()) else : queue_notify(False) #end if else : # cannot queue, notify immediately self._notify_objects_added() #end if #end object_added def object_removed(self, path, interfaces = None) : "Call this to send an ObjectManager notification about the removal of the" \ " specified set/sequence of interfaces from the specified object path. The" \ " ObjectManager interface must already have been registered on this Connection," \ " by calling «conn».register_additional_standard(managed_objects = True)." path = dbus.unsplit_path(path) if self._managed_objects == None : raise RuntimeError("ObjectManager interface not registered on this Connection") #end if queue_task = False if self._objects_removed == None : self._objects_removed = {} queue_task = True #end if if self._objects_added != None : added_entry = self._objects_added.get(path) else : added_entry = None #end if obj_entry = self._managed_objects[path] if path in self._objects_removed : removed_entry = self._objects_removed[path] else : removed_entry = {} self._objects_removed[path] = removed_entry #end if if self.loop != None : when = self.loop.time() + self.notify_delay else : when = None #end if if interfaces == None : intfs = self.find_interfaces_for_object(path) interfaces = set \ ( iface_name for iface_name in intfs if len(intfs[iface_name]._interface_props) != 0 ) #end if for interface in interfaces : if added_entry != None and interface in added_entry : # object-added notification was never sent, just cancel # it and don’t send object-removed notification del added_entry[interface] else : removed_entry[interface] = {"at" : when} #end if if self._props_changed != None : props_key = (path, interface) if self._props_changed != None and props_key in self._props_changed : # cancel pending properties-changed notification del self._props_changed[key] if len(self._props_changed) == 0 : self._props_changed = None #end if #end if #end if obj_entry.remove(interface) #end for if len(obj_entry) == 0 : del self._managed_objects[path] #end if if added_entry != None and len(added_entry) == 0 : del self._objects_added[path] if len(self._objects_added) == 0 : self._objects_added = None #end if #end if if len(removed_entry) != 0 : if self.loop != None : if queue_task : if self.notify_delay != 0 : self.loop.call_later(self.notify_delay, self._notify_objects_removed) else : self.loop.call_soon(self._notify_objects_removed) #end if #end if else : # cannot queue, notify immediately self._notify_objects_removed() #end if #end if #end object_removed def send_signal(self, *, path, interface, name, args) : "intended for server-side use: sends a signal with the specified" \ " interface and name from the specified object path. There must" \ " already be a registered interface instance with that name which" \ " defines that signal for that path." call_info = self._get_iface_entry(path, interface, name, "signal")._signal_info message = dbus.Message.new_signal \ ( path = dbus.unsplit_path(path), iface = interface, name = name ) message.append_objects(call_info["in_signature"], *args) self.connection.send(message) #end send_signal def introspect(self, destination, path, timeout = DBUS.TIMEOUT_USE_DEFAULT) : "sends an Introspect request to the specified bus name and object path," \ " and returns the resulting parsed Introspection structure." message = dbus.Message.new_method_call \ ( destination = destination, path = dbus.unsplit_path(path), iface = DBUS.INTERFACE_INTROSPECTABLE, method = "Introspect" ) reply = self.connection.send_with_reply_and_block(message, timeout) return \ Introspection.parse(reply.expect_return_objects("s")[0]) #end introspect async def introspect_async(self, destination, path, timeout = DBUS.TIMEOUT_USE_DEFAULT) : "sends an Introspect request to the specified bus name and object path," \ " and returns the resulting parsed Introspection structure." assert self.loop != None, "no event loop to attach coroutine to" message = dbus.Message.new_method_call \ ( destination = destination, path = dbus.unsplit_path(path), iface = DBUS.INTERFACE_INTROSPECTABLE, method = "Introspect" ) reply = await self.connection.send_await_reply(message, timeout) return \ Introspection.parse(reply.expect_return_objects("s")[0]) #end introspect_async def __getitem__(self, bus_name) : "for client-side use; lets you obtain references to bus peers by" \ " looking up their names in this Connection object as though it" \ " were a mapping." return \ BusPeer(self, bus_name) #end __getitem__ def get_proxy_object(self, bus_name, path) : "for client-side use; returns a BusPeer.Object instance for communicating" \ " with a specified server object. You can then call get_interface" \ " on the result to create an interface object that can be used to" \ " call any method defined on the server by that interface." return \ BusPeer(self, bus_name)[path] #end get_proxy_object def get_proxy_interface(self, destination, path, interface, timeout = DBUS.TIMEOUT_USE_DEFAULT) : "sends an Introspect request to the specified bus name and object path" \ " (if interface is not an Interface object or the name of one of the standard" \ " interfaces), and generates a client-side proxy interface for that interface." if isinstance(interface, Introspection.Interface) : definition = interface interface = definition.name elif isinstance(interface, str) : if interface in dbus.standard_interfaces : definition = dbus.standard_interfaces[interface] else : introspection = self.introspect(destination, path, timeout) interfaces = introspection.interfaces_by_name if interface not in interfaces : raise dbus.DBusError \ ( DBUS.ERROR_UNKNOWN_INTERFACE, "peer “%s” object “%s” does not understand interface “%s”" % (destination, path, interface) ) #end if definition = interfaces[interface] #end if else : raise TypeError("interface must be an Interface or name of one") #end if return \ def_proxy_interface \ ( name = interface, kind = INTERFACE.CLIENT, introspected = definition, is_async = False ) #end get_proxy_interface async def get_proxy_interface_async(self, destination, path, interface, timeout = DBUS.TIMEOUT_USE_DEFAULT) : "sends an Introspect request to the specified bus name and object path" \ " (if interface is not an Interface object or the name of one of the standard" \ " interfaces), and generates a client-side proxy interface for that interface." assert self.loop != None, "no event loop to attach coroutine to" if isinstance(interface, Introspection.Interface) : definition = interface interface = definition.name elif isinstance(interface, str) : if interface in dbus.standard_interfaces : definition = dbus.standard_interfaces[interface] else : introspection = await self.introspect_async(destination, path, timeout) interfaces = introspection.interfaces_by_name if interface not in interfaces : raise dbus.DBusError \ ( DBUS.ERROR_UNKNOWN_INTERFACE, "peer “%s” object “%s” does not understand interface “%s”" % (destination, path, interface) ) #end if definition = interfaces[interface] #end if else : raise TypeError("interface must be an Interface or name of one") #end if return \ def_proxy_interface \ ( name = interface, kind = INTERFACE.CLIENT, introspected = definition, is_async = True ) #end get_proxy_interface_async #end Connection def session_bus(**kwargs) : "returns a Connection object for the current D-Bus session bus." return \ Connection(dbus.Connection.bus_get(DBUS.BUS_SESSION, private = False), False) \ .register_additional_standard(**kwargs) #end session_bus def system_bus(**kwargs) : "returns a Connection object for the D-Bus system bus." return \ Connection(dbus.Connection.bus_get(DBUS.BUS_SYSTEM, private = False), False) \ .register_additional_standard(**kwargs) #end system_bus def starter_bus(**kwargs) : "returns a Connection object for the D-Bus starter bus." return \ Connection(dbus.Connection.bus_get(DBUS.BUS_STARTER, private = False), False) \ .register_additional_standard(**kwargs) #end starter_bus async def session_bus_async(loop = None, **kwargs) : "returns a Connection object for the current D-Bus session bus." return \ Connection \ ( await dbus.Connection.bus_get_async(DBUS.BUS_SESSION, private = False, loop = loop), False ) \ .register_additional_standard(**kwargs) #end session_bus_async async def system_bus_async(loop = None, **kwargs) : "returns a Connection object for the D-Bus system bus." return \ Connection \ ( await dbus.Connection.bus_get_async(DBUS.BUS_SYSTEM, private = False, loop = loop), False ) \ .register_additional_standard(**kwargs) #end system_bus_async async def starter_bus_async(loop = None, **kwargs) : "returns a Connection object for the D-Bus starter bus." return \ Connection \ ( await dbus.Connection.bus_get_async(DBUS.BUS_STARTER, private = False, loop = loop), False ) \ .register_additional_standard(**kwargs) #end starter_bus_async def connect_server(address, private, **kwargs) : "opens a connection to a server at the specified network address and" \ " returns a Connection object for the connection." return \ Connection(dbus.Connection.open(address, private = private), True) \ .register_additional_standard(**kwargs) #end connect_server async def connect_server_async(address, private, loop = None, timeout = DBUS.TIMEOUT_INFINITE, **kwargs) : "opens a connection to a server at the specified network address and" \ " returns a Connection object for the connection." return \ Connection \ ( await dbus.Connection.open_async(address, private = private, loop = loop, timeout = timeout), True ) \ .register_additional_standard(**kwargs) #end connect_server_async class Server : "listens for connections on a particular socket address, separate from" \ " the D-Bus daemon. Requires asyncio." __slots__ = ("server",) def __init__(self, address, loop = None) : self.server = dbus.Server.listen(address) self.server.attach_asyncio(loop) #end __init__ def __del__(self) : self.server.disconnect() #end __del__ async def await_new_connection(self, timeout = DBUS.TIMEOUT_INFINITE) : "waits for a new connection attempt and returns a wrapping Connection object." \ " If no connection appears within the specified timeout, returns None." conn = await self.server.await_new_connection(timeout) if conn != None : result = Connection(conn, True) else : result = None #end if return \ result #end await_new_connection #end Server #+ # Proxy interface objects -- for client-side use #- class BusPeer : "Intended for client-side use: a proxy for a D-Bus peer. These offer two" \ " different ways to traverse the bus-name/path/interface hierarchy. Start" \ " by obtaining a BusPeer object from a Connection:\n" \ "\n" \ " peer = conn[«bus_name»]\n" \ "\n" \ "Now you can either reference a proxy object by specifying its path:\n" \ "\n" \ " obj = peer[«object_path»]\n" \ "\n" \ "from which you can get a proxy interface thus:\n" \ "\n" \ " iface = obj.get_interface(«iface_name»)\n" \ "\n" \ "Or you can get a root proxy interface from the bus peer by" \ " introspecting some arbitrary (but suitable) reference path:\n" \ "\n" \ " iface_root = peer.get_interface(«reference_path», «iface_name»)\n" \ "\n" \ "from which you can obtain a proxy interface for a specific object thus:\n" \ "\n" \ " iface = iface_root[«object_path»]\n" \ "\n" \ "Whichever way you do it, you can now make method calls on the iface object" \ " which will automatically be communicated as method calls to the peer via" \ " the D-Bus." __slots__ = ("conn", "bus_name") class RootProxy : "abstract base class for identifying root proxy classes." pass #end RootProxy class Object : "identifies a proxy object by a bus, a bus name and a path." __slots__ = ("conn", "peer", "bus_name", "path") class ProxyInterface : "abstract base class for identifying proxy interface classes." pass #end ProxyInterface def __init__(self, conn, bus_name, path) : if not isinstance(conn, Connection) : raise TypeError("conn must be a Connection") #end if dbus.validate_bus_name(bus_name) self.conn = conn self.bus_name = bus_name self.path = path #end __init__ def introspect(self, timeout = DBUS.TIMEOUT_USE_DEFAULT) : "sends an Introspect request to the specified bus name and object path," \ " and returns the resulting parsed Introspection structure." return \ self.conn.introspect \ ( destination = self.bus_name, path = self.path, timeout = timeout, ) #end introspect async def introspect_async(self, timeout = DBUS.TIMEOUT_USE_DEFAULT) : "sends an Introspect request to the specified bus name and object path," \ " and returns the resulting parsed Introspection structure." return await \ self.conn.introspect_async \ ( destination = self.bus_name, path = self.path, timeout = timeout, ) #end introspect_async def get_interface(self, interface, timeout = DBUS.TIMEOUT_USE_DEFAULT) : "sends an Introspect request to the specified bus name and object path" \ " (if interface is not one of the standard interfaces), and generates" \ " a client-side proxy interface for the interface with the specified name." return \ self.conn.get_proxy_interface \ ( destination = self.bus_name, path = self.path, interface = interface, timeout = timeout )( connection = self.conn.connection, dest = self.bus_name, timeout = timeout, )[self.path] #end get_interface async def get_async_interface(self, interface, timeout = DBUS.TIMEOUT_USE_DEFAULT) : "sends an Introspect request to the specified bus name and object path" \ " (if interface is not one of the standard interfaces), and generates" \ " a client-side proxy interface for the interface with the specified name." return \ (await self.conn.get_proxy_interface_async ( destination = self.bus_name, path = self.path, interface = interface, timeout = timeout ))( connection = self.conn.connection, dest = self.bus_name, timeout = timeout, )[self.path] #end get_async_interface #end Object def __init__(self, conn, bus_name) : self.conn = conn self.bus_name = bus_name #end __init__ def __getitem__(self, path) : "lets you obtain references to objects implemented by this bus peer" \ " by using their paths as lookup keys in a mapping. Of course, there" \ " is no guarantee such an object actually exists within the peer." return \ type(self).Object(self.conn, self.bus_name, path) #end __getitem__ def introspect(self, path, timeout = DBUS.TIMEOUT_USE_DEFAULT) : "sends an Introspect request to the specified bus name and object path," \ " and returns the resulting parsed Introspection structure." return \ self.conn.introspect \ ( destination = self.bus_name, path = path, timeout = timeout, ) #end introspect async def introspect_async(self, path, timeout = DBUS.TIMEOUT_USE_DEFAULT) : "sends an Introspect request to the specified bus name and object path," \ " and returns the resulting parsed Introspection structure." return await \ self.conn.introspect_async \ ( destination = self.bus_name, path = path, timeout = timeout, ) #end introspect_async def get_interface(self, path, interface, timeout = DBUS.TIMEOUT_USE_DEFAULT) : "sends an Introspect request to the specified bus name and object path" \ " (if interface is not one of the standard interfaces), and generates" \ " a client-side proxy interface for the interface with the specified name." return \ self.conn.get_proxy_interface \ ( destination = self.bus_name, path = path, interface = interface, timeout = timeout, )( connection = self.conn.connection, dest = self.bus_name, timeout = timeout, ) #end get_interface async def get_interface_async(self, path, interface, timeout = DBUS.TIMEOUT_USE_DEFAULT) : "sends an Introspect request to the specified bus name and object path" \ " (if interface is not one of the standard interfaces), and generates" \ " a client-side proxy interface for the interface with the specified name." return \ (await self.conn.get_proxy_interface_async ( destination = self.bus_name, path = path, interface = interface, timeout = timeout, ))( connection = self.conn.connection, dest = self.bus_name, timeout = timeout, ) #end get_interface_async #end BusPeer #+ # Interface-dispatch mechanism #- class INTERFACE(enum.Enum) : "what kind of @interface() is this:\n" \ " * CLIENT -- client-side, for sending method calls to server and" \ " receiving signals from server\n" \ " * SERVER -- server-side, for receiving method calls from clients and" \ " sending signals to clients\n" \ " * CLIENT_AND_SERVER -- this side is both client and server." CLIENT = 1 SERVER = 2 CLIENT_AND_SERVER = 3 # CLIENT | SERVER #end INTERFACE def _send_method_return(connection, message, sig, args) : reply = message.new_method_return() reply.append_objects(sig, *args) connection.send(reply) #end _send_method_return def _message_interface_dispatch(connection, message, w_bus) : # installed as message filter on a connection to handle dispatch # to registered @interface() classes. bus = w_bus() assert bus != None, "parent Connection has gone" def dispatch_signal(level, path) : # Note I ignore handled/not handled status and pass the signal # to all registered handlers. if len(path) != 0 and path[0] in level.children : # call lower-level (more-specific) signal handlers first, # not that it’s important dispatch_signal(level.children[path[0]], path[1:]) #end if signal_listeners = level.signal_listeners name = message.member for fallback in (False, True) : # again, call more-specific (fallback = False) handlers first, # not that it’s important rulekey = _signal_key(fallback, message.interface, name) if rulekey in signal_listeners and (len(path) == 0 or fallback) : funcs = signal_listeners[rulekey] for func in funcs : try : call_info = func._signal_info try : args = message.expect_objects(call_info["in_signature"]) except (TypeError, ValueError) : raise ErrorReturn \ ( name = DBUS.ERROR_INVALID_ARGS, message = "message arguments do not match expected signature" ) #end try kwargs = {} for keyword_keyword, value in \ ( ("connection_keyword", lambda : connection), ("message_keyword", lambda : message), ("path_keyword", lambda : message.path), ("bus_keyword", lambda : bus), ) \ : if call_info[keyword_keyword] != None : kwargs[call_info[keyword_keyword]] = value() #end if #end for if call_info["args_keyword"] != None : if call_info["arg_keys"] != None : args = dict \ ( (key, value) for key, value in zip(call_info["arg_keys"], args) ) if "args_constructor" in call_info : args = call_info["args_constructor"](**args) #end if #end if kwargs[call_info["args_keyword"]] = args args = () else : if call_info["arg_keys"] != None : for key, value in zip(call_info["arg_keys"], args) : kwargs[key] = value #end for args = () #end if #end if result = func(*args, **kwargs) if asyncio.iscoroutine(result) : async def await_result(coro) : # just to gobble any ErrorReturn try : await coro except ErrorReturn : pass #end try #end await_result bus.create_task(await_result(result)) elif result != None : raise ValueError \ ( "invalid result from signal handler: %s" % repr(result) ) #end if except ErrorReturn : pass #end try #end for #end if #end for #end dispatch_signal def return_result_common(call_info, result) : # handles list, tuple, dict or Error returned from method handler; # packs into reply message and sends it off. if isinstance(result, dbus.Error) : assert result.is_set, "unset Error object returned from handler" reply = message.new_error(result.name, result.message) connection.send(reply) else : sig = dbus.parse_signature(call_info["out_signature"]) if isinstance(result, dict) and call_info["result_keys"] != None : result = list(result[key] for key in call_info["result_keys"]) # convert result items to list in right order elif ( "result_constructor" in call_info and isinstance(result, call_info["result_constructor"]) ) : result = list(result) elif len(sig) == 0 and result == None : # might as well allow method call to not bother returning an empty result result = [] elif not isinstance(result, (tuple, list)) : raise ValueError("invalid handler result %s" % repr(result)) #end if _send_method_return \ ( connection = connection, message = message, sig = sig, args = result ) #end if #end return_result_common #begin _message_interface_dispatch result = DBUS.HANDLER_RESULT_NOT_YET_HANDLED # to begin with if message.type in (DBUS.MESSAGE_TYPE_METHOD_CALL, DBUS.MESSAGE_TYPE_SIGNAL) : is_method = message.type == DBUS.MESSAGE_TYPE_METHOD_CALL if not is_method and bus._client_dispatch != None : dispatch_signal(bus._client_dispatch, dbus.split_path(message.path)) #end if iface = bus.get_dispatch_interface(message.path, message.interface) if iface != None : method_name = message.member methods = (iface._interface_signals, iface._interface_methods)[is_method] if ( iface._interface_kind != (INTERFACE.SERVER, INTERFACE.CLIENT)[is_method] and method_name in methods ) : method = methods[method_name] call_info = getattr(method, ("_signal_info", "_method_info")[is_method]) if is_method or not call_info["stub"] : to_return_result = None try : try : args = message.expect_objects(call_info["in_signature"]) except (TypeError, ValueError) : raise ErrorReturn \ ( name = DBUS.ERROR_INVALID_ARGS, message = "message arguments do not match expected signature" ) #end try kwargs = {} for keyword_keyword, value in \ ( ("connection_keyword", lambda : connection), ("message_keyword", lambda : message), ("path_keyword", lambda : message.path), ("bus_keyword", lambda : bus), ) \ : if call_info[keyword_keyword] != None : kwargs[call_info[keyword_keyword]] = value() #end if #end for if call_info["args_keyword"] != None : if call_info["arg_keys"] != None : args = dict \ ( (key, value) for key, value in zip(call_info["arg_keys"], args) ) if "args_constructor" in call_info : args = call_info["args_constructor"](**args) #end if #end if kwargs[call_info["args_keyword"]] = args args = () else : if call_info["arg_keys"] != None : for key, value in zip(call_info["arg_keys"], args) : kwargs[key] = value #end for args = () #end if #end if if is_method : # additional ways of returning method result if call_info["result_keyword"] != None : # construct a mutable result object that handler will update in place to_return_result = [None] * len(call_info["out_signature"]) if call_info["result_keys"] != None : to_return_result = dict \ ( (key, val) for key, val in zip(call_info["result_keys"], to_return_result) ) if "result_constructor" in call_info : to_return_result = call_info["result_constructor"](**to_return_result) #end if #end if kwargs[call_info["result_keyword"]] = to_return_result elif call_info["set_result_keyword"] != None : # caller wants to return result via callback def set_result(the_result) : "Call this to set the args for the reply message." nonlocal to_return_result to_return_result = the_result #end set_result kwargs[call_info["set_result_keyword"]] = set_result #end if #end if result = method(iface, *args, **kwargs) except ErrorReturn as err : result = err.as_error() #end try if result == None : if to_return_result != None or is_method : # method handler possibly used set_result mechanism return_result_common(call_info, to_return_result) #end if result = DBUS.HANDLER_RESULT_HANDLED elif asyncio.iscoroutine(result) : assert bus.loop != None, "no event loop to attach coroutine to" if is_method : # wait for result async def await_result(coro) : try : result = await coro except ErrorReturn as err : result = err.as_error() #end try if result == None and to_return_result != None : # method handler used set_result mechanism result = to_return_result #end if return_result_common(call_info, result) #end await_result bus.create_task(await_result(result)) else : bus.create_task(result) #end if result = DBUS.HANDLER_RESULT_HANDLED elif isinstance(result, bool) : # slightly tricky: interpret True as handled, False as not handled, # even though DBUS.HANDLER_RESULT_HANDLED is zero and # DBUS.HANDLER_RESULT_NOT_YET_HANDLED is nonzero. result = \ (DBUS.HANDLER_RESULT_NOT_YET_HANDLED, DBUS.HANDLER_RESULT_HANDLED)[result] elif ( result in ( DBUS.HANDLER_RESULT_HANDLED, DBUS.HANDLER_RESULT_NOT_YET_HANDLED, DBUS.HANDLER_RESULT_NEED_MEMORY, ) ) : pass else : return_result_common(call_info, result) result = DBUS.HANDLER_RESULT_HANDLED #end if #end if #end if #end if #end if dispatch_signal = None # remove reference circularity return \ result #end _message_interface_dispatch def def_attr_class(name, attrs) : "defines a class with read/write attributes with names from the sequence attrs." \ " Objects of this class can be coerced to lists or tuples, and attributes can" \ " also be accessed by index, like a list." class result : __slots__ = tuple(attrs) def __init__(self, **kwargs) : for name in type(self).__slots__ : setattr(self, name, None) #end for for name in kwargs : setattr(self, name, kwargs[name]) #end for #end __init__ def __repr__(self) : return \ ( "%s(%s)" % ( type(self).__name__, ", ".join ( "%s = %s" % (name, repr(getattr(self, name))) for name in type(self).__slots__ ), ) ) #end __repr__ def __len__(self) : return \ len(type(self).__slots__) #end __len__ def __getitem__(self, i) : return \ getattr(self, type(self).__slots__[i]) #end __getitem__ def __setitem__(self, i, val) : setattr(self, type(self).__slots__[i], val) #end __setitem__ #end class #begin def_attr_class result.__name__ = name return \ result #end def_attr_class def interface \ ( kind, *, name, property_change_notification = Introspection.PROP_CHANGE_NOTIFICATION.NEW_VALUE, deprecated = False ) : "class decorator creator for defining interface classes. “kind” is an" \ " INTERFACE.xxx value indicating whether this is for use on the client side" \ " (send methods, receive signals), server side (receive methods, send signals)" \ " or both. “name” (required) is the interface name that will be known to D-Bus." \ " Interface methods and signals should be invocable as\n" \ "\n" \ " method(self, ...)\n" \ "\n" \ " and definitions should be prefixed with calls to the “@method()” or “@signal()”" \ " decorator to identify them. The return result can be a DBUS.HANDLER_RESULT_xxx" \ " code, or None (equivalent to DBUS.HANDLER_RESULT_HANDLED), or a coroutine" \ " to queue for execution after indicating that the message has been handled. Note" \ " that if you declare the method with “async def”, then the return result seen" \ " will be such a coroutine." if not isinstance(kind, INTERFACE) : raise TypeError("kind must be an INTERFACE enum value") #end if if not isinstance(name, str) : raise ValueError("name is required") #end if dbus.validate_interface(name) def decorate(celf) : if not isinstance(celf, type) : raise TypeError("only apply decorator to classes.") #end if if not isinstance(property_change_notification, Introspection.PROP_CHANGE_NOTIFICATION) : raise TypeError \ ( "property_change_notification must be an Introspection." "PROP_CHANGE_NOTIFICATION value" ) #end if celf._interface_kind = kind celf._interface_name = name celf._interface_property_change_notification = property_change_notification celf._interface_deprecated = deprecated celf._interface_methods = \ dict \ ( (f._method_info["name"], f) for fname in dir(celf) for f in (getattr(celf, fname),) if hasattr(f, "_method_info") ) celf._interface_signals = \ dict \ ( (f._signal_info["name"], f) for fname in dir(celf) for f in (getattr(celf, fname),) if hasattr(f, "_signal_info") ) props = {} for info_type, meth_type in \ ( ("_propgetter_info", "getter"), # do first so setter can check change_notification ("_propsetter_info", "setter"), ) \ : for fname in dir(celf) : func = getattr(celf, fname) if hasattr(func, info_type) : propinfo = getattr(func, info_type) propname = propinfo["name"] if propname not in props : props[propname] = {"type" : None} #end if propentry = props[propname] if propinfo["type"] != None : if propentry["type"] != None : if propentry["type"] != propinfo["type"] : raise ValueError \ ( "disagreement on type for property “%s” between" " getter and setter: “%s” versus “%s”" % ( propname, dbus.unparse_signature(propentry["type"]), dbus.unparse_signature(propinfo["type"]), ) ) #end if else : propentry["type"] = propinfo["type"] #end if #end if if ( meth_type == "setter" and "getter" in propentry and propentry["change_notification"] == Introspection.PROP_CHANGE_NOTIFICATION.CONST ) : raise ValueError \ ( "mustn’t specify @propsetter() for a" " PROP_CHANGE_NOTIFICATION.CONST property" ) #end if if meth_type == "getter" : if propinfo["change_notification"] != None : propentry["change_notification"] = propinfo["change_notification"] else : propentry["change_notification"] = property_change_notification #end if #end if propentry[meth_type] = func #end if #end for #end for celf._interface_props = props return \ celf #end decorate #begin interface return \ decorate #end interface def is_interface(cłass) : "is cłass defined as an interface class." return \ isinstance(cłass, type) and hasattr(cłass, "_interface_name") #end is_interface def is_interface_instance(obj) : "is obj an instance of an interface class." return \ is_interface(type(obj)) #end is_interface_instance def method \ (*, name = None, in_signature, out_signature, args_keyword = None, arg_keys = None, arg_attrs = None, result_keyword = None, result_keys = None, result_attrs = None, connection_keyword = None, message_keyword = None, path_keyword = None, bus_keyword = None, set_result_keyword = None, reply = True, deprecated = False ) : "Put a call to this function as a decorator for each method of an @interface()" \ " class that is to be registered as a method of the interface." \ " “name” is the name of the method as specified in the D-Bus message; if omitted," \ " it defaults to the name of the function.\n" \ "\n" \ "This is really only useful on the server side. On the client side, omit the" \ " method definition, and even leave out the interface definition and registration" \ " altogether, unless you want to receive signals from the server; instead, use" \ " Connection.get_proxy_object() to send method calls to the server." in_signature = dbus.parse_signature(in_signature) out_signature = dbus.parse_signature(out_signature) for cond, msg in \ ( ( (result_keyword != None or result_keys != None or result_attrs != None) and not reply, "result_keyword, result_keys and result_attrs are" " meaningless if method does not reply", ), (arg_keys != None and arg_attrs != None, "specify arg_keys or arg_attrs, not both"), (arg_attrs != None and args_keyword == None, "need args_keyword with arg_attrs"), ( arg_keys != None and len(arg_keys) != len(in_signature), "number of arg_keys should match number of items in in_signature", ), ( arg_attrs != None and len(arg_attrs) != len(in_signature), "number of arg_attrs should match number of items in in_signature", ), ( set_result_keyword != None and result_keyword != None, "specify set_result_keyword or result_keyword, not both", ), ( result_keys != None and result_attrs != None, "specify result_keys or result_attrs, not both", ), ( result_attrs != None and result_keyword == None, "need result_keyword with result_attrs", ), ( result_keys != None and len(result_keys) != len(out_signature), "number of result_keys should match number of items in out_signature", ), ( result_attrs != None and len(result_attrs) != len(out_signature), "number of result_attrs should match number of items in out_signature", ), ) \ : if cond : raise ValueError(msg) #end if #end for if arg_keys == None : args_keys = arg_attrs #end if if result_keys == None : result_keys = result_attrs #end if def decorate(func) : if not callable(func) : raise TypeError("only apply decorator to callables.") #end if if name != None : func_name = name else : func_name = func.__name__ #end if dbus.validate_member(func_name) func._method_info = \ { "name" : func_name, "in_signature" : in_signature, "out_signature" : out_signature, "args_keyword" : args_keyword, "arg_keys" : arg_keys, "result_keyword" : result_keyword, "result_keys" : result_keys, "connection_keyword" : connection_keyword, "message_keyword" : message_keyword, "path_keyword" : path_keyword, "bus_keyword" : bus_keyword, "set_result_keyword" : set_result_keyword, "reply" : reply, "deprecated" : deprecated, } if arg_attrs != None : func._method_info["args_constructor"] = def_attr_class("%s_args" % func_name, arg_attrs) #end if if result_attrs != None : func._method_info["result_constructor"] = \ def_attr_class("%s_result" % func_name, result_attrs) #end if return \ func #end decorate #begin method return \ decorate #end method def signal \ (*, name = None, in_signature, args_keyword = None, arg_keys = None, arg_attrs = None, stub = False, connection_keyword = None, message_keyword = None, path_keyword = None, bus_keyword = None, deprecated = False # can signals be deprecated? ) : "Put a call to this function as a decorator for each method of an @interface()" \ " class that is to be registered as a signal of the interface." \ " “name” is the name of the signal as specified in the D-Bus message; if omitted," \ " it defaults to the name of the function.\n" \ "\n" \ "On the server side, the actual function need only be a dummy, since it is just" \ " a placeholder for storing the information used by Connection.send_signal()." in_signature = dbus.parse_signature(in_signature) if arg_attrs != None and args_keyword == None : raise ValueError("need args_keyword with arg_attrs") #end if if arg_keys != None and len(arg_keys) != len(in_signature) : raise ValueError("number of arg_keys should match number of items in in_signature") #end if if arg_attrs != None and len(arg_attrs) != len(in_signature) : raise ValueError("number of arg_attrs should match number of items in in_signature") #end if if arg_keys == None : args_keys = arg_attrs #end if def decorate(func) : if not callable(func) : raise TypeError("only apply decorator to callables.") #end if if name != None : func_name = name else : func_name = func.__name__ #end if dbus.validate_member(func_name) func._signal_info = \ { "name" : func_name, "in_signature" : in_signature, "args_keyword" : args_keyword, "arg_keys" : arg_keys, "stub" : stub, "connection_keyword" : connection_keyword, "message_keyword" : message_keyword, "path_keyword" : path_keyword, "bus_keyword" : bus_keyword, "deprecated" : deprecated, } if arg_attrs != None : func._signal_info["args_constructor"] = def_attr_class("%s_args" % func_name, arg_attrs) #end if return \ func #end decorate #begin signal return \ decorate #end signal def def_signal_stub(**kwargs) : "convenience routine for defining a signal stub function. Instead of\n" \ "\n" \ " @signal(«args»)\n" \ " def stubfunc() : pass\n" \ "\n" \ "you can do\n" \ "\n" \ " stubfunc = def_signal_stub(«args»)\n" \ "\n" \ "passing the same «args» as you would to the @signal() decorator. But note" \ " that the name arg is no longer optional." def stub() : "This is just a stub, standing in for a signal definition in a" \ " proxy interface class. It is not meant to be called." # Lack of formal args should also stymie attempts to invoke as a method. raise \ NotImplementedError("attempted call on signal stub") #end stub #begin def_signal_stub if "name" not in kwargs : raise KeyError("name arg is mandatory") #end if stub.__name__ = kwargs["name"] return \ signal(**kwargs)(stub) #end def_signal_stub def propgetter \ (*, name, type, name_keyword = None, connection_keyword = None, message_keyword = None, path_keyword = None, bus_keyword = None, change_notification = None ) : "Put a call to this function as a decorator for a method of an @interface()" \ " class that is to be the getter of the named property." def decorate(func) : if not callable(func) : raise TypeError("only apply decorator to callables.") #end if assert isinstance(name, str), "property name is mandatory" if ( change_notification != None and not isinstance(change_notification, Introspection.PROP_CHANGE_NOTIFICATION) ) : raise TypeError \ ( "change_notification must be None or an Introspection." "PROP_CHANGE_NOTIFICATION value" ) #end if func._propgetter_info = \ { "name" : name, "type" : dbus.parse_single_signature(type), "name_keyword" : name_keyword, "connection_keyword" : connection_keyword, "message_keyword" : message_keyword, "path_keyword" : path_keyword, "bus_keyword" : bus_keyword, "change_notification" : change_notification, } return \ func #end decorate #begin propgetter return \ decorate #end propgetter def propsetter \ (*, name, type, name_keyword = None, type_keyword = None, value_keyword, connection_keyword = None, message_keyword = None, path_keyword = None, bus_keyword = None ) : "Put a call to this function as a decorator for a method of an @interface()" \ " class that is to be the setter of the named property." def decorate(func) : if not callable(func) : raise TypeError("only apply decorator to callables.") #end if assert isinstance(name, str), "property name is mandatory" func._propsetter_info = \ { "name" : name, "type" : dbus.parse_single_signature(type), "name_keyword" : name_keyword, "type_keyword" : type_keyword, "value_keyword" : value_keyword, "connection_keyword" : connection_keyword, "message_keyword" : message_keyword, "path_keyword" : path_keyword, "bus_keyword" : bus_keyword, } return \ func #end decorate #begin propsetter return \ decorate #end propsetter #+ # Introspection #- def introspect(interface) : "returns an Introspection.Interface object that describes the specified" \ " @interface() class." if not is_interface(interface) : raise TypeError("interface must be an @interface()-type class") #end if def add_deprecated(annots, deprecated) : # common routine for generating “deprecated” annotations. if deprecated : annots.append \ ( Introspection.Annotation(name = "org.freedesktop.DBus.Deprecated", value = "true") ) #end if #end add_deprecated #begin introspect methods = [] for name in interface._interface_methods : method = interface._interface_methods[name] annots = [] add_deprecated(annots, method._method_info["deprecated"]) if not method._method_info["reply"] : annots.append \ ( Introspection.Annotation ( name = "org.freedesktop.DBus.Method.NoReply", value = "true" ) ) #end if args = [] for keys_keyword, sig_keyword, direction in \ ( ("arg_keys", "in_signature", Introspection.DIRECTION.IN), ("result_keys", "out_signature", Introspection.DIRECTION.OUT), ) \ : arg_sigs = dbus.parse_signature(method._method_info[sig_keyword]) arg_names = method._method_info[keys_keyword] if arg_names == None : arg_names = [None] * len(arg_sigs) #end if for arg_name, arg_sig in zip(arg_names, arg_sigs) : args.append \ ( Introspection.Interface.Method.Arg ( name = arg_name, type = arg_sig, direction = direction ) ) #end for #end for methods.append \ ( Introspection.Interface.Method ( name = name, args = args, annotations = annots ) ) #end for signals = [] for name in interface._interface_signals : signal = interface._interface_signals[name] annots = [] add_deprecated(annots, signal._signal_info["deprecated"]) args = [] arg_sigs = dbus.parse_signature(signal._signal_info["in_signature"]) arg_names = signal._signal_info["arg_keys"] if arg_names == None : arg_names = [None] * len(arg_sigs) #end if for arg_name, arg_sig in zip(arg_names, arg_sigs) : args.append \ ( Introspection.Interface.Signal.Arg(name = arg_name, type = arg_sig) ) #end for signals.append \ ( Introspection.Interface.Signal ( name = name, args = args, annotations = annots ) ) #end for properties = [] for name in interface._interface_props : prop = interface._interface_props[name] annots = [] if ( "getter" in prop and prop["change_notification"] != interface._interface_property_change_notification ) : annots.append \ ( Introspection.Annotation ( name = "org.freedesktop.DBus.Property.EmitsChangedSignal", value = prop["change_notification"].value ) ) #end if properties.append \ ( Introspection.Interface.Property ( name = name, type = dbus.parse_single_signature(prop["type"]), access = ( None, Introspection.ACCESS.READ, Introspection.ACCESS.WRITE, Introspection.ACCESS.READWRITE, )[ int("getter" in prop) | int("setter" in prop) << 1 ], annotations = annots ) ) #end for annots = [] if ( interface._interface_property_change_notification != Introspection.PROP_CHANGE_NOTIFICATION.NEW_VALUE ) : annots.append \ ( Introspection.Annotation ( name = "org.freedesktop.DBus.Property.EmitsChangedSignal", value = interface._interface_property_change_notification.value ) ) #end if add_deprecated(annots, interface._interface_deprecated) return \ Introspection.Interface \ ( name = interface._interface_name, methods = methods, signals = signals, properties = properties, annotations = annots ) #end introspect def _append_args(message, call_info, args, kwargs) : message_args = [None] * len(call_info.in_signature) if len(args) != 0 : if len(args) > len(message_args) : raise ValueError("too many args") #end if message_args[:len(args)] = args #end if if len(kwargs) != 0 : arg_positions = {} idx = 0 for arg in call_info.args : if ( isinstance(arg, Introspection.Interface.Signal.Arg) or arg.direction == Introspection.DIRECTION.IN ) : arg_positions[arg.name] = idx idx += 1 #end if #end for for arg_name in kwargs : if arg_name not in arg_positions : raise KeyError("no such arg name “%s”" % arg_name) #end if pos = arg_positions[arg_name] if message_args[pos] != None : raise ValueError("duplicate value for arg %d" % pos) #end if message_args[pos] = kwargs[arg_name] #end for #end if missing = set(pos for pos in range(len(message_args)) if message_args[pos] == None) if len(missing) != 0 : raise ValueError \ ( "too few args specified: missing %s" % ", ".join("%d" % pos for pos in sorted(missing)) ) #end if message.append_objects(call_info.in_signature, *message_args) #end _append_args def def_proxy_interface(kind, *, name, introspected, is_async) : "given an Introspection.Interface object, creates a proxy class that can be" \ " instantiated by a client to send method-call messages to a server," \ " or by a server to send signal messages to clients. is_async indicates" \ " whether method calls are done via coroutines as opposed to blocking" \ " the thread. The resulting class can be instantiated by\n" \ "\n" \ " instance = proxy_class(«connection», «dest»)\n" \ "\n" \ " where «connection» is a dbussy.Connection object to use for sending and receiving" \ " the messages, and «dest» is the bus name to which to send the messages. The" \ " resulting instance is a “root proxy”: a proxy for a particular object path" \ " is obtained from this by an indexing call like\n" \ "\n" \ " obj = instance[«object_path»]\n" \ "\n" \ "from which you can make proxy method calls like obj.«method»(«args») and so on." if not isinstance(kind, INTERFACE) : raise TypeError("kind must be an INTERFACE enum value") #end if if not isinstance(introspected, Introspection.Interface) : raise TypeError("introspected must be an Introspection.Interface") #end if class proxy(BusPeer.Object.ProxyInterface) : # class that will be constructed, to be instantiated for a given connection, # destination and path. # class field _iface_name contains interface name. __slots__ = ("_parent", "_conn", "_dest", "_path", "_timeout") def __init__(self, *, parent, connection, dest, path, timeout = DBUS.TIMEOUT_USE_DEFAULT) : if is_async : assert connection.loop != None, "no event loop to attach coroutines to" #end if self._parent = parent self._conn = connection self._dest = dest self._path = path self._timeout = timeout #end __init__ # rest filled in dynamically below. #end proxy def def_method(intr_method) : # constructs a method-call method. if is_async : async def call_method(self, *args, **kwargs) : message = dbus.Message.new_method_call \ ( destination = self._dest, path = dbus.unsplit_path(self._path), iface = self._iface_name, method = intr_method.name ) _append_args(message, intr_method, args, kwargs) if intr_method.expect_reply : reply = await self._conn.send_await_reply(message, self._timeout) result = reply.expect_return_objects(intr_method.out_signature) else : message.no_reply = True self._conn.send(message) result = None #end if return \ result #end call_method else : def call_method(self, *args, **kwargs) : message = dbus.Message.new_method_call \ ( destination = self._dest, path = dbus.unsplit_path(self._path), iface = self._iface_name, method = intr_method.name ) _append_args(message, intr_method, args, kwargs) if intr_method.expect_reply : reply = self._conn.send_with_reply_and_block(message, self._timeout) result = reply.expect_return_objects(intr_method.out_signature) else : message.no_reply = True self._conn.send(message) result = None #end if return \ result #end call_method #end if #begin def_method call_method.__name__ = intr_method.name call_method.__doc__ = \ ( "method, %(args)s, %(result)s" % { "args" : ( lambda : "no args", lambda : "args %s" % dbus.unparse_signature(intr_method.in_signature), )[len(intr_method.in_signature) != 0](), "result" : ( lambda : "no result", lambda : "result %s" % dbus.unparse_signature(intr_method.out_signature), )[len(intr_method.out_signature) != 0](), } ) setattr(proxy, intr_method.name, call_method) #end def_method def def_signal(intr_signal) : # constructs a signal method. These are never async, since messages # are queued and there is no reply. def send_signal(self, *args, **kwargs) : message = dbus.Message.new_signal \ ( path = dbus.unsplit_path(self._path), iface = self._iface_name, name = intr_signal.name ) _append_args(message, intr_signal, args, kwargs) self._conn.send(message) #end send_signal #begin def_signal send_signal.__name__ = intr_signal.name send_signal.__doc__ = \ ( "signal, %(args)s" % { "args" : ( lambda : "no args", lambda : "args %s" % dbus.unparse_signature(intr_signal.in_signature), )[len(intr_signal.in_signature) != 0](), } ) setattr(proxy, signal.name, send_signal) #end def_signal def def_prop(intr_prop) : # defines getter and/or setter methods as appropriate for a property. if is_async : async def get_prop(self) : message = dbus.Message.new_method_call \ ( destination = self._dest, path = dbus.unsplit_path(self._path), iface = DBUS.INTERFACE_PROPERTIES, method = "Get" ) message.append_objects("ss", self._iface_name, intr_prop.name) reply = await self._conn.send_await_reply(message, self._timeout) return \ reply.expect_return_objects("v")[0][1] #end get_prop def set_prop(self, value) : # Unfortunately, Python doesn’t (currently) allow “await” # on the LHS of an assignment. So to avoid holding up the # thread, I put a task on the event loop to watch over # the completion of the send. This means any error is # going to be reported asynchronously. C’est la vie. message = dbus.Message.new_method_call \ ( destination = self._dest, path = dbus.unsplit_path(self._path), iface = DBUS.INTERFACE_PROPERTIES, method = "Set" ) message.append_objects("ssv", self._iface_name, intr_prop.name, (intr_prop.type, value)) set_prop_pending = self._conn.loop.create_future() self._parent._set_prop_pending.append(set_prop_pending) pending = self._conn.send_with_reply(message, self._timeout) async def sendit() : reply = await pending.await_reply() self._parent._set_prop_pending.pop \ ( self._parent._set_prop_pending.index(set_prop_pending) ) if reply.type == DBUS.MESSAGE_TYPE_METHOD_RETURN : set_prop_pending.set_result(reply) elif reply.type == DBUS.MESSAGE_TYPE_ERROR : failed = dbus.DBusError(reply.error_name, reply.expect_objects("s")[0]) if self._parent._set_prop_failed == None : set_prop_pending.set_exception(failed) self._parent._set_prop_failed = set_prop_pending else : # don’t let failures pile up raise failed #end if else : raise ValueError("unexpected reply type %d" % reply.type) #end if #end sendit self._conn.create_task(sendit()) #end set_prop else : def get_prop(self) : message = dbus.Message.new_method_call \ ( destination = self._dest, path = dbus.unsplit_path(self._path), iface = DBUS.INTERFACE_PROPERTIES, method = "Get" ) message.append_objects("ss", self._iface_name, intr_prop.name) reply = self._conn.send_with_reply_and_block(message, self._timeout) return \ reply.expect_return_objects("v")[0][1] #end get_prop def set_prop(self, value) : message = dbus.Message.new_method_call \ ( destination = self._dest, path = dbus.unsplit_path(self._path), iface = DBUS.INTERFACE_PROPERTIES, method = "Set" ) message.append_objects("ssv", self._iface_name, intr_prop.name, (intr_prop.type, value)) reply = self._conn.send_with_reply_and_block(message, self._timeout) if reply.type == DBUS.MESSAGE_TYPE_METHOD_RETURN : pass elif reply.type == DBUS.MESSAGE_TYPE_ERROR : raise dbus.DBusError(reply.error_name, reply.expect_objects("s")[0]) else : raise ValueError("unexpected reply type %d" % reply.type) #end if #end set_prop #end if def get_prop_noaccess(self) : raise dbus.DbusError \ ( name = DBUS.ERROR_ACCESS_DENIED, message = "property “%s” cannot be read" % intro_prop.name ) #end get_prop_noaccess #begin def_prop if intr_prop.access == Introspection.ACCESS.WRITE : get_prop = get_prop_noaccess #end if if intr_prop.access == Introspection.ACCESS.READ : set_prop = None #end if prop = property(fget = get_prop, fset = set_prop) setattr(proxy, intr_prop.name, prop) #end def_prop class proxy_factory(BusPeer.RootProxy) : # class that will be returned. __slots__ = ("connection", "dest", "timeout", "_set_prop_pending", "_set_prop_failed") # class variables: # template -- = proxy class (set up above) # props -- dict of introspected.properties by name def __init__(self, *, connection, dest, timeout = DBUS.TIMEOUT_USE_DEFAULT) : if is_async : assert connection.loop != None, "no event loop to attach coroutines to" #end if self.connection = connection self.dest = dest self.timeout = timeout if is_async : self._set_prop_pending = [] else : self._set_prop_pending = None #end if self._set_prop_failed = None #end __init__ def __getitem__(self, path) : return \ self.template \ ( parent = self, connection = self.connection, dest = self.dest, path = path, timeout = self.timeout ) #end __getitem__ if is_async : async def set_prop_flush(self) : "workaround for the fact that prop-setter has to queue a separate" \ " asynchronous task; caller can await this coroutine to ensure that" \ " all pending set-property calls have completed." if not is_async : raise RuntimeError("not without an event loop") #end if if self._set_prop_failed != None : set_prop_pending = [self._set_prop_failed] self._set_prop_failed = None else : set_prop_pending = self._set_prop_pending #end if if len(set_prop_pending) != 0 : done = (await self.connection.wait(set_prop_pending))[0] failed = list(e for f in done for e in (f.exception(),) if e != None) if len(failed) > 1 : raise RuntimeError \ ( "multiple failures to set properties: %s" % ", ".join(str(f) for f in failed) ) elif len(failed) == 1 : raise failed[0] #end if #end if #end set_prop_flush def set_prop(self, path, propname, newvalue) : "alternative way of asynchronously setting a new property value:" \ " returns a Future that can be explicitly awaited." if propname not in self.props : raise dbus.DBusError \ ( DBUS.ERROR_UNKNOWN_PROPERTY, message = "no such property “%s”" % propname ) #end if propdef = self.props[propname] if propdef.access == Introspection.ACCESS.READ : raise dbus.DBusError \ ( DBUS.ERROR_PROPERTY_READ_ONLY, message = "property “%s” cannot be written" % propdef.name ) #end if message = dbus.Message.new_method_call \ ( destination = self.dest, path = dbus.unsplit_path(path), iface = DBUS.INTERFACE_PROPERTIES, method = "Set" ) message.append_objects("ssv", self.template._iface_name, propname, (propdef.type, newvalue)) set_prop_pending = self.connection.loop.create_future() pending = self.connection.send_with_reply(message, self.timeout) async def sendit() : reply = await pending.await_reply() if reply.type == DBUS.MESSAGE_TYPE_METHOD_RETURN : set_prop_pending.set_result(None) elif reply.type == DBUS.MESSAGE_TYPE_ERROR : set_prop_pending.set_exception \ ( dbus.DBusError(reply.error_name, reply.expect_objects("s")[0]) ) else : raise ValueError("unexpected reply type %d" % reply.type) #end if #end sendit self.connection.create_task(sendit()) return \ set_prop_pending #end set_prop #end if #end proxy_factory #begin def_proxy_interface if name != None : class_name = name else : class_name = introspected.name.replace(".", "_") #end if proxy.__name__ = class_name proxy._iface_name = introspected.name proxy.__doc__ = "for making method calls on the %s interface." % introspected.name if kind != INTERFACE.SERVER : for method in introspected.methods : def_method(method) #end for for prop in introspected.properties : def_prop(prop) #end for #end if if kind != INTERFACE.CLIENT : for signal in introspected.signals : def_signal(signal) #end for #end if proxy_factory.__name__ = "%s_factory" % proxy.__name__ proxy_factory.__doc__ = \ ( "proxy factory for a %(kind)s D-Bus interface named %(iname)s. Instantiate as\n" "\n" " %(cname)s(connection = «connection»[, dest = «dest»[, timeout = «timeout»]])\n" "\n" "where «connection» is the dbussy.Connection instance to use for sending" " messages and receiving replies, and «dest» is the destination" \ " bus name for sending method calls (not needed if only sending signals)." " The resulting «proxy» object can be indexed by object path, as follows:\n" "\n" " «proxy»[«path»]\n" "\n" "to obtain the actual proxy interface object that can be used to do method" " calls or signal calls." % { "cname" : class_name, "iname" : introspected.name, "kind" : { INTERFACE.CLIENT : "client-side", INTERFACE.SERVER : "server-side", INTERFACE.CLIENT_AND_SERVER : "client-and-server-side", }[kind] } ) proxy_factory.template = proxy proxy_factory.props = dict \ ( (prop.name, prop) for prop in introspected.properties ) return \ proxy_factory #end def_proxy_interface async def set_prop_flush(iface) : "iface must be either a BusPeer.RootProxy or BusPeer.Object.ProxyInterface" \ " instance; calls the set_prop_flush() method on the correct root proxy in" \ " either case." if isinstance(iface, BusPeer.RootProxy) : await iface.set_prop_flush() elif isinstance(iface, BusPeer.Object.ProxyInterface) : await iface._parent.set_prop_flush() else : raise TypeError("iface type %s is not a RootProxy or a ProxyInterface" % type(iface).__name__) #end if #end set_prop_flush #+ # Predefined interfaces #- @interface(INTERFACE.CLIENT_AND_SERVER, name = DBUS.INTERFACE_PEER) class PeerStub : "This is registered as a fallback at the root of your object tree to get" \ " automatic introspection of the DBUS.INTERFACE_PEER interface. The" \ " implementation is hard-coded inside libdbus itself, so the methods" \ " here will never be called." @method \ ( name = "Ping", in_signature = "", out_signature = "", ) def ping(self) : raise NotImplementedError("How did you get here?") #end ping @method \ ( name = "GetMachineId", in_signature = "", out_signature = "s", result_keys = ["machine_uuid"], ) def get_machine_id(self) : raise NotImplementedError("How did you get here?") #end get_machine_id #end PeerStub @interface(INTERFACE.CLIENT_AND_SERVER, name = DBUS.INTERFACE_INTROSPECTABLE) class IntrospectionHandler : "Register this as a fallback at the root of your object tree to obtain" \ " automatic introspection of any point in the tree." @method \ ( name = "Introspect", in_signature = "", out_signature = "s", path_keyword = "path", message_keyword = "message", bus_keyword = "bus", ) def introspect(self, message, bus, path) : interfaces = {} children = None # actually redundant level = bus._server_dispatch levels = iter(dbus.split_path(path)) while True : component = next(levels, None) for entry in level.interfaces.values() : if component == None or entry.fallback : interface = type(entry.interface) if interface._interface_kind != INTERFACE.CLIENT : interfaces[interface._interface_name] = interface # replace any higher-level entry for same name #end if #end if #end for if ( component == None # reached bottom of path or component not in level.children # no handlers to be found further down path ) : children = sorted(level.children.keys()) break #end if level = level.children[component] # search another step down the path #end while introspection = Introspection \ ( interfaces = list ( introspect(iface) for iface in sorted(interfaces.values(), key = lambda iface : iface._interface_name) ), nodes = list ( Introspection.Node(name = child) for child in children ) ) _send_method_return(bus.connection, message, "s", [introspection.unparse()]) return \ DBUS.HANDLER_RESULT_HANDLED #end introspect #end IntrospectionHandler @interface(INTERFACE.CLIENT_AND_SERVER, name = DBUS.INTERFACE_PROPERTIES) class PropertyHandler : "Register this as a fallback at the root of your object tree to provide" \ " automatic dispatching to any @propgetter() and @propsetter() methods" \ " defined for registered interfaces appropriate to an object path." @method \ ( name = "Get", in_signature = "ss", out_signature = "v", args_keyword = "args", path_keyword = "path", message_keyword = "message", bus_keyword = "bus" ) def getprop(self, bus, message, path, args) : interface_name, propname = args dispatch = bus.get_dispatch_interface(path, interface_name) props = type(dispatch)._interface_props if propname in props : propentry = props[propname] if "getter" in propentry : getter = getattr(dispatch, propentry["getter"].__name__) kwargs = {} for keyword_keyword, value in \ ( ("name_keyword", lambda : propname), ("connection_keyword", lambda : bus.connection), ("message_keyword", lambda : message), ("path_keyword", lambda : path), ("bus_keyword", lambda : bus), ) \ : if getter._propgetter_info[keyword_keyword] != None : kwargs[getter._propgetter_info[keyword_keyword]] = value() #end if #end for try : propvalue = getter(**kwargs) except ErrorReturn as err : propvalue = err.as_error() #end try if asyncio.iscoroutine(propvalue) : assert bus.loop != None, "no event loop to attach coroutine to" async def await_return_value(task) : try : propvalue = await task except ErrorReturn as err : result = err.as_error() reply = message.new_error(result.name, result.message) bus.connection.send(reply) else : _send_method_return \ ( connection = bus.connection, message = message, sig = [dbus.VariantType()], args = [(propentry["type"], propvalue)] ) #end try #end await_return_value bus.create_task(await_return_value(propvalue)) reply = None elif isinstance(propvalue, dbus.Error) : assert propvalue.is_set, "unset Error object returned from propgetter" reply = message.new_error(propvalue.name, propvalue.nessage) else : _send_method_return \ ( connection = bus.connection, message = message, sig = [dbus.VariantType()], args = [(propentry["type"], propvalue)] ) reply = None #end if else : reply = message.new_error \ ( name = DBUS.ERROR_ACCESS_DENIED, message = "property “%s” cannot be read" % propname ) #end if else : reply = message.new_error \ ( name = DBUS.ERROR_UNKNOWN_PROPERTY, message = "property “%s” cannot be found" % propname ) #end if if reply != None : bus.connection.send(reply) #end if return \ DBUS.HANDLER_RESULT_HANDLED #end getprop @method \ ( name = "Set", in_signature = "ssv", out_signature = "", args_keyword = "args", path_keyword = "path", message_keyword = "message", bus_keyword = "bus" ) def setprop(self, bus, message, path, args) : def notify_changed() : # sends property-changed signal if appropriate. if "getter" in propentry : notify = propentry["change_notification"] if notify == Introspection.PROP_CHANGE_NOTIFICATION.NEW_VALUE : bus.prop_changed(path, interface_name, propname, proptype, propvalue) elif notify == Introspection.PROP_CHANGE_NOTIFICATION.INVALIDATES : bus.prop_changed(path, interface_name, propname, None, None) #end if #end if #end notify_changed #begin setprop interface_name, propname, (proptype, propvalue) = args dispatch = bus.get_dispatch_interface(path, interface_name) props = type(dispatch)._interface_props if propname in props : propentry = props[propname] if "setter" in propentry : setter = getattr(dispatch, propentry["setter"].__name__) try : if propentry["type"] != None and propentry["type"] != dbus.parse_single_signature(proptype) : raise ErrorReturn \ ( name = DBUS.ERROR_INVALID_ARGS, message = "new property type %s does not match expected signature %s" % (proptype, dbus.unparse_signature(propentry["type"])) ) #end if kwargs = {} for keyword_keyword, value in \ ( ("name_keyword", lambda : propname), ("type_keyword", lambda : proptype), ("value_keyword", lambda : propvalue), ("connection_keyword", lambda : bus.connection), ("message_keyword", lambda : message), ("path_keyword", lambda : path), ("bus_keyword", lambda : bus), ) \ : if setter._propsetter_info[keyword_keyword] != None : kwargs[setter._propsetter_info[keyword_keyword]] = value() #end if #end for setresult = setter(**kwargs) except ErrorReturn as err : setresult = err.as_error() #end try if asyncio.iscoroutine(setresult) : assert bus.loop != None, "no event loop to attach coroutine to" async def wait_set_done() : await setresult reply = message.new_method_return() bus.connection.send(reply) notify_changed() #end wait_set_done bus.create_task(wait_set_done()) reply = None # for now elif isinstance(setresult, dbus.Error) : assert setresult.is_set, "unset Error object returned" reply = message.new_error(setresult.name, setresult.message) elif setresult == None : reply = message.new_method_return() notify_changed() else : raise ValueError("invalid propsetter result %s" % repr(setresult)) #end if else : reply = message.new_error \ ( name = DBUS.ERROR_PROPERTY_READ_ONLY, message = "property “%s” cannot be written" % propname ) #end if else : reply = message.new_error \ ( name = DBUS.ERROR_UNKNOWN_PROPERTY, message = "property “%s” cannot be found" % propname ) #end if if reply != None : bus.connection.send(reply) #end if return \ DBUS.HANDLER_RESULT_HANDLED #end setprop @method \ ( name = "GetAll", in_signature = "s", out_signature = "a{sv}", args_keyword = "args", path_keyword = "path", message_keyword = "message", bus_keyword = "bus" ) def get_all_props(self, bus, message, path, args) : properror = None propvalues = {} to_await = [] def return_result() : if properror != None : reply = message.new_error(properror.name, properror.message) bus.connection.send(reply) else : _send_method_return(bus.connection, message, "a{sv}", [propvalues]) #end if #end return_result async def await_propvalues() : nonlocal properror for propname, fute in to_await : try : propvalue = await fute except ErrorReturn as err : properror = err.as_error() break #end try propvalues[propname] = (propvalues[propname][0], propvalue) #end for return_result() #end await_propvalues #begin get_all_props interface_name, = args try : propvalues, to_await = bus._get_all_my_props(message, path, interface_name) except ErrorReturn as err : properror = err.as_error() #end try if len(to_await) != 0 : bus.create_task(await_propvalues()) else : return_result() #end if return \ DBUS.HANDLER_RESULT_HANDLED #end get_all_props @signal(name = "PropertiesChanged", in_signature = "sa{sv}as", stub = True) def properties_changed(self) : "for use with Connection.send_signal." pass #end properties_changed #end PropertyHandler @interface(INTERFACE.CLIENT_AND_SERVER, name = DBUSX.INTERFACE_OBJECT_MANAGER) class ManagedObjectsHandler : "Register this as a fallback at the root of your object tree to provide" \ " handling of the ObjectManager interface." @method \ ( name = "GetManagedObjects", in_signature = "", out_signature = "a{oa{sa{sv}}}", set_result_keyword = "set_result", bus_keyword = "bus", message_keyword = "message", path_keyword = "base_path", ) def get_managed_objects(self, bus, message, base_path, set_result) : result = {} to_await = [] async def await_propvalues() : for path, interface_name, propname, fute in to_await : propvalue = await fute # any ErrorReturn raised will be automatically converted # to error reply by _message_interface_dispatch (above) propvalues = result[path][interface_name] propvalues[propname] = (propvalues[propname][0], propvalue) #end for set_result([result]) #end await_propvalues #begin get_managed_objects "returns supported interfaces and current property values for all" \ " currently-existent managed objects." for path, interfaces in bus._managed_objects.items() : if base_path == "/" and path != "/" or path.startswith(base_path + "/") : obj_entry = {} for interface_name in interfaces : obj_entry[interface_name], await_props = \ bus._get_all_my_props(message, path, interface_name) for propname, propvalue in await_props : to_await.append((path, interface_name, propname, propvalue)) #end for #end for result[path] = obj_entry #end if #end for if len(to_await) != 0 : return_result = await_propvalues() # result will be available when this is done else : set_result([result]) return_result = None #end if return \ return_result #end get_managed_objects @signal(name = "InterfacesAdded", in_signature = "oa{sa{sv}}", stub = True) def interfaces_added(self) : "for use with Connection.send_signal." pass #end interfaces_added @signal(name = "InterfacesRemoved", in_signature = "oas", stub = True) def interfaces_removed(self) : "for use with Connection.send_signal." pass #end interfaces_removed #end ManagedObjectsHandler #+ # Cleanup #- def _atexit() : # disable all __del__ methods at process termination to avoid unpredictable behaviour for cls in Connection, Server : delattr(cls, "__del__") #end for #end _atexit atexit.register(_atexit) del _atexit