bridge (embedded): Update and fix:

Fix embedded bridge and adapt to changes in core code:

- Add `EmbeddedBridgeSettings` so it can be used in settings.
- Add `AIOBridge` and `TwistedBridge` wrappers.
- Fix callbacks, instead of logging them (and breaking the errback chain), they are now
  raised using `BridgeException` like in other bridges.
- Ensure that a singleton `Bridge` is used between backend and frontend.
- Init backend on first call to `bridge_connect`.
- Add Python 3.14 fix (as it is normally done in Launcher which is not run with with
`embedded`, we have to do it there).
- Track generated files.
This commit is contained in:
Goffi 2026-05-21 20:57:37 +02:00
commit a722464238
6 changed files with 1639 additions and 16 deletions

View file

@ -66,9 +66,7 @@ class EmbeddedConstructor(base_constructor.Constructor):
d = self._methods_cbs["{name}"]({args_result})
if callback is not None:
d.addCallback({cb_or_lambda})
if errback is None:
d.addErrback(lambda failure_: log.error(failure_))
else:
if errback is not None:
d.addErrback(errback)
return d
""".format(**completion)

View file

@ -17,4 +17,4 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from libervia.backend.bridge.embedded import bridge
from libervia.backend.bridge.embedded import bridge, TwistedBridge, AIOBridge

View file

@ -21,11 +21,16 @@ from libervia.backend.core.log import getLogger
log = getLogger(__name__)
from libervia.backend.core import exceptions
from libervia.frontends.bridge.bridge_frontend import BridgeException
import asyncio
from twisted.internet import defer
from twisted.internet.error import ReactorAlreadyInstalledError
class _Bridge(object):
class _Bridge:
def __init__(self):
log.debug("Init embedded bridge...")
log.debug("Init embedded bridge…")
self._methods_cbs = {}
self._signals_cbs = {"core": {}, "plugin": {}}
@ -55,9 +60,7 @@ class _Bridge(object):
d = self._methods_cbs[name](*args, **kwargs)
if callback is not None:
d.addCallback(callback if out_sign else lambda __: callback())
if errback is None:
d.addErrback(lambda failure_: log.error(failure_))
else:
if errback is not None:
d.addErrback(errback)
return d
else:
@ -114,12 +117,118 @@ class _Bridge(object):
##METHODS_PART##
# we want the same instance for both core and frontend
bridge = None
# Singleton: shared instance between backend and frontend
_bridge_instance = None
def bridge():
global bridge
if bridge is None:
bridge = _Bridge()
return bridge
"""Return the singleton _Bridge instance, creating it if necessary."""
global _bridge_instance
if _bridge_instance is None:
_bridge_instance = _Bridge()
return _bridge_instance
class TwistedBridge:
"""Embedded bridge wrapper that returns Twisted Deferreds.
Starts ``LiberviaBackend`` on first bridge connection.
"""
def __init__(self):
self._connected = False
async def post_init(self):
pass
def _ensure_reactor(self):
"""Ensure asyncioreactor is installed."""
from twisted.internet import asyncioreactor
# Python 3.14+ workaround: ensure an event loop exists before installing reactor
try:
asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
asyncioreactor.install()
except ReactorAlreadyInstalledError:
from twisted.internet import reactor
if not isinstance(reactor, asyncioreactor.AsyncioSelectorReactor):
raise exceptions.InternalError(
f"Wrong reactor installed: {reactor}"
) from None
def _start_backend(self):
"""Start LiberviaBackend and return its initialised Deferred."""
from libervia.backend.core.main import LiberviaBackend
self._ensure_reactor()
backend = LiberviaBackend()
backend._init()
return backend.initialised
def bridge_connect(self):
"""Connect to the embedded backend, starting it if necessary."""
if self._connected:
return defer.succeed(None)
self._connected = True
return self._start_backend()
def register_signal(self, name, handler, iface="core"):
"""Register a signal handler on the embedded bridge."""
bridge().register_signal(name, handler, iface)
def _errback(self, failure_):
"""Convert Failure to BridgeException."""
e = failure_.value
if isinstance(e, BridgeException):
raise e
try:
condition = e.condition
except AttributeError:
condition = ""
raise BridgeException(
type(e).__module__ + "." + type(e).__name__, str(e), condition
) from e
def __getattr__(self, name):
"""Delegate method calls, ensuring they return Deferreds."""
attr = getattr(bridge(), name)
if callable(attr) and not name.startswith("_"):
def deferred_wrapper(*args, **kwargs):
result = attr(*args, **kwargs)
if isinstance(result, defer.Deferred):
result.addErrback(self._errback)
return result
return defer.succeed(result)
return deferred_wrapper
return attr
class AIOBridge(TwistedBridge):
"""Embedded bridge wrapper that returns asyncio Futures."""
def bridge_connect(self):
"""Connect to the embedded backend, starting it if necessary."""
if self._connected:
return asyncio.ensure_future(defer.succeed(None))
self._connected = True
d = self._start_backend()
return d.asFuture(asyncio.get_event_loop())
def __getattr__(self, name):
"""Delegate method calls, converting Deferreds to Futures."""
attr = super().__getattr__(name)
if callable(attr) and not name.startswith("_"):
def aio_wrapper(*args, **kwargs):
return attr(*args, **kwargs).asFuture(asyncio.get_event_loop())
return aio_wrapper
return attr

File diff suppressed because it is too large Load diff

View file

@ -232,8 +232,18 @@ class AutoBridgeSettings(ConfigOptions):
type: Literal["auto"] = "auto"
class EmbeddedBridgeSettings(ConfigOptions):
"""Embedded Bridge.
Runs frontend and backend in the same process, backend is used like a library.
The backend is started automatically on first bridge connection.
"""
type: Literal["embedded"] = "embedded"
BridgeSettings = Annotated[
DBusBridgeSettings | PbBridgeSettings | AutoBridgeSettings,
DBusBridgeSettings | PbBridgeSettings | AutoBridgeSettings | EmbeddedBridgeSettings,
Field(discriminator="type"),
]

View file

@ -0,0 +1,20 @@
#!/usr/bin/env python3
# Libervia: an XMPP client
# 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from libervia.backend.bridge.embedded import bridge, TwistedBridge, AIOBridge