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:
parent
241fc0165f
commit
6c853262e1
6 changed files with 499 additions and 942 deletions
|
|
@ -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 * " "}'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
1
setup.py
1
setup.py
|
|
@ -54,6 +54,7 @@ install_requires = [
|
|||
'pyyaml',
|
||||
'sqlalchemy >= 1.4',
|
||||
'aiosqlite',
|
||||
'txdbus'
|
||||
]
|
||||
|
||||
extras_require = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue