diff --git a/libervia/backend/bridge/bridge_constructor/constructors/dbus/dbus_core_template.py b/libervia/backend/bridge/bridge_constructor/constructors/dbus/dbus_core_template.py index 7cda7442..aceacffa 100644 --- a/libervia/backend/bridge/bridge_constructor/constructors/dbus/dbus_core_template.py +++ b/libervia/backend/bridge/bridge_constructor/constructors/dbus/dbus_core_template.py @@ -30,7 +30,7 @@ log = getLogger(__name__) # Interface prefix conf = config.parse_main_conf() -bridge_conf = conf.common.get_bridge_conf(config.DBusBridgeSettings) +bridge_conf = conf.get_bridge_conf(config.DBusBridgeSettings) const_INT_PREFIX = bridge_conf.int_prefix const_ERROR_PREFIX = const_INT_PREFIX + ".error" const_OBJ_PATH = "/org/libervia/Libervia/bridge" 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 fc73a4c7..bf3477b8 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 @@ -32,7 +32,7 @@ log = getLogger(__name__) # Interface prefix conf = config.parse_main_conf() -bridge_conf = conf.common.get_bridge_conf(config.DBusBridgeSettings) +bridge_conf = conf.get_bridge_conf(config.DBusBridgeSettings) const_INT_PREFIX = bridge_conf.int_prefix const_ERROR_PREFIX = const_INT_PREFIX + ".error" const_OBJ_PATH = "/org/libervia/Libervia/bridge" diff --git a/libervia/backend/bridge/bridge_constructor/constructors/pb/pb_core_template.py b/libervia/backend/bridge/bridge_constructor/constructors/pb/pb_core_template.py index 1d4e0923..88db419f 100644 --- a/libervia/backend/bridge/bridge_constructor/constructors/pb/pb_core_template.py +++ b/libervia/backend/bridge/bridge_constructor/constructors/pb/pb_core_template.py @@ -108,7 +108,7 @@ class bridge(object): log.info("Init Perspective Broker...") self.root = PBRoot() conf = config.parse_main_conf() - bridge_conf = conf.common.get_bridge_conf(config.PbBridgePbSettings) + bridge_conf = conf.get_bridge_conf(config.PbBridgeSettings) conn_type = bridge_conf.connection_type if conn_type == "unix_socket": socket_path = bridge_conf.socket_dir / "bridge_pb" 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 2ffa331e..a7ce0bea 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 @@ -137,7 +137,7 @@ class bridge(object): def bridge_connect(self, callback, errback): factory = pb.PBClientFactory() conf = config.parse_main_conf() - bridge_conf = conf.common.get_bridge_conf(config.PbBridgePbSettings) + bridge_conf = conf.get_bridge_conf(config.PbBridgeSettings) conn_type = bridge_conf.connection_type if conn_type == "unix_socket": socket_path = bridge_conf.socket_dir / "bridge_pb" diff --git a/libervia/backend/bridge/dbus_bridge.py b/libervia/backend/bridge/dbus_bridge.py index 13bb6ed4..c352208d 100644 --- a/libervia/backend/bridge/dbus_bridge.py +++ b/libervia/backend/bridge/dbus_bridge.py @@ -30,7 +30,7 @@ log = getLogger(__name__) # Interface prefix conf = config.parse_main_conf() -bridge_conf = conf.common.get_bridge_conf(config.DBusBridgeSettings) +bridge_conf = conf.get_bridge_conf(config.DBusBridgeSettings) const_INT_PREFIX = bridge_conf.int_prefix const_ERROR_PREFIX = const_INT_PREFIX + ".error" const_OBJ_PATH = "/org/libervia/Libervia/bridge" diff --git a/libervia/backend/bridge/pb.py b/libervia/backend/bridge/pb.py index 83b520bc..2215f846 100644 --- a/libervia/backend/bridge/pb.py +++ b/libervia/backend/bridge/pb.py @@ -108,7 +108,7 @@ class bridge(object): log.info("Init Perspective Broker...") self.root = PBRoot() conf = config.parse_main_conf() - bridge_conf = conf.common.get_bridge_conf(config.PbBridgePbSettings) + bridge_conf = conf.get_bridge_conf(config.PbBridgeSettings) conn_type = bridge_conf.connection_type if conn_type == "unix_socket": socket_path = bridge_conf.socket_dir / "bridge_pb" diff --git a/libervia/backend/core/main.py b/libervia/backend/core/main.py index 8c2782f6..8bd78caa 100644 --- a/libervia/backend/core/main.py +++ b/libervia/backend/core/main.py @@ -29,6 +29,11 @@ from libervia import backend from libervia.backend.core.i18n import _, D_, language_switch from libervia.backend.core import patches from libervia.backend.models.tor import TorData +from libervia.backend.tools.config import ( + BridgeSettings, + DBusBridgeSettings, + PbBridgeSettings, +) patches.apply() from twisted.application import app, service @@ -78,6 +83,7 @@ class LiberviaBackend(service.Service): self.initialised = defer.Deferred() self.profiles = {} self.plugins = {} + self._bridge = None # map for short name to whole namespace, # extended by plugins with register_namespace self.ns_map = { @@ -85,28 +91,12 @@ class LiberviaBackend(service.Service): } self.memory = memory.Memory(self) - config = self.memory.config # trigger are used to change Libervia behaviour self.trigger = trigger.TriggerManager() - bridges = config.common.bridges - # FIXME: We only handle first bridge for now. - bridge = next(iter(bridges)) - bridge_name: str = bridge.type - bridge_module = dynamic_import.bridge(bridge_name) - if bridge_module is None: - log.error(f"Can't find bridge module of name {bridge_name}") - sys.exit(1) - log.info(f"using {bridge_name} bridge") - try: - self.bridge = bridge_module.bridge() - except exceptions.BridgeInitError: - log.exception("bridge can't be initialised, can't start Libervia Backend") - sys.exit(1) - # FIXME: Seems not used anymore, maybe delete? - if config.common.use_local_shared_tmp: + if self.memory.config.common.use_local_shared_tmp: self.local_shared_path = self.get_local_path( None, C.CACHE_DIR, C.LOCAL_SHARED_DIR ) @@ -141,23 +131,25 @@ class LiberviaBackend(service.Service): else: return version + @property + def bridge(self): + assert self._bridge is not None + return self._bridge + @property def bridge_name(self): return os.path.splitext(os.path.basename(self.bridge.__file__))[0] async def _post_init(self): try: - bridge_pi = self.bridge.post_init - except AttributeError: - pass - else: - try: - await bridge_pi() - except Exception: - log.exception("Could not initialize bridge") - # because init is not complete at this stage, we use callLater - reactor.callLater(0, self.stop) - return + self._bridge, __ = await dynamic_import.load_and_init_bridge( + G.config, is_backend=True + ) + except exceptions.BridgeInitError: + log.exception("bridge can't be initialised, can't start Libervia Backend") + # because init is not complete at this stage, we use callLater + reactor.callLater(0, self.stop) + return self.bridge.register_method("init_pre_script", lambda: self.init_pre_script) self.bridge.register_method("ready_get", lambda: self.initialised) diff --git a/libervia/backend/tools/common/dynamic_import.py b/libervia/backend/tools/common/dynamic_import.py index 13687efe..02a5b612 100644 --- a/libervia/backend/tools/common/dynamic_import.py +++ b/libervia/backend/tools/common/dynamic_import.py @@ -19,17 +19,28 @@ """tools dynamic import""" from importlib import import_module +from types import ModuleType +from typing import Any, Literal + +from libervia.backend.core import exceptions from libervia.backend.core.log import getLogger +from libervia.backend.tools.config import ( + BridgeSettings, + DBusBridgeSettings, + PbBridgeSettings, + Settings, +) log = getLogger(__name__) +module_path = "libervia.frontends.bridge" -def bridge(name, module_path="libervia.backend.bridge"): + +def import_bridge(name: str) -> ModuleType | None: """import bridge module - @param module_path(str): path of the module to import - @param name(str): name of the bridge to import (e.g.: dbus) - @return (module, None): imported module or None if nothing is found + @param name: name of the bridge to import (e.g.: dbus) + @return: imported module or None if nothing is found """ try: bridge_module = import_module(module_path + "." + name) @@ -40,3 +51,120 @@ def bridge(name, module_path="libervia.backend.bridge"): log.warning(f"Can't import bridge {name!r}: {e}") bridge_module = None return bridge_module + + +def _load_bridge(bridge_settings: BridgeSettings, verbose: bool = True): + """Load requested bridge. + + @raise BridgeInitError: Bridge can't be initialised. + """ + bridge_name: str = bridge_settings.type + bridge_module = import_bridge(bridge_name) + if bridge_module is None: + log.error(f"Can't find bridge module of name {bridge_name}") + raise exceptions.BridgeInitError + if verbose: + log.info(f"using {bridge_name} bridge") + + # FIXME: temporary class selection, bridges need to be refactored to always use async + # classes (and to not use a class starting with a lower case) + try: + bridge_cls = bridge_module.AIOBridge + except AttributeError: + bridge_cls = bridge_module.bridge + try: + return bridge_cls() + except exceptions.BridgeInitError as e: + raise e + except Exception as e: + log.exception(f"Can't load bridge {bridge_name}") + raise exceptions.BridgeInitError from e + + +def load_bridge(config: Settings, verbose: bool = True) -> tuple[Any, BridgeSettings]: + """Load and initialise the bridge. + + "auto" bridge is handled + + @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. + """ + bridges = config.bridges + # FIXME: We only handle first bridge for now. + bridge_settings = next(iter(bridges)) + bridge = None + if bridge_settings.type == "auto": + try: + bridge_settings = DBusBridgeSettings() + config.bridges = [bridge_settings] + bridge = _load_bridge(bridge_settings, verbose=verbose) + except exceptions.BridgeInitError as e: + 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 = _load_bridge(bridge_settings, verbose=verbose) + + return bridge, bridge_settings + + +async def _load_and_init_bridge(bridge_settings: BridgeSettings): + """Load and set requested bridge. + + @raise BridgeInitError: Bridge can't be initialised. + """ + bridge = _load_bridge(bridge_settings) + try: + bridge_post_init = bridge.post_init + except AttributeError: + pass + else: + try: + await bridge_post_init() + except Exception as e: + log.warning(f"Could not initialize bridge: {e}") + raise exceptions.BridgeInitError from e + return bridge + + +async def load_and_init_bridge( + config: Settings, is_backend=False +) -> tuple[Any, BridgeSettings]: + """Load and initialise the bridge. + + "auto" bridge is handled + + @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. + """ + + if is_backend: + global module_path + module_path = "libervia.backend.bridge" + + bridges = config.bridges + # FIXME: We only handle first bridge for now. + bridge_settings = next(iter(bridges)) + bridge = None + if bridge_settings.type == "auto": + try: + bridge_settings = DBusBridgeSettings() + config.bridges = [bridge_settings] + bridge = await _load_and_init_bridge(bridge_settings) + except exceptions.BridgeInitError as e: + 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) + + return bridge, bridge_settings diff --git a/libervia/backend/tools/config.py b/libervia/backend/tools/config.py index 3677b9f6..75d7c428 100644 --- a/libervia/backend/tools/config.py +++ b/libervia/backend/tools/config.py @@ -158,7 +158,7 @@ class FrontendSettings(DynamicSettingsBase): D = TypeVar("D", bound=DynamicSettingsBase) -class PbBridgePbSettings(ConfigOptions): +class PbBridgeSettings(ConfigOptions): type: Literal["pb"] = "pb" connection_type: Literal["unix_socket", "socket"] = Field( default="unix_socket", description="type of connection to use." @@ -185,8 +185,18 @@ class DBusBridgeSettings(ConfigOptions): ) +class AutoBridgeSettings(ConfigOptions): + """Automatic Bridge. + + Will try D-Bus first, then fall back to Pb if necessary. + """ + + type: Literal["auto"] = "auto" + + BridgeSettings = Annotated[ - DBusBridgeSettings | PbBridgePbSettings, Field(discriminator="type") + DBusBridgeSettings | PbBridgeSettings | AutoBridgeSettings, + Field(discriminator="type"), ] B = TypeVar("B", bound=BridgeSettings) @@ -252,14 +262,6 @@ class CommonSettings(ConfigOptions): default=None, description="Logging level (debug, info, warning, error)" ) log_fmt: str | None = Field(default=None, description="Log message format") - bridges: list[BridgeSettings] = Field( - default_factory=list, - description=( - "Bridge is the name of the IPC used to communicate between backend and" - " frontends. Leave empty to let Libervia determine the best one. If you" - " indicate several bridges, they will run in parallel." - ), - ) pid_dir: DirectoryPathAuto = Field( default=C.TEMP_DIR, description=( @@ -346,12 +348,47 @@ class CommonSettings(ConfigOptions): description="JID of the entity used for group chat searches.", ) + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + extra="allow", + env_prefix="LIBERVIA_", + env_nested_delimiter="__", + toml_file=C.CONFIG_FILES, + ) + + common: CommonSettings = Field(default_factory=CommonSettings) + bridges: list[BridgeSettings] = Field( + default_factory=list, + description=( + "Bridge is the name of the IPC used to communicate between backend and" + " frontends. Leave empty to let Libervia determine the best one. If you" + " indicate several bridges, they will run in parallel." + ), + ) + email: EmailSettings = Field(default_factory=EmailSettings) + tls: TLSSettings = Field(default_factory=TLSSettings) + tor: TorSettings = Field(default_factory=TorSettings) + application: dict[str, dict[str, str]] = Field( + default_factory=dict, + description="Application specific settings, used by app manager.", + ) + plugin: dict = Field(default_factory=dict, description="Plugin specific settings.") + component: dict = Field( + default_factory=dict, description="Component specific settings." + ) + @field_validator("bridges", mode="after") @classmethod def set_bridges_auto(cls, bridges: list[BridgeSettings]) -> list[BridgeSettings]: if not bridges: - # FIXME: We default to DBus for now. - bridges = [DBusBridgeSettings()] + bridges = [AutoBridgeSettings()] + elif any(isinstance(b, AutoBridgeSettings) for b in bridges) and any( + isinstance(b, (DBusBridgeSettings, PbBridgeSettings)) for b in bridges + ): + raise ValueError( + '"auto" bridge is mutally exclusive with "dbus" and "pb" bridges.' + ) return bridges def get_bridge_conf(self, bridge_type: type[B]) -> B: @@ -368,31 +405,9 @@ class CommonSettings(ConfigOptions): # We raise internal error because there should not be any request for conf if the # bridge is not in "bridges" and thus instanciating. raise exceptions.InternalError( - f"No bridge configuration found for type {type!r}." + f"No bridge configuration found for type {bridge_type}." ) - -class Settings(BaseSettings): - model_config = SettingsConfigDict( - extra="allow", - env_prefix="LIBERVIA_", - env_nested_delimiter="__", - toml_file=C.CONFIG_FILES, - ) - - common: CommonSettings = Field(default_factory=CommonSettings) - email: EmailSettings = Field(default_factory=EmailSettings) - tls: TLSSettings = Field(default_factory=TLSSettings) - tor: TorSettings = Field(default_factory=TorSettings) - application: dict[str, dict[str, str]] = Field( - default_factory=dict, - description="Application specific settings, used by app manager.", - ) - plugin: dict = Field(default_factory=dict, description="Plugin specific settings.") - component: dict = Field( - default_factory=dict, description="Component specific settings." - ) - def get_settings(self, dyn_settings_cls: type[D]) -> D: """Get dynamic settings instance. diff --git a/libervia/cli/base.py b/libervia/cli/base.py index 92dcb4c7..fb75a97c 100644 --- a/libervia/cli/base.py +++ b/libervia/cli/base.py @@ -73,13 +73,6 @@ class CLISettings(config.FrontendSettings, key="cli"): ) -## bridge handling -# we get bridge name from conf and initialise the right class accordingly -main_config = config.parse_main_conf(ignore_cache=True) -bridge_name = next(iter(main_config.common.bridges)).type -LiberviaCLILoop = get_libervia_cli_loop(bridge_name) - - try: import progressbar except ImportError: @@ -145,16 +138,21 @@ 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.set_color_theme() - bridge_module = dynamic_import.bridge(bridge_name, "libervia.frontends.bridge") - if bridge_module is None: - log.error("Can't import {} bridge".format(bridge_name)) - sys.exit(1) + self.bridge = bridge - self.bridge = bridge_module.AIOBridge() self._on_quit_callbacks = [] def guess_background(self): @@ -856,6 +854,7 @@ class LiberviaCli: return def _run(self, args=None, namespace=None): + LiberviaCLILoop = get_libervia_cli_loop(self.bridge_settings.type) self.loop = LiberviaCLILoop() try: self.loop.run(self, args, namespace) diff --git a/libervia/frontends/bridge/dbus_bridge.py b/libervia/frontends/bridge/dbus_bridge.py index d7ce6f39..738203a3 100644 --- a/libervia/frontends/bridge/dbus_bridge.py +++ b/libervia/frontends/bridge/dbus_bridge.py @@ -32,7 +32,7 @@ log = getLogger(__name__) # Interface prefix conf = config.parse_main_conf() -bridge_conf = conf.common.get_bridge_conf(config.DBusBridgeSettings) +bridge_conf = conf.get_bridge_conf(config.DBusBridgeSettings) const_INT_PREFIX = bridge_conf.int_prefix const_ERROR_PREFIX = const_INT_PREFIX + ".error" const_OBJ_PATH = "/org/libervia/Libervia/bridge" diff --git a/libervia/frontends/bridge/pb.py b/libervia/frontends/bridge/pb.py index c93684df..32b92000 100644 --- a/libervia/frontends/bridge/pb.py +++ b/libervia/frontends/bridge/pb.py @@ -137,7 +137,7 @@ class bridge(object): def bridge_connect(self, callback, errback): factory = pb.PBClientFactory() conf = config.parse_main_conf() - bridge_conf = conf.common.get_bridge_conf(config.PbBridgePbSettings) + bridge_conf = conf.get_bridge_conf(config.PbBridgeSettings) conn_type = bridge_conf.connection_type if conn_type == "unix_socket": socket_path = bridge_conf.socket_dir / "bridge_pb" diff --git a/libervia/tui/base.py b/libervia/tui/base.py index 018f10c3..52d5b63e 100644 --- a/libervia/tui/base.py +++ b/libervia/tui/base.py @@ -338,7 +338,7 @@ class LiberviaTUIApp(QuickApp, InputHistory): AVATARS_HANDLER = False def __init__(self): - bridge_module = dynamic_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)