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