diff --git a/libervia/cli/base.py b/libervia/cli/base.py index 07384525..5e2b966e 100644 --- a/libervia/cli/base.py +++ b/libervia/cli/base.py @@ -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. diff --git a/libervia/cli/loops.py b/libervia/cli/loops.py index 6bec2633..ec37c0c0 100644 --- a/libervia/cli/loops.py +++ b/libervia/cli/loops.py @@ -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)