bridge: automatic bridge:

Add a new "auto" bridge which tries D-Bus and fall back to PB in case of problem. This is
the defaut reactor, but can also be explicitely used.
This commit is contained in:
Goffi 2026-03-10 15:23:22 +01:00
commit 84321be4cc
13 changed files with 222 additions and 88 deletions

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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)

View file

@ -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

View file

@ -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.

View file

@ -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)

View file

@ -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"

View file

@ -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"

View file

@ -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)