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.
177 lines
5.7 KiB
Python
177 lines
5.7 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
|
|
from libervia.frontends.tools import aio
|
|
|
|
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
|
|
|
|
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_dbus = False
|
|
|
|
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
|
|
|
|
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__
|
|
|
|
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."
|
|
)
|
|
|
|
if self._is_dbus:
|
|
self._run_dbus()
|
|
else:
|
|
self._run_twisted(libervia_cli)
|
|
|
|
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)
|