#!/usr/bin/env python3 # Libervia CLI # 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 . 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 log.basicConfig(level=log.WARNING, format="[%(name)s] %(message)s") USER_INTER_MSG = _("User interruption: good bye") class QuitException(BaseException): """Quitting is requested This is used to stop execution when host.quit() is called """ class LiberviaCLILoop: """Libervia CLI event loop. This class must be created before bridge initialization, and "install_hooks" must be called after. """ def __init__(self): """Initialize the loop. 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 # FIXME: Python 3.14 compatibility fix (see libervia/backend/core/launcher.py for # details). try: asyncio.get_event_loop() except RuntimeError: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) asyncioreactor.install() from twisted.internet import reactor self.loop = asyncio.get_event_loop() self.reactor = reactor self._hooks_installed = False # 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_asyncio = True def install_hooks(self, bridge_name): """Install signal handlers and event loop hooks. This must be called after bridge initialization and before run(). @param bridge_name: Name of the bridge. """ if self._hooks_installed: return self._hooks_installed = True import signal signal.signal(signal.SIGINT, self._on_sigint) def run(self, libervia_cli): """Run the event loop. @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." ) self._run_asyncio() def _run_asyncio(self): """Run the dbus-based loop.""" loop = self.loop loop.run_forever() def quit(self, exit_code): """Stop the loop and exit. @param exit_code: Exit code to return """ self._exit_code = exit_code loop = asyncio.get_event_loop() loop.stop() 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 """ 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 loop. """ print("\r" + USER_INTER_MSG) self._exit_code = C.EXIT_USER_CANCELLED self.quit(self._exit_code)