libervia-backend/libervia/cli/loops.py
2026-04-24 17:38:57 +02:00

134 lines
4.2 KiB
Python

#!/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 <http://www.gnu.org/licenses/>.
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)