bridge (dbus, pb): fix intialisation issues:

- D-Bus bridge check if it can import GLib, to raise early a BridgeInitError if it's not
  possible
- PB bridge now do lazy import of `reactor` to avoid race condition in Twisted loop
  initialisation.
- regenerated bridges.
This commit is contained in:
Goffi 2026-03-10 16:34:48 +01:00
commit 30619f5689
8 changed files with 145 additions and 40 deletions

View file

@ -26,6 +26,13 @@ from libervia.backend.core.exceptions import BridgeExceptionNoService, BridgeIni
from dbus.mainloop.glib import DBusGMainLoop
from .bridge_frontend import BridgeException
# 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__)
@ -178,6 +185,9 @@ class bridge:
class AIOBridge(bridge):
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(

View file

@ -21,7 +21,7 @@ from logging import getLogger
from functools import partial
from pathlib import Path
from twisted.spread import pb
from twisted.internet import reactor, defer
from twisted.internet import defer
from twisted.internet.error import ConnectionRefusedError, ConnectError
from libervia.backend.core import exceptions
from libervia.backend.tools import config
@ -135,6 +135,11 @@ class bridge(object):
raise failure_
def bridge_connect(self, callback, errback):
# We lazy import "reactor" to avoid race condition during loop initialisation:
# twisted automatically init default loop when we import reactor, and we must be
# sure that the right loop (usually asyncioreactor) is used.
from twisted.internet import reactor
factory = pb.PBClientFactory()
conf = config.parse_main_conf()
bridge_conf = conf.get_bridge_conf(config.PbBridgeSettings)
@ -176,6 +181,14 @@ class AIOBridge(bridge):
def __init__(self):
self.signals_handler = AIOSignalsHandler()
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 BridgeException(

View file

@ -162,6 +162,7 @@ class DBusObject(objects.DBusObject):
Method("sub_waiting_get", arguments="s", returns="a{ss}"),
Method("subscription", arguments="sss", returns=""),
Method("tor_infos_get", arguments="ss", returns="s"),
Method("tor_mode_get", arguments="s", returns="s"),
Method("version_get", arguments="", returns="s"),
Signal("_debug", "sa{ss}s"),
Signal("action_new", "ssis"),
@ -533,6 +534,9 @@ class DBusObject(objects.DBusObject):
def dbus_tor_infos_get(self, extra, profile_key):
return self._callback("tor_infos_get", extra, profile_key)
def dbus_tor_mode_get(self, profile_key):
return self._callback("tor_mode_get", profile_key)
def dbus_version_get(
self,
):

View file

@ -113,12 +113,16 @@ def load_bridge(config: Settings, verbose: bool = True) -> tuple[Any, BridgeSett
return bridge, bridge_settings
async def _load_and_init_bridge(bridge_settings: BridgeSettings):
async def _load_and_init_bridge(
bridge_settings: BridgeSettings, verbose: bool, is_backend: bool
):
"""Load and set requested bridge.
@raise BridgeInitError: Bridge can't be initialised.
@raise BridgeExceptionNoService: Bridge is initialised, but the backend can't be
found.
"""
bridge = _load_bridge(bridge_settings)
bridge = _load_bridge(bridge_settings, verbose)
try:
bridge_post_init = bridge.post_init
except AttributeError:
@ -127,13 +131,22 @@ async def _load_and_init_bridge(bridge_settings: BridgeSettings):
try:
await bridge_post_init()
except Exception as e:
log.warning(f"Could not initialize bridge: {e}")
if verbose:
log.warning(f"Could not initialize bridge: {e}")
raise exceptions.BridgeInitError from e
if not is_backend:
# For frontends, we connect the bridge.
try:
await bridge.bridge_connect()
except (exceptions.BridgeInitError, exceptions.BridgeExceptionNoService) as e:
raise e
except Exception as e:
raise exceptions.BridgeInitError from e
return bridge
async def load_and_init_bridge(
config: Settings, is_backend=False
config: Settings, verbose: bool = True, is_backend=False
) -> tuple[Any, BridgeSettings]:
"""Load and initialise the bridge.
@ -142,7 +155,9 @@ async def load_and_init_bridge(
@param config: Libervia Settings
@param is_backend: Set to True if calling from backend.
@return: initialised bridge and bridge settings
@raise BridgeInitError: Can't initialise this bridge.
@raise BridgeInitError: Bridge can't be initialised.
@raise BridgeExceptionNoService: Bridge is initialised, but the backend can't be
found.
"""
if is_backend:
@ -157,14 +172,15 @@ async def load_and_init_bridge(
try:
bridge_settings = DBusBridgeSettings()
config.bridges = [bridge_settings]
bridge = await _load_and_init_bridge(bridge_settings)
bridge = await _load_and_init_bridge(bridge_settings, verbose, is_backend)
except exceptions.BridgeInitError as e:
log.warning(f"Can't load D-Bus bridge: {e}")
log.info("Falling back to PB bridge.")
if verbose:
log.warning(f"Can't load D-Bus bridge: {e}")
log.info("Falling back to PB bridge.")
bridge_settings = PbBridgeSettings()
config.bridges = [bridge_settings]
if bridge is None:
bridge = await _load_and_init_bridge(bridge_settings)
bridge = await _load_and_init_bridge(bridge_settings, verbose, is_backend)
return bridge, bridge_settings

View file

@ -138,20 +138,10 @@ class LiberviaCli:
@attribute progress_failure(callable): method to call when progress failed
by default display a message
"""
main_config = config.parse_main_conf(ignore_cache=True)
try:
bridge, self.bridge_settings = dynamic_import.load_bridge(
main_config, verbose=False
)
except exceptions.BridgeInitError:
log.exception("Can't import bridge")
sys.exit(1)
self.console = console.Console(theme=C.THEME_DEFAULT)
self.settings = main_config
self.cli_settings = main_config.get_settings(CLISettings)
self.settings = config.parse_main_conf(ignore_cache=True)
self.cli_settings = self.settings.get_settings(CLISettings)
self.set_color_theme()
self.bridge = bridge
self._on_quit_callbacks = []
@ -819,23 +809,25 @@ class LiberviaCli:
async def main(self, args, namespace):
try:
await self.bridge.bridge_connect()
self.bridge, self.bridge_settings = await dynamic_import.load_and_init_bridge(
self.settings, verbose=False
)
except exceptions.BridgeExceptionNoService as e:
self.disp(
_(
"Can't connect to Libervia backend, are you sure that it's "
"launched ?"
),
error=True,
)
self.quit(C.EXIT_BACKEND_NOT_FOUND, raise_exc=False)
except exceptions.BridgeInitError as e:
self.disp(_("Can't init bridge"), error=True)
self.quit(C.EXIT_BRIDGE_ERROR, raise_exc=False)
except Exception as e:
if isinstance(e, exceptions.BridgeExceptionNoService):
print(
_(
"Can't connect to Libervia backend, are you sure that it's "
"launched ?"
)
)
self.quit(C.EXIT_BACKEND_NOT_FOUND, raise_exc=False)
elif isinstance(e, exceptions.BridgeInitError):
print(_("Can't init bridge"))
self.quit(C.EXIT_BRIDGE_ERROR, raise_exc=False)
else:
print(_("Error while initialising bridge: {e}").format(e=e))
self.quit(C.EXIT_BRIDGE_ERROR, raise_exc=False)
return
raise exceptions.InternalError(
"Something went wrong while initialising bridge."
) from e
# we wait on init_pre_script instead of ready_get, so the CLI frontend can be used
# in init script.
await self.bridge.init_pre_script()

View file

@ -26,6 +26,13 @@ from libervia.backend.core.exceptions import BridgeExceptionNoService, BridgeIni
from dbus.mainloop.glib import DBusGMainLoop
from .bridge_frontend import BridgeException
# 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__)
@ -1455,6 +1462,22 @@ class bridge:
)
)
def tor_mode_get(self, profile_key, callback=None, errback=None):
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))
return str(
self.db_core_iface.tor_mode_get(
profile_key,
timeout=const_TIMEOUT,
reply_handler=callback,
error_handler=error_handler,
)
)
def version_get(self, callback=None, errback=None):
if callback is None:
error_handler = None
@ -1472,6 +1495,9 @@ class bridge:
class AIOBridge(bridge):
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(
@ -2750,6 +2776,21 @@ class AIOBridge(bridge):
)
return fut
def tor_mode_get(self, profile_key):
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_core_iface.tor_mode_get(
profile_key,
timeout=const_TIMEOUT,
reply_handler=reply_handler,
error_handler=error_handler,
)
return fut
def version_get(self):
loop = asyncio.get_running_loop()
fut = loop.create_future()

View file

@ -21,7 +21,7 @@ from logging import getLogger
from functools import partial
from pathlib import Path
from twisted.spread import pb
from twisted.internet import reactor, defer
from twisted.internet import defer
from twisted.internet.error import ConnectionRefusedError, ConnectError
from libervia.backend.core import exceptions
from libervia.backend.tools import config
@ -135,6 +135,11 @@ class bridge(object):
raise failure_
def bridge_connect(self, callback, errback):
# We lazy import "reactor" to avoid race condition during loop initialisation:
# twisted automatically init default loop when we import reactor, and we must be
# sure that the right loop (usually asyncioreactor) is used.
from twisted.internet import reactor
factory = pb.PBClientFactory()
conf = config.parse_main_conf()
bridge_conf = conf.get_bridge_conf(config.PbBridgeSettings)
@ -1019,6 +1024,15 @@ class bridge(object):
else:
d.addErrback(self._errback, ori_errback=errback)
def tor_mode_get(self, profile_key, callback=None, errback=None):
d = self.root.callRemote("tor_mode_get", profile_key)
if callback is not None:
d.addCallback(callback)
if errback is None:
d.addErrback(self._generic_errback)
else:
d.addErrback(self._errback, ori_errback=errback)
def version_get(self, callback=None, errback=None):
d = self.root.callRemote("version_get")
if callback is not None:
@ -1043,6 +1057,14 @@ class AIOBridge(bridge):
def __init__(self):
self.signals_handler = AIOSignalsHandler()
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 BridgeException(
@ -1503,6 +1525,11 @@ class AIOBridge(bridge):
d.addErrback(self._errback)
return d.asFuture(asyncio.get_event_loop())
def tor_mode_get(self, profile_key):
d = self.root.callRemote("tor_mode_get", profile_key)
d.addErrback(self._errback)
return d.asFuture(asyncio.get_event_loop())
def version_get(self):
d = self.root.callRemote("version_get")
d.addErrback(self._errback)

View file

@ -338,7 +338,9 @@ class LiberviaTUIApp(QuickApp, InputHistory):
AVATARS_HANDLER = False
def __init__(self):
bridge_module = dynamic_import.import_bridge(bridge_name, "libervia.frontends.bridge")
bridge_module = dynamic_import.import_bridge(
bridge_name, "libervia.frontends.bridge"
)
if bridge_module is None:
log.error("Can't import {} bridge".format(bridge_name))
sys.exit(3)