diff --git a/libervia/backend/bridge/bridge_constructor/constructors/dbus/dbus_frontend_template.py b/libervia/backend/bridge/bridge_constructor/constructors/dbus/dbus_frontend_template.py index bf3477b8..a19dca4f 100644 --- a/libervia/backend/bridge/bridge_constructor/constructors/dbus/dbus_frontend_template.py +++ b/libervia/backend/bridge/bridge_constructor/constructors/dbus/dbus_frontend_template.py @@ -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( diff --git a/libervia/backend/bridge/bridge_constructor/constructors/pb/pb_frontend_template.py b/libervia/backend/bridge/bridge_constructor/constructors/pb/pb_frontend_template.py index a7ce0bea..8ee56bc6 100644 --- a/libervia/backend/bridge/bridge_constructor/constructors/pb/pb_frontend_template.py +++ b/libervia/backend/bridge/bridge_constructor/constructors/pb/pb_frontend_template.py @@ -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( diff --git a/libervia/backend/bridge/dbus_bridge.py b/libervia/backend/bridge/dbus_bridge.py index c352208d..99a54427 100644 --- a/libervia/backend/bridge/dbus_bridge.py +++ b/libervia/backend/bridge/dbus_bridge.py @@ -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, ): diff --git a/libervia/backend/tools/common/dynamic_import.py b/libervia/backend/tools/common/dynamic_import.py index 02a5b612..f203ed42 100644 --- a/libervia/backend/tools/common/dynamic_import.py +++ b/libervia/backend/tools/common/dynamic_import.py @@ -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 diff --git a/libervia/cli/base.py b/libervia/cli/base.py index fb75a97c..07384525 100644 --- a/libervia/cli/base.py +++ b/libervia/cli/base.py @@ -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() diff --git a/libervia/frontends/bridge/dbus_bridge.py b/libervia/frontends/bridge/dbus_bridge.py index 738203a3..849bcca4 100644 --- a/libervia/frontends/bridge/dbus_bridge.py +++ b/libervia/frontends/bridge/dbus_bridge.py @@ -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() diff --git a/libervia/frontends/bridge/pb.py b/libervia/frontends/bridge/pb.py index 32b92000..770cb345 100644 --- a/libervia/frontends/bridge/pb.py +++ b/libervia/frontends/bridge/pb.py @@ -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) diff --git a/libervia/tui/base.py b/libervia/tui/base.py index 52d5b63e..7b3a4daa 100644 --- a/libervia/tui/base.py +++ b/libervia/tui/base.py @@ -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)