bridge (dbus): replace D-Bus implementation:

The D-Bus bridge was the oldest one and using the legacy `dbus-python` implementation. In
addition to be dependent on GLib, this implementation has numerous issues and it's even
recommanded on its own documentation to use alternative.

On the other hand, txdbus is Twisted native D-Bus implementation and can be used natively
with asyncio reactor.

Regenerated bridges.
This commit is contained in:
Goffi 2026-03-12 11:59:15 +01:00
commit baab23abe4
4 changed files with 1184 additions and 2595 deletions

View file

@ -306,8 +306,10 @@ class Constructor(object):
extend_method(completion, function, default, arg_doc, async_)
for part, fmt in FORMATS.items():
if part.startswith(function["type"]) or part.startswith(
f"async_{function['type']}"
if (
part.startswith(function["type"])
or part.startswith(f"async_{function['type']}")
or part.startswith(f"twisted_{function['type']}")
):
parts[part.upper()].append(fmt.format(**completion))

View file

@ -41,16 +41,14 @@ class DbusConstructor(base_constructor.Constructor):
FRONTEND_DEST = CORE_DEST
FRONTEND_FORMATS = {
"methods": """\
def {name}(self, {args}{async_comma}{async_args}):
{error_handler}{blocking_call}{debug}return {result}\n""",
def {name}(self, {args}):
{debug}return self.call("{name}", {args_result})\n""",
"async_methods": """\
def {name}(self{async_comma}{args}):
loop = asyncio.get_running_loop()
fut = loop.create_future()
reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret)
error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err))
self.db_{category}_iface.{name}({args_result}{async_comma}timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler)
{debug}return fut\n""",
async def {name}(self, {args}):
{debug}return await self.call("{name}", {args_result})\n""",
"twisted_methods": """\
def {name}(self, {args}):
{debug}return self.call("{name}", {args_result})\n""",
}
def core_completion_signal(self, completion, function, default, arg_doc, async_):
@ -75,42 +73,11 @@ class DbusConstructor(base_constructor.Constructor):
def frontend_completion_method(self, completion, function, default, arg_doc, async_):
completion.update(
{
# XXX: we can manage blocking call in the same way as async one: if callback is None the call will be blocking
"debug": (
""
if not self.args.debug
else 'log.debug ("%s")\n%s' % (completion["name"], 8 * " ")
),
"args_result": self.get_arguments(function["sig_in"], name=arg_doc),
"async_args": "callback=None, errback=None",
"async_comma": ", " if function["sig_in"] else "",
"error_handler": """if callback is None:
error_handler = None
else:
if errback is None:
errback = log.error
error_handler = lambda err:errback(dbus_to_bridge_exception(err))
""",
}
)
if async_:
completion["blocking_call"] = ""
completion["async_args_result"] = (
"timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler"
)
else:
# XXX: To have a blocking call, we must have not reply_handler, so we test if callback exists, and add reply_handler only in this case
completion["blocking_call"] = """kwargs={}
if callback is not None:
kwargs['timeout'] = const_TIMEOUT
kwargs['reply_handler'] = callback
kwargs['error_handler'] = error_handler
"""
completion["async_args_result"] = "**kwargs"
result = (
"self.db_%(category)s_iface.%(name)s(%(args_result)s%(async_comma)s%(async_args_result)s)"
% completion
)
completion["result"] = (
"str(%s)" if self.args.unicode and function["sig_out"] == "s" else "%s"
) % result

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python3
# SàT communication bridge
# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
# Libervia D-Bus communication bridge
# Copyright (C) 2009-2026 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -17,23 +17,17 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
import dbus
import ast
from libervia.backend.core.i18n import _
from libervia.backend.tools import config
from libervia.backend.core.log import getLogger
from libervia.backend.core.exceptions import BridgeExceptionNoService, BridgeInitError
from dbus.mainloop.glib import DBusGMainLoop
from functools import partial
from twisted.internet import reactor
from txdbus import client
from .bridge_frontend import BridgeException
from libervia.backend.core.exceptions import BridgeExceptionNoService, BridgeInitError
from libervia.backend.core.i18n import _
from libervia.backend.core.log import getLogger
from libervia.backend.tools import config
# We check if GLib is actually available to avoid init error at a later stage.
try:
from gi.repository import GLib # type: ignore
except ImportError as e:
raise BridgeInitError(f"GLib is required for D-Bus bridge: {e}")
DBusGMainLoop(set_as_default=True)
log = getLogger(__name__)
@ -48,207 +42,306 @@ const_PLUGIN_SUFFIX = ".plugin"
const_TIMEOUT = 120
def dbus_to_bridge_exception(dbus_e):
"""Convert a DBusException to a BridgeException.
def dbus_to_bridge_exception(failure):
"""Convert a txdbus Failure to a BridgeException.
@param dbus_e (DBusException)
@param failure (Failure or RemoteError)
@return: BridgeException
"""
full_name = dbus_e.get_dbus_name()
if full_name.startswith(const_ERROR_PREFIX):
name = dbus_e.get_dbus_name()[len(const_ERROR_PREFIX) + 1 :]
from twisted.python.failure import Failure
if isinstance(failure, Failure):
err = failure.value
message = failure.getErrorMessage()
else:
err = failure
message = str(failure)
full_name = getattr(err, "dbusErrorName", None) or getattr(err, "errName", None)
if full_name is None:
full_name = ".".join(
(const_ERROR_PREFIX, err.__class__.__module__, err.__class__.__name__)
)
if full_name and full_name.startswith(const_ERROR_PREFIX):
name = full_name[len(const_ERROR_PREFIX) + 1 :]
else:
name = full_name
# XXX: dbus_e.args doesn't contain the original DBusException args, but we
# receive its serialized form in dbus_e.args[0]. From that we can rebuild
# the original arguments list thanks to ast.literal_eval (secure eval).
message = dbus_e.get_dbus_message() # similar to dbus_e.args[0]
try:
message, condition = ast.literal_eval(message)
except (SyntaxError, ValueError, TypeError):
condition = ""
return BridgeException(name, message, condition)
return BridgeException(name, message, "")
class SignalsHandler:
def __init__(self, db_object, iface_name):
self.db_object = db_object
self._iface_names = {
"core": const_INT_PREFIX + const_CORE_SUFFIX,
"plugin": iface_name,
}
def __getattr__(self, name):
if name.startswith("remote_"):
log.debug("calling an unregistered signal: {name}".format(name=name[7:]))
return lambda *args, **kwargs: None
else:
raise AttributeError(name)
def register_signal(self, name, handler, iface="core"):
log.debug("registering signal {name}".format(name=name))
method_name = "remote_" + name
try:
self.__getattribute__(method_name)
except AttributeError:
pass
else:
from libervia.backend.core import exceptions
raise exceptions.InternalError(
"{name} signal handler has been registered twice".format(name=method_name)
)
def on_signal(*args):
handler(*args)
d = self.db_object.notifyOnSignal(
name, on_signal, interface=self._iface_names[iface]
)
d.addErrback(
lambda failure_: log.error(
f"Failed to register signal {name}: {failure_.getErrorMessage()}"
)
)
setattr(self, method_name, on_signal)
class bridge:
def bridge_connect(self, callback, errback):
def __init__(self):
self.conn = None
self.db_object = None
self.db_core_iface = None
self.db_plugin_iface = None
self.signals_handler = None
def __getattr__(self, name):
return partial(self.call, name)
@staticmethod
def _unwrap_txdbus_result(d, return_sig):
"""Unwrap txdbus result."""
# txdbus seems to wrongly wrap result in a list, this is a workaround.
# FIXME: check txdbus behaviour, and report upstream if it's confirmed to be a
# bug.
if return_sig and return_sig.startswith("("):
d.addCallback(
lambda result: (
result[0] if isinstance(result, list) and len(result) == 1 else result
)
)
return d
def call(self, name: str, *args, **kwargs):
"""Call a remote method.
@param name: name of the bridge method
@param args: method arguments
@param kwargs: keyword arguments (callback, errback, and methods arguments)
"""
# Determine which interface to use by checking where the method is defined
for iface in self.db_object.interfaces:
if name in iface.methods:
interface_name = iface.name
return_sig = iface.methods[name].sigOut
break
else:
# Default to core interface if method not found (will raise AttributeError)
interface_name = const_INT_PREFIX + const_CORE_SUFFIX
return_sig = None
# Call the remote method
d = self.db_object.callRemote(
name,
*args,
interface=interface_name,
timeout=const_TIMEOUT,
)
self._unwrap_txdbus_result(d, return_sig)
return d
async def bridge_connect(self) -> None:
"""Connect to the D-Bus bridge."""
try:
self.sessions_bus = dbus.SessionBus()
self.db_object = self.sessions_bus.get_object(
self.conn = await client.connect(reactor)
self.db_object = await self.conn.getRemoteObject(
const_INT_PREFIX, const_OBJ_PATH
)
self.db_core_iface = dbus.Interface(
self.db_object, dbus_interface=const_INT_PREFIX + const_CORE_SUFFIX
# Find the core and plugin interfaces
for iface in self.db_object.interfaces:
if iface.name == const_INT_PREFIX + const_CORE_SUFFIX:
self.db_core_iface = iface
elif iface.name == const_INT_PREFIX + const_PLUGIN_SUFFIX:
self.db_plugin_iface = iface
if self.db_core_iface is None:
raise BridgeInitError(
f"Core interface {const_INT_PREFIX + const_CORE_SUFFIX} not found"
)
if self.db_plugin_iface is None:
raise BridgeInitError(
f"Plugin interface {const_INT_PREFIX + const_PLUGIN_SUFFIX} not found"
)
# Initialize signals handler
self.signals_handler = SignalsHandler(
self.db_object, const_INT_PREFIX + const_PLUGIN_SUFFIX
)
self.db_plugin_iface = dbus.Interface(
self.db_object, dbus_interface=const_INT_PREFIX + const_PLUGIN_SUFFIX
)
except dbus.exceptions.DBusException as e:
if e._dbus_error_name in (
"org.freedesktop.DBus.Error.ServiceUnknown",
"org.freedesktop.DBus.Error.Spawn.ExecFailed",
except BridgeInitError as e:
raise e
except Exception as e:
msg = str(e)
if (
"org.freedesktop.DBus.Error.ServiceUnknown" in msg
or "org.freedesktop.DBus.Error.Spawn.ExecFailed" in msg
):
errback(BridgeExceptionNoService())
elif e._dbus_error_name == "org.freedesktop.DBus.Error.NotSupported":
raise BridgeExceptionNoService from e
elif "org.freedesktop.DBus.Error.NotSupported" in msg:
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"
)
)
errback(BridgeInitError)
else:
errback(e)
else:
callback()
# props = self.db_core_iface.getProperties()
raise BridgeInitError from e
def register_signal(self, functionName, handler, iface="core"):
if iface == "core":
self.db_core_iface.connect_to_signal(functionName, handler)
elif iface == "plugin":
self.db_plugin_iface.connect_to_signal(functionName, handler)
else:
log.error(_("Unknown interface"))
def __getattribute__(self, name):
"""usual __getattribute__ if the method exists, else try to find a plugin method"""
try:
return object.__getattribute__(self, name)
except AttributeError:
# The attribute is not found, we try the plugin proxy to find the requested method
def get_plugin_method(*args, **kwargs):
# We first check if we have an async call. We detect this in two ways:
# - if we have the 'callback' and 'errback' keyword arguments
# - or if the last two arguments are callable
async_ = False
args = list(args)
if kwargs:
if "callback" in kwargs:
async_ = True
_callback = kwargs.pop("callback")
_errback = kwargs.pop(
"errback", lambda failure: log.error(str(failure))
)
try:
args.append(kwargs.pop("profile"))
except KeyError:
try:
args.append(kwargs.pop("profile_key"))
except KeyError:
pass
# at this point, kwargs should be empty
if kwargs:
log.warning(
"unexpected keyword arguments, they will be ignored: {}".format(
kwargs
)
)
elif len(args) >= 2 and callable(args[-1]) and callable(args[-2]):
async_ = True
_errback = args.pop()
_callback = args.pop()
method = getattr(self.db_plugin_iface, name)
if async_:
kwargs["timeout"] = const_TIMEOUT
kwargs["reply_handler"] = _callback
kwargs["error_handler"] = lambda err: _errback(
dbus_to_bridge_exception(err)
)
try:
return method(*args, **kwargs)
except ValueError as e:
if e.args[0].startswith("Unable to guess signature"):
# XXX: if frontend is started too soon after backend, the
# inspection misses methods (notably plugin dynamically added
# methods). The following hack works around that by redoing the
# cache of introspected methods signatures.
log.debug("using hack to work around inspection issue")
proxy = self.db_plugin_iface.proxy_object
IN_PROGRESS = proxy.INTROSPECT_STATE_INTROSPECT_IN_PROGRESS
proxy._introspect_state = IN_PROGRESS
proxy._Introspect()
return self.db_plugin_iface.get_dbus_method(name)(*args, **kwargs)
raise e
return get_plugin_method
if self.signals_handler is None:
log.error("Bridge not initialized")
return
self.signals_handler.register_signal(functionName, handler, iface)
##METHODS_PART##
class AIOBridge(bridge):
class AIOSignalsHandler(SignalsHandler):
async def post_init(self) -> None:
pass
def register_signal(self, functionName, handler, iface="core"):
loop = asyncio.get_running_loop()
async_handler = lambda *args: asyncio.run_coroutine_threadsafe(
handler(*args), loop
)
return super().register_signal(functionName, async_handler, iface)
def __getattribute__(self, name):
"""usual __getattribute__ if the method exists, else try to find a plugin method"""
try:
return object.__getattribute__(self, name)
except AttributeError:
# The attribute is not found, we try the plugin proxy to find the requested method
def get_plugin_method(*args, **kwargs):
loop = asyncio.get_running_loop()
fut = loop.create_future()
method = getattr(self.db_plugin_iface, name)
reply_handler = lambda ret=None: loop.call_soon_threadsafe(
fut.set_result, ret
)
error_handler = lambda err: loop.call_soon_threadsafe(
fut.set_exception, dbus_to_bridge_exception(err)
)
try:
method(
*args,
**kwargs,
timeout=const_TIMEOUT,
reply_handler=reply_handler,
error_handler=error_handler,
)
except ValueError as e:
if e.args[0].startswith("Unable to guess signature"):
# same hack as for bridge.__getattribute__
log.warning("using hack to work around inspection issue")
proxy = self.db_plugin_iface.proxy_object
IN_PROGRESS = proxy.INTROSPECT_STATE_INTROSPECT_IN_PROGRESS
proxy._introspect_state = IN_PROGRESS
proxy._Introspect()
self.db_plugin_iface.get_dbus_method(name)(
*args,
**kwargs,
timeout=const_TIMEOUT,
reply_handler=reply_handler,
error_handler=error_handler,
)
else:
raise e
return fut
return get_plugin_method
def bridge_connect(self):
loop = asyncio.get_running_loop()
fut = loop.create_future()
super().bridge_connect(
callback=lambda: loop.call_soon_threadsafe(fut.set_result, None),
errback=lambda e: loop.call_soon_threadsafe(fut.set_exception, e),
)
return fut
def register_signal(self, name, handler, iface="core"):
"""Register an async signal handler."""
async_handler = lambda *args: asyncio.ensure_future(handler(*args))
return super().register_signal(name, async_handler, iface)
##ASYNC_METHODS_PART##
class AIOBridge(bridge):
def __init__(self):
super().__init__()
self.signals_handler = None
async def post_init(self):
"""Post-initialization hook.
Actually no post init is needed, but the "__getattr__" return "partial" for
unknown methods, and this causes "dynamic_import.load_and_init_bridge" to fail.
"""
pass
def call(self, name: str, *args, **kwargs):
"""Call a remote method and return an awaitable.
@param name: name of the bridge method
"""
d = super().call(name, *args, **kwargs)
return d.asFuture(asyncio.get_event_loop())
async def bridge_connect(self):
"""Connect to the D-Bus bridge (async version)."""
try:
self.conn = await client.connect(reactor).asFuture(asyncio.get_event_loop())
self.db_object = await self.conn.getRemoteObject(
const_INT_PREFIX, const_OBJ_PATH
).asFuture(asyncio.get_event_loop())
# Find the core and plugin interfaces
for iface in self.db_object.interfaces:
if iface.name == const_INT_PREFIX + const_CORE_SUFFIX:
self.db_core_iface = iface
elif iface.name == const_INT_PREFIX + const_PLUGIN_SUFFIX:
self.db_plugin_iface = iface
if self.db_core_iface is None:
raise BridgeInitError(
f"Core interface {const_INT_PREFIX + const_CORE_SUFFIX} not found"
)
if self.db_plugin_iface is None:
raise BridgeInitError(
f"Plugin interface {const_INT_PREFIX + const_PLUGIN_SUFFIX} not found"
)
# Initialize signals handler
self.signals_handler = AIOSignalsHandler(
self.db_object, const_INT_PREFIX + const_PLUGIN_SUFFIX
)
except BridgeInitError as e:
raise e
except Exception as e:
msg = str(e)
if (
"org.freedesktop.DBus.Error.ServiceUnknown" in msg
or "org.freedesktop.DBus.Error.Spawn.ExecFailed" in msg
):
raise BridgeExceptionNoService from e
elif "org.freedesktop.DBus.Error.NotSupported" in msg:
log.error(
_(
"D-Bus is not launched, please see README to see instructions on"
" how to launch it"
)
)
raise BridgeInitError from e
def register_signal(self, functionName, handler, iface="core"):
if self.signals_handler is None:
log.error("Bridge not initialized")
return
self.signals_handler.register_signal(functionName, handler, iface)
class TwistedBridge(bridge):
async def post_init(self):
"""Post-initialization hook.
Actually no post init is needed, but the "__getattr__" return "partial" for
unknown methods, and this causes "dynamic_import.load_and_init_bridge" to fail.
"""
pass
def _errback(self, failure_):
"""Convert Failure to BridgeException"""
raise dbus_to_bridge_exception(failure_)
def call(self, name, *args, **kwargs):
"""Call a remote method and return a Deferred.
@param name(str): name of the bridge method
@param args(list): arguments
@param kwargs(dict): keyword arguments (profile, etc.)
"""
return super().call(name, *args, **kwargs)
async def bridge_connect(self) -> None:
"""Connect to the D-Bus bridge."""
return await super().bridge_connect()
def register_signal(self, functionName, handler, iface="core"):
super().register_signal(functionName, handler, iface)
##TWISTED_METHODS_PART##

File diff suppressed because it is too large Load diff