cli (base): Refactor loop handling:

The loop was chosen according to the bridge used, but this is not possible anymore before
running it, because there is now an "auto" bridge which falls back from D-Bus to PB. To
work around that, the loop has been refactored to be started before knowing which bridge
is used, then the bridge-dependent hooks are installed later.
This commit is contained in:
Goffi 2026-03-10 18:05:14 +01:00
commit 1256e92cec
2 changed files with 135 additions and 102 deletions

View file

@ -47,7 +47,7 @@ from libervia.backend.tools.common import data_format
from libervia.backend.tools.common.ansi import ANSI as A
import libervia.cli
from libervia.cli.constants import Const as C
from libervia.cli.loops import QuitException, get_libervia_cli_loop
from libervia.cli.loops import QuitException, LiberviaCLILoop
from libervia.frontends.bridge.bridge_frontend import BridgeException
from libervia.frontends.tools import aio, misc
from libervia.frontends.tools.jid import JID
@ -828,6 +828,8 @@ class LiberviaCli:
raise exceptions.InternalError(
"Something went wrong while initialising bridge."
) from e
self.loop.install_hooks(self.bridge_settings.type)
# 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()
@ -846,10 +848,10 @@ 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)
self.loop.loop.run_until_complete(self.main(args=args, namespace=namespace))
self.loop.run(self)
except SystemExit as e:
# The loop is stopped, but we execute it one more time to call `a_quit` which
# will call any cleaning method including async ones.

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python3
# Libervia CLI
# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
# 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
@ -19,6 +19,7 @@
import sys
import asyncio
import logging as log
from libervia.backend.core.exceptions import InternalError
from libervia.backend.core.i18n import _
from libervia.cli.constants import Const as C
from libervia.frontends.tools import aio
@ -35,112 +36,142 @@ class QuitException(BaseException):
"""
def get_libervia_cli_loop(bridge_name):
if "dbus" in bridge_name:
import signal
class LiberviaCLILoop:
"""Libervia CLI event loop.
class LiberviaCLILoop:
This class must be created before bridge initialization, and "install_hooks" must be
called after.
"""
def __init__(self):
self.loop = asyncio.get_event_loop()
def __init__(self):
"""Initialize the loop.
def run(self, libervia_cli, args, namespace):
aio.install_glib_asyncio_iteration()
signal.signal(signal.SIGINT, self._on_sigint)
loop = self.loop
loop.run_until_complete(libervia_cli.main(args=args, namespace=namespace))
loop.run_forever()
def quit(self, exit_code):
loop = asyncio.get_event_loop()
loop.stop()
sys.exit(exit_code)
def call_later(self, delay, callback, *args):
"""call a callback repeatedly
@param delay(int): delay between calls in s
@param callback(callable): method to call
if the callback return True, the call will continue
else the calls will stop
@param *args: args of the callbac
"""
loop = asyncio.get_event_loop()
loop.call_later(delay, callback, *args)
def _on_sigint(self, sig_number, stack_frame):
"""Called on keyboard interruption
Print user interruption message, set exit code and stop reactor
"""
print("\r" + USER_INTER_MSG)
self.quit(C.EXIT_USER_CANCELLED)
else:
import signal
The actual event loop backend (dbus/GLib or Twisted) is set up
later via install_hooks() after bridge initialization.
"""
# we always install asyncioreactor because it must be done before any asyncio
# loop is used. If we end up using dbus, this shouldn't cause trouble.
from twisted.internet import asyncioreactor
asyncioreactor.install()
from twisted.internet import reactor, defer
from twisted.internet import reactor
class LiberviaCLILoop:
self.loop = asyncio.get_event_loop()
self.reactor = reactor
self._hooks_installed = False
def __init__(self):
# exit code must be set when using quit, so if it's not set
# something got wrong and we must report it
self._exit_code = C.EXIT_INTERNAL_ERROR
# exit code must be set when using quit, so if it's not set something got wrong
# and we must report it
self._exit_code = C.EXIT_INTERNAL_ERROR
self._is_dbus = False
def run(self, libervia_cli, *args):
self.libervia_cli = libervia_cli
signal.signal(signal.SIGINT, self._on_sigint)
defer.ensureDeferred(self._start(libervia_cli, *args))
try:
reactor.run(installSignalHandlers=False)
except SystemExit as e:
self._exit_code = e.code
sys.exit(self._exit_code)
def install_hooks(self, bridge_name):
"""Install signal handlers and event loop hooks.
async def _start(self, libervia_cli, *args):
fut = asyncio.ensure_future(libervia_cli.main(*args))
try:
await defer.Deferred.fromFuture(fut)
except BaseException:
import traceback
This must be called after bridge initialization and before run().
traceback.print_exc()
libervia_cli.quit(1)
@param bridge_name: Name of the bridge.
"""
if self._hooks_installed:
return
self._hooks_installed = True
def quit(self, exit_code):
self._exit_code = exit_code
reactor.stop()
import signal
def _timeout_cb(self, args, callback, delay):
try:
ret = callback(*args)
# FIXME: temporary hack to avoid traceback when using XMLUI
# to be removed once create_task is not used anymore in
# xmlui_manager (i.e. once libervia.frontends.tools.xmlui fully supports
# async syntax)
except QuitException:
return
if ret:
reactor.callLater(delay, self._timeout_cb, args, callback, delay)
if "dbus" in bridge_name:
self._is_dbus = True
# Install GLib integration for dbus
aio.install_glib_asyncio_iteration()
elif bridge_name == "embedded":
raise NotImplementedError
else:
self._is_dbus = False
# Twisted reactor has already been installed in __init__
def call_later(self, delay, callback, *args):
reactor.callLater(delay, self._timeout_cb, args, callback, delay)
signal.signal(signal.SIGINT, self._on_sigint)
def _on_sigint(self, sig_number, stack_frame):
"""Called on keyboard interruption
def run(self, libervia_cli):
"""Run the event loop.
Print user interruption message, set exit code and stop reactor
"""
print("\r" + USER_INTER_MSG)
self._exit_code = C.EXIT_USER_CANCELLED
reactor.callFromThread(reactor.stop)
@param libervia_cli: The LiberviaCli instance
"""
if not self._hooks_installed:
raise InternalError(
"LiberviaCLILoop.run() called before install_hooks(). Ensure"
" install_hooks(bridge_name) is called after bridge initialization."
)
if bridge_name == "embedded":
raise NotImplementedError
# from sat.core import sat_main
# sat = sat_main.SAT()
if self._is_dbus:
self._run_dbus()
else:
self._run_twisted(libervia_cli)
return LiberviaCLILoop
def _run_dbus(self):
"""Run the dbus-based loop."""
loop = self.loop
loop.run_forever()
def _run_twisted(self, libervia_cli):
"""Run the Twisted-based loop."""
self.libervia_cli = libervia_cli
try:
self.reactor.run(installSignalHandlers=False)
except SystemExit as e:
self._exit_code = e.code
sys.exit(self._exit_code)
def quit(self, exit_code):
"""Stop the loop and exit.
@param exit_code: Exit code to return
"""
self._exit_code = exit_code
if self._is_dbus:
loop = asyncio.get_event_loop()
loop.stop()
sys.exit(exit_code)
else:
# Only stop the reactor if it's running
if self.reactor.running:
self.reactor.stop()
else:
# Reactor not running (e.g., --help case), just exit
sys.exit(exit_code)
def call_later(self, delay, callback, *args):
"""Call a callback after a delay.
@param delay: Delay in seconds
@param callback: Method to call
@param args: Arguments to pass to callback
"""
if self._is_dbus:
loop = asyncio.get_event_loop()
loop.call_later(delay, callback, *args)
else:
self.reactor.callLater(delay, self._timeout_cb, args, callback, delay)
def _timeout_cb(self, args, callback, delay):
"""Callback wrapper for Twisted-based delayed calls."""
try:
ret = callback(*args)
# FIXME: temporary hack to avoid traceback when using XMLUI
# to be removed once create_task is not used anymore in
# xmlui_manager (i.e. once libervia.frontends.tools.xmlui fully supports
# async syntax)
except QuitException:
return
if ret:
self.reactor.callLater(delay, self._timeout_cb, args, callback, delay)
def _on_sigint(self, sig_number, stack_frame):
"""Called on keyboard interruption
Print user interruption message, set exit code and stop loop.
"""
print("\r" + USER_INTER_MSG)
self._exit_code = C.EXIT_USER_CANCELLED
if self._is_dbus:
self.quit(self._exit_code)
else:
self.reactor.callFromThread(self.reactor.stop)