bridge (dbus): use Tx DBus for backend part of D-Bus bridge:

Due to recent SQLAlchemy integration, Libervia is now using AsyncIO loop exclusively as
main loop, thus GLib's one can't be used anymore (event if it could be in a separate
thread).
Furthermore Python-DBus is known to have design flaws mentioned even in the official
documentation.

Tx DBus is now used to replace Python-DBus, but only for the backend for now, as it will
need some work on the frontend before we can get completely rid of it.
This commit is contained in:
Goffi 2021-06-03 15:21:43 +02:00
commit 6c853262e1
6 changed files with 499 additions and 942 deletions

View file

@ -25,20 +25,19 @@ class DbusConstructor(base_constructor.Constructor):
CORE_TEMPLATE = "dbus_core_template.py"
CORE_DEST = "dbus_bridge.py"
CORE_FORMATS = {
"signals": """\
@dbus.service.signal(const_INT_PREFIX+const_{category}_SUFFIX,
signature='{sig_in}')
def {name}(self, {args}):
{body}\n""",
"methods_declarations": """\
Method('{name}', arguments='{sig_in}', returns='{sig_out}'),""",
"methods": """\
@dbus.service.method(const_INT_PREFIX+const_{category}_SUFFIX,
in_signature='{sig_in}', out_signature='{sig_out}',
async_callbacks={async_callbacks})
def {name}(self, {args}{async_comma}{async_args_def}):
{debug}return self._callback("{name}", {args_result}{async_comma}{async_args_call})\n""",
"signal_direct_calls": """\
def dbus_{name}(self, {args}):
{debug}return self._callback("{name}", {args_no_default})\n""",
"signals_declarations": """\
Signal('{name}', '{sig_in}'),""",
"signals": """\
def {name}(self, {args}):
self.dbus_bridge.{name}({args})\n""",
self._obj.emitSignal("{name}", {args})\n""",
}
FRONTEND_TEMPLATE = "dbus_frontend_template.py"
@ -68,17 +67,10 @@ class DbusConstructor(base_constructor.Constructor):
def core_completion_method(self, completion, function, default, arg_doc, async_):
completion.update(
{
"debug": ""
if not self.args.debug
else 'log.debug ("%s")\n%s' % (completion["name"], 8 * " "),
"args_result": self.getArguments(
function["sig_in"], name=arg_doc, unicode_protect=self.args.unicode
),
"async_comma": ", " if async_ and function["sig_in"] else "",
"async_args_def": "callback=None, errback=None" if async_ else "",
"async_args_call": "callback=callback, errback=errback" if async_ else "",
"async_callbacks": "('callback', 'errback')" if async_ else "None",
"category": completion["category"].upper(),
"debug": (
"" if not self.args.debug
else f'log.debug ("{completion["name"]}")\n{8 * " "}'
)
}
)

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# SàT communication bridge
# Libervia communication bridge
# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
@ -16,15 +16,15 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from types import MethodType
from functools import partialmethod
from twisted.internet import defer, reactor
from sat.core.i18n import _
import dbus
import dbus.service
import dbus.mainloop.glib
import inspect
from sat.core.log import getLogger
from sat.tools import config
from twisted.internet.defer import Deferred
from sat.core.exceptions import BridgeInitError
from sat.tools import config
from txdbus import client, objects, error
from txdbus.interface import DBusInterface, Method, Signal
log = getLogger(__name__)
@ -45,251 +45,127 @@ class ParseError(Exception):
pass
class MethodNotRegistered(dbus.DBusException):
_dbus_error_name = const_ERROR_PREFIX + ".MethodNotRegistered"
class DBusException(Exception):
pass
class InternalError(dbus.DBusException):
_dbus_error_name = const_ERROR_PREFIX + ".InternalError"
class MethodNotRegistered(DBusException):
dbusErrorName = const_ERROR_PREFIX + ".MethodNotRegistered"
class AsyncNotDeferred(dbus.DBusException):
_dbus_error_name = const_ERROR_PREFIX + ".AsyncNotDeferred"
class DeferredNotAsync(dbus.DBusException):
_dbus_error_name = const_ERROR_PREFIX + ".DeferredNotAsync"
class GenericException(dbus.DBusException):
class GenericException(DBusException):
def __init__(self, twisted_error):
"""
@param twisted_error (Failure): instance of twisted Failure
@return: DBusException
error message is used to store a repr of message and condition in a tuple,
so it can be evaluated by the frontend bridge.
"""
super(GenericException, self).__init__()
try:
# twisted_error.value is a class
class_ = twisted_error.value().__class__
except TypeError:
# twisted_error.value is an instance
class_ = twisted_error.value.__class__
message = twisted_error.getErrorMessage()
data = twisted_error.getErrorMessage()
try:
self.args = (message, twisted_error.value.condition)
data = (data, twisted_error.value.condition)
except AttributeError:
self.args = (message,)
self._dbus_error_name = ".".join(
[const_ERROR_PREFIX, class_.__module__, class_.__name__]
data = (data,)
else:
data = (str(twisted_error),)
self.dbusErrorName = ".".join(
(const_ERROR_PREFIX, class_.__module__, class_.__name__)
)
super(GenericException, self).__init__(repr(data))
@classmethod
def create_and_raise(cls, exc):
raise cls(exc)
class DbusObject(dbus.service.Object):
def __init__(self, bus, path):
dbus.service.Object.__init__(self, bus, path)
log.debug("Init DbusObject...")
class DBusObject(objects.DBusObject):
core_iface = DBusInterface(
const_INT_PREFIX + const_CORE_SUFFIX,
##METHODS_DECLARATIONS_PART##
##SIGNALS_DECLARATIONS_PART##
)
plugin_iface = DBusInterface(
const_INT_PREFIX + const_PLUGIN_SUFFIX
)
dbusInterfaces = [core_iface, plugin_iface]
def __init__(self, path):
super().__init__(path)
log.debug("Init DBusObject...")
self.cb = {}
def register_method(self, name, cb):
self.cb[name] = cb
def _callback(self, name, *args, **kwargs):
"""call the callback if it exists, raise an exception else
if the callback return a deferred, use async methods"""
if not name in self.cb:
"""Call the callback if it exists, raise an exception else"""
try:
cb = self.cb[name]
except KeyError:
raise MethodNotRegistered
if "callback" in kwargs:
# we must have errback too
if not "errback" in kwargs:
log.error("errback is missing in method call [%s]" % name)
raise InternalError
callback = kwargs.pop("callback")
errback = kwargs.pop("errback")
async_ = True
else:
async_ = False
result = self.cb[name](*args, **kwargs)
if async_:
if not isinstance(result, Deferred):
log.error("Asynchronous method [%s] does not return a Deferred." % name)
raise AsyncNotDeferred
result.addCallback(
lambda result: callback() if result is None else callback(result)
)
result.addErrback(lambda err: errback(GenericException(err)))
else:
if isinstance(result, Deferred):
log.error("Synchronous method [%s] return a Deferred." % name)
raise DeferredNotAsync
return result
### signals ###
@dbus.service.signal(const_INT_PREFIX + const_PLUGIN_SUFFIX, signature="")
def dummySignal(self):
# FIXME: workaround for addSignal (doesn't work if one method doensn't
# already exist for plugins), probably missing some initialisation, need
# further investigations
pass
##SIGNALS_PART##
### methods ###
d = defer.maybeDeferred(cb, *args, **kwargs)
d.addErrback(GenericException.create_and_raise)
return d
##METHODS_PART##
def __attributes(self, in_sign):
"""Return arguments to user given a in_sign
@param in_sign: in_sign in the short form (using s,a,i,b etc)
@return: list of arguments that correspond to a in_sign (e.g.: "sss" return "arg1, arg2, arg3")"""
i = 0
idx = 0
attr = []
while i < len(in_sign):
if in_sign[i] not in ["b", "y", "n", "i", "x", "q", "u", "t", "d", "s", "a"]:
raise ParseError("Unmanaged attribute type [%c]" % in_sign[i])
attr.append("arg_%i" % idx)
idx += 1
class Bridge:
if in_sign[i] == "a":
i += 1
if (
in_sign[i] != "{" and in_sign[i] != "("
): # FIXME: must manage tuples out of arrays
i += 1
continue # we have a simple type for the array
opening_car = in_sign[i]
assert opening_car in ["{", "("]
closing_car = "}" if opening_car == "{" else ")"
opening_count = 1
while True: # we have a dict or a list of tuples
i += 1
if i >= len(in_sign):
raise ParseError("missing }")
if in_sign[i] == opening_car:
opening_count += 1
if in_sign[i] == closing_car:
opening_count -= 1
if opening_count == 0:
break
i += 1
return attr
def addMethod(self, name, int_suffix, in_sign, out_sign, method, async_=False):
"""Dynamically add a method to Dbus Bridge"""
inspect_args = inspect.getfullargspec(method)
_arguments = inspect_args.args
_defaults = list(inspect_args.defaults or [])
if inspect.ismethod(method):
# if we have a method, we don't want the first argument (usually 'self')
del (_arguments[0])
# first arguments are for the _callback method
arguments_callback = ", ".join(
[repr(name)]
+ (
(_arguments + ["callback=callback", "errback=errback"])
if async_
else _arguments
)
)
if async_:
_arguments.extend(["callback", "errback"])
_defaults.extend([None, None])
# now we create a second list with default values
for i in range(1, len(_defaults) + 1):
_arguments[-i] = "%s = %s" % (_arguments[-i], repr(_defaults[-i]))
arguments_defaults = ", ".join(_arguments)
code = compile(
"def %(name)s (self,%(arguments_defaults)s): return self._callback(%(arguments_callback)s)"
% {
"name": name,
"arguments_defaults": arguments_defaults,
"arguments_callback": arguments_callback,
},
"<DBus bridge>",
"exec",
)
exec(code) # FIXME: to the same thing in a cleaner way, without compile/exec
method = locals()[name]
async_callbacks = ("callback", "errback") if async_ else None
setattr(
DbusObject,
name,
dbus.service.method(
const_INT_PREFIX + int_suffix,
in_signature=in_sign,
out_signature=out_sign,
async_callbacks=async_callbacks,
)(method),
)
function = getattr(self, name)
func_table = self._dbus_class_table[
self.__class__.__module__ + "." + self.__class__.__name__
][function._dbus_interface]
func_table[function.__name__] = function # Needed for introspection
def addSignal(self, name, int_suffix, signature, doc={}):
"""Dynamically add a signal to Dbus Bridge"""
attributes = ", ".join(self.__attributes(signature))
# TODO: use doc parameter to name attributes
# code = compile ('def '+name+' (self,'+attributes+'): log.debug ("'+name+' signal")', '<DBus bridge>','exec') #XXX: the log.debug is too annoying with xmllog
code = compile(
"def " + name + " (self," + attributes + "): pass", "<DBus bridge>", "exec"
)
exec(code)
signal = locals()[name]
setattr(
DbusObject,
name,
dbus.service.signal(const_INT_PREFIX + int_suffix, signature=signature)(
signal
),
)
function = getattr(self, name)
func_table = self._dbus_class_table[
self.__class__.__module__ + "." + self.__class__.__name__
][function._dbus_interface]
func_table[function.__name__] = function # Needed for introspection
class Bridge(object):
def __init__(self):
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
log.info("Init DBus...")
self._obj = DBusObject(const_OBJ_PATH)
async def postInit(self):
try:
self.session_bus = dbus.SessionBus()
except dbus.DBusException as e:
if e._dbus_error_name == "org.freedesktop.DBus.Error.NotSupported":
conn = await client.connect(reactor)
except error.DBusException as e:
if e.errName == "org.freedesktop.DBus.Error.NotSupported":
log.error(
_(
"D-Bus is not launched, please see README to see instructions on how to launch it"
"D-Bus is not launched, please see README to see instructions on "
"how to launch it"
)
)
raise BridgeInitError
self.dbus_name = dbus.service.BusName(const_INT_PREFIX, self.session_bus)
self.dbus_bridge = DbusObject(self.session_bus, const_OBJ_PATH)
raise BridgeInitError(str(e))
##SIGNAL_DIRECT_CALLS_PART##
conn.exportObject(self._obj)
await conn.requestBusName(const_INT_PREFIX)
##SIGNALS_PART##
def register_method(self, name, callback):
log.debug("registering DBus bridge method [%s]" % name)
self.dbus_bridge.register_method(name, callback)
log.debug(f"registering DBus bridge method [{name}]")
self._obj.register_method(name, callback)
def addMethod(self, name, int_suffix, in_sign, out_sign, method, async_=False, doc={}):
"""Dynamically add a method to Dbus Bridge"""
def emitSignal(self, name, *args):
self._obj.emitSignal(name, *args)
def addMethod(
self, name, int_suffix, in_sign, out_sign, method, async_=False, doc={}
):
"""Dynamically add a method to D-Bus Bridge"""
# FIXME: doc parameter is kept only temporary, the time to remove it from calls
log.debug("Adding method [%s] to DBus bridge" % name)
self.dbus_bridge.addMethod(name, int_suffix, in_sign, out_sign, method, async_)
log.debug(f"Adding method {name!r} to D-Bus bridge")
self._obj.plugin_iface.addMethod(
Method(name, arguments=in_sign, returns=out_sign)
)
# we have to create a method here instead of using partialmethod, because txdbus
# uses __func__ which doesn't work with partialmethod
def caller(self_, *args, **kwargs):
return self_._callback(name, *args, **kwargs)
setattr(self._obj, f"dbus_{name}", MethodType(caller, self._obj))
self.register_method(name, method)
def addSignal(self, name, int_suffix, signature, doc={}):
self.dbus_bridge.addSignal(name, int_suffix, signature, doc)
setattr(Bridge, name, getattr(self.dbus_bridge, name))
"""Dynamically add a signal to D-Bus Bridge"""
log.debug(f"Adding signal {name!r} to D-Bus bridge")
self._obj.plugin_iface.addSignal(Signal(name, signature))
setattr(Bridge, name, partialmethod(Bridge.emitSignal, name))

File diff suppressed because it is too large Load diff

View file

@ -81,7 +81,7 @@ class SAT(service.Service):
self.memory = memory.Memory(self)
# trigger are used to change SàT behaviour
# trigger are used to change Libervia behaviour
self.trigger = (
trigger.TriggerManager()
)
@ -90,14 +90,53 @@ class SAT(service.Service):
bridge_module = dynamic_import.bridge(bridge_name)
if bridge_module is None:
log.error("Can't find bridge module of name {}".format(bridge_name))
log.error(f"Can't find bridge module of name {bridge_name}")
sys.exit(1)
log.info("using {} bridge".format(bridge_name))
log.info(f"using {bridge_name} bridge")
try:
self.bridge = bridge_module.Bridge()
except exceptions.BridgeInitError:
log.error("Bridge can't be initialised, can't start SàT core")
log.error("Bridge can't be initialised, can't start Libervia Backend")
sys.exit(1)
defer.ensureDeferred(self._postInit())
@property
def version(self):
"""Return the short version of Libervia"""
return C.APP_VERSION
@property
def full_version(self):
"""Return the full version of Libervia
In developement mode, release name and extra data are returned too
"""
version = self.version
if version[-1] == "D":
# we are in debug version, we add extra data
try:
return self._version_cache
except AttributeError:
self._version_cache = "{} « {} » ({})".format(
version, C.APP_RELEASE_NAME, utils.getRepositoryData(sat)
)
return self._version_cache
else:
return version
@property
def bridge_name(self):
return os.path.splitext(os.path.basename(self.bridge.__file__))[0]
async def _postInit(self):
try:
bridge_pi = self.bridge.postInit
except AttributeError:
pass
else:
await bridge_pi()
self.bridge.register_method("getReady", lambda: self.initialised)
self.bridge.register_method("getVersion", lambda: self.full_version)
self.bridge.register_method("getFeatures", self.getFeatures)
@ -176,37 +215,8 @@ class SAT(service.Service):
self.bridge.register_method("imageResize", self._imageResize)
self.bridge.register_method("imageGeneratePreview", self._imageGeneratePreview)
self.bridge.register_method("imageConvert", self._imageConvert)
defer.ensureDeferred(self._postInit())
@property
def version(self):
"""Return the short version of SàT"""
return C.APP_VERSION
@property
def full_version(self):
"""Return the full version of SàT
In developement mode, release name and extra data are returned too
"""
version = self.version
if version[-1] == "D":
# we are in debug version, we add extra data
try:
return self._version_cache
except AttributeError:
self._version_cache = "{} « {} » ({})".format(
version, C.APP_RELEASE_NAME, utils.getRepositoryData(sat)
)
return self._version_cache
else:
return version
@property
def bridge_name(self):
return os.path.splitext(os.path.basename(self.bridge.__file__))[0]
async def _postInit(self):
await self.memory.initialise()
self.common_cache = cache.Cache(self, None)
log.info(_("Memory initialised"))

View file

@ -28,15 +28,14 @@ class BridgeException(Exception):
@param message (str): error message
@param condition (str) : error condition
"""
Exception.__init__(self)
super().__init__()
self.fullname = str(name)
self.message = str(message)
self.condition = str(condition) if condition else ""
self.module, __, self.classname = str(self.fullname).rpartition(".")
def __str__(self):
message = (": %s" % self.message) if self.message else ""
return self.classname + message
return self.classname + (f": {self.message}" if self.message else "")
def __eq__(self, other):
return self.classname == other

View file

@ -54,6 +54,7 @@ install_requires = [
'pyyaml',
'sqlalchemy >= 1.4',
'aiosqlite',
'txdbus'
]
extras_require = {