# # Client support for DBus proxies # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # from abc import ABCMeta, abstractmethod from threading import Lock from dasbus.client.handler import ClientObjectHandler from dasbus.client.property import PropertyProxy from dasbus.specification import DBusSpecificationError __all__ = [ "AbstractObjectProxy", "ObjectProxy", "InterfaceProxy", "get_object_path", "disconnect_proxy" ] def get_object_handler(proxy): """Get an object handler of the DBus proxy. :param proxy: a DBus proxy :return: a DBus proxy handler """ if not isinstance(proxy, AbstractObjectProxy): raise TypeError("Invalid type '{}'.".format(type(proxy).__name__)) return getattr(proxy, "_handler") def get_object_path(proxy): """Get an object path of the remote DBus object. :param proxy: a DBus proxy :return: a DBus path """ handler = get_object_handler(proxy) return handler.object_path def disconnect_proxy(proxy): """Disconnect the DBus proxy from the remote object. :param proxy: a DBus proxy """ handler = get_object_handler(proxy) handler.disconnect_members() class AbstractObjectProxy(metaclass=ABCMeta): """Abstract proxy of a remote DBus object.""" __slots__ = [ "_handler", "_members", "_lock", "__weakref__" ] # Set of local instance attributes. _locals = {*__slots__} def __init__(self, message_bus, service_name, object_path, handler_factory=ClientObjectHandler, **handler_arguments): """Create a new proxy. :param message_bus: a message bus :param service_name: a DBus name of the service :param object_path: a DBus path the object :param handler_factory: a factory of a DBus client object handler :param handler_arguments: additional arguments for the handler factory """ self._handler = handler_factory( message_bus, service_name, object_path, **handler_arguments ) self._members = dict() self._lock = Lock() @abstractmethod def _get_interface(self, member_name): """Get the DBus interface of the member. :param member_name: a member name :return: an interface name """ pass def _get_member(self, *key): """Find a member of the DBus object. If the member doesn't exist, we will acquire a lock and ask a handler to create it. This method is thread-safe. :param key: a member key :return: a member :raise: AttributeError if invalid """ try: return self._members[key] except KeyError: pass return self._create_member(*key) def _create_member(self, *key): """Create a member of the DBus object. If the member doesn't exist, ask a handler to create it. This method is thread-safe. :param key: a member key :return: a member :raise: DBusSpecificationError if invalid """ with self._lock: try: return self._members[key] except KeyError: pass try: member = self._handler.create_member(*key) except DBusSpecificationError as e: raise AttributeError(str(e)) from None self._members[key] = member return member def __getattr__(self, name): """Get the attribute. Called when an attribute lookup has not found the attribute in the usual places. Always call the DBus handler in this case. """ member = self._get_member(self._get_interface(name), name) if isinstance(member, PropertyProxy): return member.get() return member def __setattr__(self, name, value): """Set the attribute. Called when an attribute assignment is attempted. Call the DBus handler if the name is not a name of an instance attribute defined in _locals. """ if name in self._locals: return super().__setattr__(name, value) member = self._get_member(self._get_interface(name), name) if isinstance(member, PropertyProxy): return member.set(value) raise AttributeError( "Can't set DBus attribute '{}'.".format(name) ) class ObjectProxy(AbstractObjectProxy): """Proxy of a remote DBus object.""" __slots__ = ["_interface_names"] # Set of instance attributes. _locals = {*AbstractObjectProxy._locals, *__slots__} def __init__(self, *args, **kwargs): """Create a new proxy. :param handler: a DBus client object handler """ super().__init__(*args, **kwargs) self._interface_names = None def _get_interface(self, member_name): """Get the DBus interface of the member. The members of standard interfaces have a priority. """ if self._interface_names is None: members = reversed( self._handler.specification.members ) self._interface_names = { m.name: m.interface_name for m in members } try: return self._interface_names[member_name] except KeyError: pass raise AttributeError( "DBus object has no attribute '{}'.".format(member_name) ) class InterfaceProxy(AbstractObjectProxy): """Proxy of a remote DBus interface.""" __slots__ = ["_interface_name"] # Set of instance attributes. _locals = {*AbstractObjectProxy._locals, *__slots__} def __init__(self, message_bus, service_name, object_path, interface_name, *args, **kwargs): """Create a new proxy. :param message_bus: a message bus :param service_name: a DBus name of the service :param object_path: a DBus path the object :param handler: a DBus client object handler """ super().__init__(message_bus, service_name, object_path, *args, **kwargs) self._interface_name = interface_name def _get_interface(self, member_name): """Get the DBus interface of the member.""" return self._interface_name