Compare commits
2 Commits
0c26025a81
...
613fc514fb
Author | SHA1 | Date | |
---|---|---|---|
|
613fc514fb | ||
|
a71da1ad2a |
659
src/cthulhu/dbus_service.py
Normal file
659
src/cthulhu/dbus_service.py
Normal file
@@ -0,0 +1,659 @@
|
||||
# Cthulhu
|
||||
#
|
||||
# Copyright 2025 Stormux <storm_dragon@stormux.org>
|
||||
# Copyright 2025 Valve Corporation
|
||||
# Author: Joanmarie Diggs <jdiggs@igalia.com>
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
||||
# Boston MA 02110-1301 USA.
|
||||
|
||||
"""Provides a D-Bus interface for remotely controlling Cthulhu."""
|
||||
|
||||
__id__ = "$Id$"
|
||||
__version__ = "$Revision$"
|
||||
__date__ = "$Date$"
|
||||
__copyright__ = "Copyright (c) 2025 Stormux <storm_dragon@stormux.org>"
|
||||
__license__ = "LGPL"
|
||||
|
||||
import enum
|
||||
from typing import Callable
|
||||
|
||||
try:
|
||||
from dasbus.connection import SessionMessageBus
|
||||
from dasbus.error import DBusError
|
||||
from dasbus.loop import EventLoop
|
||||
from dasbus.server.interface import dbus_interface
|
||||
from dasbus.server.publishable import Publishable
|
||||
_dasbus_available = True
|
||||
except ImportError:
|
||||
_dasbus_available = False
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from . import debug
|
||||
from . import cthulhu_platform # pylint: disable=no-name-in-module
|
||||
from . import script_manager
|
||||
from . import cthulhu_state
|
||||
|
||||
# Lazy import to avoid circular dependency
|
||||
def _get_input_event():
|
||||
from . import input_event
|
||||
return input_event
|
||||
|
||||
class HandlerType(enum.Enum):
|
||||
"""Enumeration of handler types for D-Bus methods."""
|
||||
|
||||
COMMAND = enum.auto()
|
||||
GETTER = enum.auto()
|
||||
SETTER = enum.auto()
|
||||
|
||||
def command(func):
|
||||
"""Decorator to mark a method as a D-Bus command using its docstring.
|
||||
|
||||
Usage:
|
||||
@command
|
||||
def toggle_speech(self, script=None, event=None):
|
||||
'''Toggles speech on and off.'''
|
||||
# method implementation
|
||||
"""
|
||||
description = func.__doc__ or f"D-Bus command: {func.__name__}"
|
||||
func.dbus_command_description = description
|
||||
return func
|
||||
|
||||
def getter(func):
|
||||
"""Decorator to mark a method as a D-Bus getter using its docstring.
|
||||
|
||||
Usage:
|
||||
@getter
|
||||
def get_rate(self):
|
||||
'''Returns the current speech rate.'''
|
||||
# method implementation
|
||||
"""
|
||||
description = func.__doc__ or f"D-Bus getter: {func.__name__}"
|
||||
func.dbus_getter_description = description
|
||||
return func
|
||||
|
||||
def setter(func):
|
||||
"""Decorator to mark a method as a D-Bus setter using its docstring.
|
||||
|
||||
Usage:
|
||||
@setter
|
||||
def set_rate(self, value):
|
||||
'''Sets the current speech rate.'''
|
||||
# method implementation
|
||||
"""
|
||||
description = func.__doc__ or f"D-Bus setter: {func.__name__}"
|
||||
func.dbus_setter_description = description
|
||||
return func
|
||||
|
||||
class _HandlerInfo:
|
||||
"""Stores processed information about a function exposed via D-Bus."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
python_function_name: str,
|
||||
description: str,
|
||||
action: Callable[..., bool],
|
||||
handler_type: 'HandlerType' = HandlerType.COMMAND
|
||||
):
|
||||
self.python_function_name: str = python_function_name
|
||||
self.description: str = description
|
||||
self.action: Callable[..., bool] = action
|
||||
self.handler_type: HandlerType = handler_type
|
||||
|
||||
|
||||
if _dasbus_available:
|
||||
@dbus_interface("org.stormux.Cthulhu.Module")
|
||||
class CthulhuModuleDBusInterface(Publishable):
|
||||
"""A D-Bus interface representing a specific Cthulhu module (e.g., a manager)."""
|
||||
|
||||
def __init__(self,
|
||||
module_name: str,
|
||||
handlers_info: list[_HandlerInfo]):
|
||||
super().__init__()
|
||||
self._module_name = module_name
|
||||
self._commands: dict[str, _HandlerInfo] = {}
|
||||
self._getters: dict[str, _HandlerInfo] = {}
|
||||
self._setters: dict[str, _HandlerInfo] = {}
|
||||
|
||||
for info in handlers_info:
|
||||
handler_type = getattr(info, "handler_type", HandlerType.COMMAND)
|
||||
normalized_name = self._normalize_handler_name(info.python_function_name)
|
||||
if handler_type == HandlerType.GETTER:
|
||||
self._getters[normalized_name] = info
|
||||
elif handler_type == HandlerType.SETTER:
|
||||
self._setters[normalized_name] = info
|
||||
else:
|
||||
self._commands[normalized_name] = info
|
||||
|
||||
msg = (
|
||||
f"DBUS SERVICE: CthulhuModuleDBusInterface for {module_name} initialized "
|
||||
f"with {len(self._commands)} commands, {len(self._getters)} getters, "
|
||||
f"{len(self._setters)} setters."
|
||||
)
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
def ExecuteRuntimeGetter(self, getter_name: str) -> GLib.Variant: # pylint: disable=invalid-name
|
||||
"""Executes the named getter returning the value as a GLib.Variant for D-Bus marshalling."""
|
||||
|
||||
handler_info = self._getters.get(getter_name)
|
||||
if not handler_info:
|
||||
msg = f"DBUS SERVICE: Unknown getter '{getter_name}' for '{self._module_name}'."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return GLib.Variant("v", GLib.Variant("s", ""))
|
||||
|
||||
result = handler_info.action()
|
||||
msg = f"DBUS SERVICE: Getter '{getter_name}' returned: {result}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return self._to_variant(result)
|
||||
|
||||
def ExecuteRuntimeSetter(self, setter_name: str, value: GLib.Variant) -> bool: # pylint: disable=invalid-name
|
||||
"""Executes the named setter, returning True if succeeded."""
|
||||
|
||||
handler_info = self._setters.get(setter_name)
|
||||
if handler_info is None:
|
||||
msg = f"DBUS SERVICE: Unknown setter '{setter_name}' for '{self._module_name}'."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return False
|
||||
|
||||
unpacked = value.unpack()
|
||||
result = handler_info.action(unpacked)
|
||||
msg = f"DBUS SERVICE: Setter '{setter_name}' with value '{unpacked}' returned: {result}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return result
|
||||
|
||||
def ListCommands(self) -> list[tuple[str, str]]: # pylint: disable=invalid-name
|
||||
"""Returns a list of (command_name, description) for this module (commands only)."""
|
||||
|
||||
command_list = []
|
||||
for camel_case_name, info in self._commands.items():
|
||||
command_list.append((camel_case_name, info.description))
|
||||
return command_list
|
||||
|
||||
def ListRuntimeGetters(self) -> list[tuple[str, str]]: # pylint: disable=invalid-name
|
||||
"""Returns a list of (getter_name, description) for this module."""
|
||||
|
||||
getter_list = []
|
||||
for camel_case_name, info in self._getters.items():
|
||||
getter_list.append((camel_case_name, info.description))
|
||||
return getter_list
|
||||
|
||||
def ListRuntimeSetters(self) -> list[tuple[str, str]]: # pylint: disable=invalid-name
|
||||
"""Returns a list of (setter_name, description) for this module."""
|
||||
|
||||
setter_list = []
|
||||
for camel_case_name, info in self._setters.items():
|
||||
setter_list.append((camel_case_name, info.description))
|
||||
return setter_list
|
||||
|
||||
def ExecuteCommand(self, command_name: str, notify_user: bool) -> bool: # pylint: disable=invalid-name
|
||||
"""Executes the named command and returns True if the command succeeded."""
|
||||
|
||||
if command_name not in self._commands:
|
||||
msg = f"DBUS SERVICE: Unknown command '{command_name}' for '{self._module_name}'."
|
||||
debug.printMessage(debug.LEVEL_WARNING, msg, True)
|
||||
return False
|
||||
|
||||
handler_info = self._commands[command_name]
|
||||
result = handler_info.action(notify_user)
|
||||
msg = (
|
||||
f"DBUS SERVICE: '{command_name}' in '{self._module_name}' executed. "
|
||||
f"Result: {result}, notify_user: {notify_user}"
|
||||
)
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return result
|
||||
|
||||
def for_publication(self):
|
||||
"""Returns the D-Bus interface XML for publication."""
|
||||
|
||||
return self.__dbus_xml__ # pylint: disable=no-member
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _normalize_handler_name(function_name: str) -> str:
|
||||
"""Normalizes a Python function name for D-Bus exposure (getter/setter/command)."""
|
||||
|
||||
if function_name.startswith("get_") or function_name.startswith("set_"):
|
||||
function_name = function_name[4:]
|
||||
return "".join(word.capitalize() for word in function_name.split("_"))
|
||||
|
||||
@staticmethod
|
||||
def _to_variant(result):
|
||||
"""Converts a Python value to a correctly-typed GLib.Variant for D-Bus marshalling."""
|
||||
if isinstance(result, bool):
|
||||
return GLib.Variant("b", result)
|
||||
elif isinstance(result, int):
|
||||
return GLib.Variant("i", result)
|
||||
elif isinstance(result, float):
|
||||
return GLib.Variant("d", result)
|
||||
elif isinstance(result, str):
|
||||
return GLib.Variant("s", result)
|
||||
elif isinstance(result, dict):
|
||||
return GLib.Variant(
|
||||
"a{sv}", {str(k): GLib.Variant("v", v) for k, v in result.items()})
|
||||
elif isinstance(result, list) or isinstance(result, tuple):
|
||||
if all(isinstance(x, str) for x in result):
|
||||
return GLib.Variant("as", list(result))
|
||||
elif all(isinstance(x, int) for x in result):
|
||||
return GLib.Variant("ax", list(result))
|
||||
elif all(isinstance(x, bool) for x in result):
|
||||
return GLib.Variant("ab", list(result))
|
||||
else:
|
||||
return GLib.Variant("av", [GLib.Variant("v", x) for x in result])
|
||||
elif result is None:
|
||||
return GLib.Variant("v", GLib.Variant("s", ""))
|
||||
else:
|
||||
return GLib.Variant("s", str(result))
|
||||
|
||||
|
||||
@dbus_interface("org.stormux.Cthulhu.Service")
|
||||
class CthulhuDBusServiceInterface(Publishable):
|
||||
"""Internal D-Bus service object that handles D-Bus specifics."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._registered_modules: set[str] = set()
|
||||
msg = "DBUS SERVICE: CthulhuDBusServiceInterface initialized."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
def for_publication(self):
|
||||
"""Returns the D-Bus interface XML for publication."""
|
||||
|
||||
return self.__dbus_xml__ # pylint: disable=no-member
|
||||
|
||||
def add_module_interface(
|
||||
self,
|
||||
module_name: str,
|
||||
handlers_info: list[_HandlerInfo],
|
||||
bus: SessionMessageBus,
|
||||
object_path_base: str
|
||||
) -> None:
|
||||
"""Creates and prepares a D-Bus interface for a Cthulhu module."""
|
||||
|
||||
object_path = f"{object_path_base}/{module_name}"
|
||||
if module_name in self._registered_modules:
|
||||
msg = f"DBUS SERVICE: Interface {module_name} already registered. Replacing."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
try:
|
||||
bus.unpublish_object(object_path)
|
||||
except DBusError as e:
|
||||
msg = f"DBUS SERVICE: Error unpublishing old interface for {module_name}: {e}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._registered_modules.discard(module_name)
|
||||
try:
|
||||
module_iface = CthulhuModuleDBusInterface(module_name, handlers_info)
|
||||
bus.publish_object(object_path, module_iface)
|
||||
self._registered_modules.add(module_name)
|
||||
msg = f"DBUS SERVICE: Successfully published {module_name} at {object_path}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
except DBusError as e:
|
||||
msg = (
|
||||
f"DBUS SERVICE: Failed to create or publish D-Bus interface for "
|
||||
f"module {module_name} at {object_path}: {e}"
|
||||
)
|
||||
debug.printMessage(debug.LEVEL_SEVERE, msg, True)
|
||||
|
||||
def remove_module_interface(
|
||||
self,
|
||||
module_name: str,
|
||||
bus: SessionMessageBus,
|
||||
object_path_base: str
|
||||
) -> bool:
|
||||
"""Removes and unpublishes a D-Bus interface for a Cthulhu module."""
|
||||
|
||||
if module_name not in self._registered_modules:
|
||||
msg = f"DBUS SERVICE: Module {module_name} is not registered."
|
||||
debug.printMessage(debug.LEVEL_WARNING, msg, True)
|
||||
return False
|
||||
|
||||
object_path = f"{object_path_base}/{module_name}"
|
||||
try:
|
||||
bus.unpublish_object(object_path)
|
||||
self._registered_modules.discard(module_name)
|
||||
msg = f"DBUS SERVICE: Successfully removed {module_name} from {object_path}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
except DBusError as e:
|
||||
msg = f"DBUS SERVICE: Error removing interface for {module_name}: {e}"
|
||||
debug.printMessage(debug.LEVEL_WARNING, msg, True)
|
||||
return False
|
||||
|
||||
def ListModules(self) -> list[str]: # pylint: disable=invalid-name
|
||||
"""Returns a list of registered module names."""
|
||||
|
||||
return list(self._registered_modules)
|
||||
|
||||
def ListCommands(self) -> list[tuple[str, str]]: # pylint: disable=invalid-name
|
||||
"""Returns available commands on the main service interface."""
|
||||
|
||||
commands = []
|
||||
for attr_name in dir(self):
|
||||
if not attr_name.startswith('_') and attr_name[0].isupper():
|
||||
attr = getattr(self, attr_name)
|
||||
if callable(attr) and hasattr(attr, '__doc__'):
|
||||
description = (attr.__doc__.strip() if attr.__doc__
|
||||
else f"Service command: {attr_name}")
|
||||
commands.append((attr_name, description))
|
||||
|
||||
return sorted(commands)
|
||||
|
||||
def PresentMessage(self, message: str) -> bool: # pylint: disable=invalid-name
|
||||
"""Presents message to the user."""
|
||||
|
||||
msg = f"DBUS SERVICE: PresentMessage called with: '{message}'"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
manager = script_manager.getManager()
|
||||
script = cthulhu_state.activeScript or script_manager.getManager().getDefaultScript()
|
||||
if script is None:
|
||||
msg = "DBUS SERVICE: No script available"
|
||||
debug.printMessage(debug.LEVEL_WARNING, msg, True)
|
||||
return False
|
||||
|
||||
script.presentMessage(message)
|
||||
return True
|
||||
|
||||
def GetVersion(self) -> str: # pylint: disable=invalid-name
|
||||
"""Returns Cthulhu's version and revision if available."""
|
||||
|
||||
result = cthulhu_platform.version
|
||||
if cthulhu_platform.revision:
|
||||
result += f" (rev {cthulhu_platform.revision})"
|
||||
|
||||
msg = f"DBUS SERVICE: GetVersion called, returning: {result}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return result
|
||||
|
||||
def shutdown_service(self, bus: SessionMessageBus, object_path_base: str) -> None:
|
||||
"""Releases D-Bus resources held by this service and its modules."""
|
||||
|
||||
msg = "DBUS SERVICE: Releasing D-Bus resources for service."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
for module_name in list(self._registered_modules):
|
||||
module_object_path = f"{object_path_base}/{module_name}"
|
||||
msg = (
|
||||
f"DBUS SERVICE: Shutting down and unpublishing module {module_name} "
|
||||
f"from main service."
|
||||
)
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
try:
|
||||
bus.unpublish_object(module_object_path)
|
||||
except DBusError as e:
|
||||
msg = f"DBUS SERVICE: Error unpublishing interface for {module_name}: {e}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._registered_modules.clear()
|
||||
|
||||
else:
|
||||
# Fallback classes when dasbus is not available
|
||||
class CthulhuModuleDBusInterface:
|
||||
"""Fallback class when dasbus is not available."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
class CthulhuDBusServiceInterface:
|
||||
"""Fallback class when dasbus is not available."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
class CthulhuRemoteController:
|
||||
"""Manages Cthulhu's D-Bus service for remote control."""
|
||||
|
||||
SERVICE_NAME = "org.stormux.Cthulhu.Service"
|
||||
OBJECT_PATH = "/org/stormux/Cthulhu/Service"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._dbus_service_interface: CthulhuDBusServiceInterface | None = None
|
||||
self._is_running: bool = False
|
||||
self._bus: SessionMessageBus | None = None
|
||||
self._event_loop: EventLoop | None = None
|
||||
self._pending_registrations: dict[str, object] = {}
|
||||
self._dasbus_available = _dasbus_available
|
||||
|
||||
def start(self) -> bool:
|
||||
"""Starts the D-Bus service."""
|
||||
|
||||
if not self._dasbus_available:
|
||||
msg = "REMOTE CONTROLLER: dasbus library not available, D-Bus service disabled."
|
||||
debug.printMessage(debug.LEVEL_WARNING, msg, True)
|
||||
return False
|
||||
|
||||
if self._is_running:
|
||||
msg = "REMOTE CONTROLLER: Start called but service is already running."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
|
||||
msg = "REMOTE CONTROLLER: Attempting to start D-Bus service."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
try:
|
||||
self._bus = SessionMessageBus()
|
||||
msg = (
|
||||
f"REMOTE CONTROLLER: SessionMessageBus acquired: "
|
||||
f"{self._bus.connection.get_unique_name()}"
|
||||
)
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
except DBusError as e:
|
||||
self._bus = None
|
||||
msg = f"REMOTE CONTROLLER: Failed to acquire D-Bus session bus: {e}"
|
||||
debug.printMessage(debug.LEVEL_SEVERE, msg, True)
|
||||
return False
|
||||
|
||||
self._dbus_service_interface = CthulhuDBusServiceInterface()
|
||||
try:
|
||||
self._bus.publish_object(self.OBJECT_PATH, self._dbus_service_interface)
|
||||
self._bus.register_service(self.SERVICE_NAME)
|
||||
except DBusError as e:
|
||||
msg = f"REMOTE CONTROLLER: Failed to publish service or request name: {e}"
|
||||
debug.printMessage(debug.LEVEL_SEVERE, msg, True)
|
||||
if self._dbus_service_interface and self._bus:
|
||||
try:
|
||||
self._bus.unpublish_object(self.OBJECT_PATH)
|
||||
except DBusError:
|
||||
pass
|
||||
self._dbus_service_interface = None
|
||||
self._bus = None
|
||||
return False
|
||||
|
||||
self._is_running = True
|
||||
msg = (
|
||||
f"REMOTE CONTROLLER: Service started name={self.SERVICE_NAME} "
|
||||
f"path={self.OBJECT_PATH}."
|
||||
)
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._process_pending_registrations()
|
||||
return True
|
||||
|
||||
def _process_pending_registrations(self) -> None:
|
||||
"""Processes any module registrations that were queued before the service was ready."""
|
||||
|
||||
if not self._pending_registrations:
|
||||
return
|
||||
|
||||
msg = (
|
||||
f"REMOTE CONTROLLER: Processing {len(self._pending_registrations)} "
|
||||
f"pending module registrations."
|
||||
)
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
for module_name, module_instance in self._pending_registrations.items():
|
||||
msg = f"REMOTE CONTROLLER: Processing pending registration for {module_name}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._register_decorated_commands_internal(module_name, module_instance)
|
||||
|
||||
self._pending_registrations.clear()
|
||||
|
||||
def register_decorated_module(self, module_name: str, module_instance) -> None:
|
||||
"""Registers a module's decorated D-Bus commands."""
|
||||
|
||||
if not self._dasbus_available:
|
||||
return
|
||||
|
||||
if not self._is_running or not self._dbus_service_interface or not self._bus:
|
||||
msg = (
|
||||
f"REMOTE CONTROLLER: Service not ready; queuing decorated registration "
|
||||
f"for {module_name}."
|
||||
)
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._pending_registrations[module_name] = module_instance
|
||||
return
|
||||
|
||||
self._register_decorated_commands_internal(module_name, module_instance)
|
||||
|
||||
def _register_decorated_commands_internal(self, module_name: str, module_instance) -> None:
|
||||
"""Internal method that registers decorated commands from a module instance."""
|
||||
|
||||
if not self._is_running or not self._dbus_service_interface or not self._bus:
|
||||
msg = (
|
||||
f"REMOTE CONTROLLER: Internal error - _register_decorated_commands_internal "
|
||||
f"called for {module_name} but service is not ready."
|
||||
)
|
||||
debug.printMessage(debug.LEVEL_SEVERE, msg, True)
|
||||
return
|
||||
|
||||
handlers_info = []
|
||||
for attr_name in dir(module_instance):
|
||||
attr = getattr(module_instance, attr_name)
|
||||
# Command
|
||||
if callable(attr) and hasattr(attr, "dbus_command_description"):
|
||||
description = attr.dbus_command_description
|
||||
def _create_wrapper(method=attr):
|
||||
def _wrapper(notify_user):
|
||||
event = _get_input_event().RemoteControllerEvent()
|
||||
script = cthulhu_state.activeScript
|
||||
return method(script=script, event=event, notify_user=notify_user)
|
||||
return _wrapper
|
||||
handler_info = _HandlerInfo(
|
||||
python_function_name=attr_name,
|
||||
description=description,
|
||||
action=_create_wrapper(),
|
||||
handler_type=HandlerType.COMMAND
|
||||
)
|
||||
handlers_info.append(handler_info)
|
||||
msg = f"REMOTE CONTROLLER: Found decorated command '{attr_name}': {description}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
# Getter
|
||||
elif callable(attr) and hasattr(attr, "dbus_getter_description"):
|
||||
description = attr.dbus_getter_description
|
||||
def _create_getter_wrapper(method=attr):
|
||||
def _wrapper(_notify_user=None):
|
||||
return method()
|
||||
return _wrapper
|
||||
handler_info = _HandlerInfo(
|
||||
python_function_name=attr_name,
|
||||
description=description,
|
||||
action=_create_getter_wrapper(),
|
||||
handler_type=HandlerType.GETTER
|
||||
)
|
||||
handlers_info.append(handler_info)
|
||||
msg = f"REMOTE CONTROLLER: Found decorated getter '{attr_name}': {description}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
# Setter
|
||||
elif callable(attr) and hasattr(attr, "dbus_setter_description"):
|
||||
description = attr.dbus_setter_description
|
||||
def _create_setter_wrapper(method=attr):
|
||||
def _wrapper(value):
|
||||
return method(value)
|
||||
return _wrapper
|
||||
handler_info = _HandlerInfo(
|
||||
python_function_name=attr_name,
|
||||
description=description,
|
||||
action=_create_setter_wrapper(),
|
||||
handler_type=HandlerType.SETTER
|
||||
)
|
||||
handlers_info.append(handler_info)
|
||||
msg = f"REMOTE CONTROLLER: Found decorated setter '{attr_name}': {description}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
if not handlers_info:
|
||||
return
|
||||
|
||||
self._dbus_service_interface.add_module_interface(
|
||||
module_name, handlers_info, self._bus, self.OBJECT_PATH)
|
||||
msg = (
|
||||
f"REMOTE CONTROLLER: Successfully registered {len(handlers_info)} "
|
||||
f"decorated commands/getters/setters for module {module_name}."
|
||||
)
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
def deregister_module_commands(self, module_name: str) -> bool:
|
||||
"""Deregisters D-Bus commands for a Cthulhu module."""
|
||||
|
||||
if not self._dasbus_available:
|
||||
return False
|
||||
|
||||
if module_name in self._pending_registrations:
|
||||
msg = f"REMOTE CONTROLLER: Removing pending registration for {module_name}."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
del self._pending_registrations[module_name]
|
||||
return True
|
||||
|
||||
if not self._is_running or not self._dbus_service_interface or not self._bus:
|
||||
msg = (
|
||||
f"REMOTE CONTROLLER: Cannot deregister commands for {module_name}; "
|
||||
"service not running or bus not available."
|
||||
)
|
||||
debug.printMessage(debug.LEVEL_WARNING, msg, True)
|
||||
return False
|
||||
|
||||
return self._dbus_service_interface.remove_module_interface(
|
||||
module_name, self._bus, self.OBJECT_PATH)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""Shuts down the D-Bus service."""
|
||||
|
||||
if not self._dasbus_available:
|
||||
return
|
||||
|
||||
if not self._is_running:
|
||||
msg = "REMOTE CONTROLLER: Shutdown called but service is not running."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return
|
||||
|
||||
msg = "REMOTE CONTROLLER: Attempting to shut down D-Bus service."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
if self._dbus_service_interface and self._bus:
|
||||
self._dbus_service_interface.shutdown_service(self._bus, self.OBJECT_PATH)
|
||||
try:
|
||||
self._bus.unpublish_object(self.OBJECT_PATH)
|
||||
except DBusError as e:
|
||||
msg = f"REMOTE CONTROLLER: Error unpublishing main service object: {e}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._dbus_service_interface = None
|
||||
|
||||
if self._bus:
|
||||
try:
|
||||
self._bus.unregister_service(self.SERVICE_NAME)
|
||||
except DBusError as e:
|
||||
msg = f"REMOTE CONTROLLER: Error releasing bus name: {e}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._bus.disconnect()
|
||||
self._bus = None
|
||||
|
||||
self._is_running = False
|
||||
msg = "REMOTE CONTROLLER: D-Bus service shut down."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._pending_registrations.clear()
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""Checks if the D-Bus service is currently running."""
|
||||
|
||||
return self._is_running
|
||||
|
||||
_remote_controller: CthulhuRemoteController = CthulhuRemoteController()
|
||||
|
||||
def get_remote_controller() -> CthulhuRemoteController:
|
||||
"""Returns the CthulhuRemoteController singleton."""
|
||||
|
||||
return _remote_controller
|
1736
src/cthulhu/debug-2025-06-25-23:07:09.out
Normal file
1736
src/cthulhu/debug-2025-06-25-23:07:09.out
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user