From 50789853e97271ddc6ad117b6735048eb470fa97 Mon Sep 17 00:00:00 2001 From: Sven Sager Date: Tue, 27 May 2025 12:19:32 +0200 Subject: [PATCH] feat(dbus): Add D-Bus interface for managing software services Introduce `InterfaceSoftwareServices` to handle service enable/disable actions, status, and availability via D-Bus. Consolidate `avahi` service configuration into the new interface by removing redundant logic from `revpi_config.py`. Signed-off-by: Sven Sager --- .../system_config/__init__.py | 1 + .../system_config/interface_config.py | 11 - .../system_config/interface_services.py | 205 ++++++++++++++++++ .../system_config/revpi_config.py | 12 - 4 files changed, 206 insertions(+), 23 deletions(-) create mode 100644 src/revpi_middleware/dbus_middleware1/system_config/interface_services.py diff --git a/src/revpi_middleware/dbus_middleware1/system_config/__init__.py b/src/revpi_middleware/dbus_middleware1/system_config/__init__.py index 8f6ef5d..2b51c7a 100644 --- a/src/revpi_middleware/dbus_middleware1/system_config/__init__.py +++ b/src/revpi_middleware/dbus_middleware1/system_config/__init__.py @@ -3,3 +3,4 @@ # SPDX-License-Identifier: GPL-2.0-or-later """D-Bus interfaces for system configuration.""" from .interface_config import InterfaceRevpiConfig +from .interface_services import InterfaceSoftwareServices diff --git a/src/revpi_middleware/dbus_middleware1/system_config/interface_config.py b/src/revpi_middleware/dbus_middleware1/system_config/interface_config.py index d5989ce..fba099f 100644 --- a/src/revpi_middleware/dbus_middleware1/system_config/interface_config.py +++ b/src/revpi_middleware/dbus_middleware1/system_config/interface_config.py @@ -9,7 +9,6 @@ from pydbus.generic import signal from .revpi_config import ( ConfigActions, - configure_avahi_daemon, configure_bluetooth, configure_con_can, configure_dphys_swapfile, @@ -96,17 +95,7 @@ AVAILABLE_FEATURES = { "gui": FeatureFunction(configure_gui, []), "revpi-con-can": FeatureFunction(configure_con_can, []), "dphys-swapfile": FeatureFunction(configure_dphys_swapfile, []), - "pimodbus-master": FeatureFunction(simple_systemd, ["pimodbus-master.service"]), - "pimodbus-slave": FeatureFunction(simple_systemd, ["pimodbus-slave.service"]), - "systemd-timesyncd": FeatureFunction(simple_systemd, ["systemd-timesyncd.service"]), - "ssh": FeatureFunction(simple_systemd, ["ssh.service"]), - "nodered": FeatureFunction(simple_systemd, ["nodered.service"]), - "noderedrevpinodes-server": FeatureFunction( - simple_systemd, ["noderedrevpinodes-server.service"] - ), - "revpipyload": FeatureFunction(simple_systemd, ["revpipyload.service"]), "bluetooth": FeatureFunction(configure_bluetooth, []), "wlan": FeatureFunction(configure_wlan, []), - "avahi": FeatureFunction(configure_avahi_daemon, []), "external-antenna": FeatureFunction(configure_external_antenna, []), } diff --git a/src/revpi_middleware/dbus_middleware1/system_config/interface_services.py b/src/revpi_middleware/dbus_middleware1/system_config/interface_services.py new file mode 100644 index 0000000..747ba8b --- /dev/null +++ b/src/revpi_middleware/dbus_middleware1/system_config/interface_services.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: 2025 KUNBUS GmbH +# SPDX-License-Identifier: GPL-2.0-or-later +"""D-Bus interfaces for software services.""" +from logging import getLogger +from typing import List + +from pydbus.generic import signal + +from ..dbus_helper import DbusInterface +from ..systemd_helper import simple_systemd, ServiceActions + +log = getLogger(__name__) + + +class InterfaceSoftwareServices(DbusInterface): + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ + + AvailabilityChanged = signal() + StatusChanged = signal() + + def __init__(self, bus): + super().__init__(bus) + + self.mrk_available = {} + self.mrk_status = {} + + self.services = { + "pimodbus-master": ["pimodbus-master.service"], + "pimodbus-slave": ["pimodbus-slave.service"], + "systemd-timesyncd": ["systemd-timesyncd.service"], + "ssh": ["ssh.service"], + "nodered": ["nodered.service"], + "noderedrevpinodes-server": ["noderedrevpinodes-server.service"], + "revpipyload": ["revpipyload.service"], + "avahi": ["avahi-daemon.service", "avahi-daemon.socket"], + } + + # Create a systemd manager interface object + systemd = self.bus.get( + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + ) + systemd_manager = systemd["org.freedesktop.systemd1.Manager"] + + # Load all unit paths and subscribe to properties changed signal + self.object_paths = {} + for feature in self.services: + + # Get the status and availability of the feature + self.mrk_available[feature] = self.GetAvailability(feature) + self.mrk_status[feature] = self.GetStatus(feature) + + # Subscribe to properties changed signal for each unit + for unit_name in self.services[feature]: + unit_path = systemd_manager.LoadUnit(unit_name) + self.object_paths[unit_path] = feature + + self.bus.subscribe( + iface="org.freedesktop.DBus.Properties", + signal="PropertiesChanged", + object=unit_path, + signal_fired=self._callback_properties_changed, + ) + + # Subscribe to the reloading signal to update the availability of the feature + self.bus.subscribe( + iface="org.freedesktop.systemd1.Manager", + signal="Reloading", + object="/org/freedesktop/systemd1", + signal_fired=self._callback_reloading_signal, + ) + + def _callback_reloading_signal(self, sender, object_path, interface, signal, parameters): + """ + Handles the signal emitted for reloading and checks for changes in availability + and status for a set of services. If changes are identified, corresponding + update methods are triggered to reflect the new states. + + Args: + sender: The entity sending the signal. + object_path: Path to the object emitting the signal. + interface: Interface through which the signal is sent. + signal: The signal being received. + parameters: A list of parameters associated with the signal. + + Raises: + None + """ + if parameters[0]: + return + + for feature in self.services: + availability = self.GetAvailability(feature) + if self.mrk_available[feature] != availability: + self.mrk_available[feature] = availability + self.AvailabilityChanged(feature, availability) + + status = self.GetStatus(feature) + if self.mrk_status[feature] != status: + self.mrk_status[feature] = status + self.StatusChanged(feature, status) + + def _callback_properties_changed(self, sender, object_path, interface, signal, parameters): + """ + Handles the 'PropertiesChanged' signal callback by updating internal status tracking for given + features and invoking status change notifications if necessary. + + Args: + sender (Any): Information about the signal sender. + object_path (str): The path of the object emitting the signal. + interface (str): The interface where the signal was emitted. + signal (str): The name of the emitted signal. + parameters (tuple): Signal parameters containing interface name, changed properties, and + invalidated properties. + + Raises: + TypeError: If the 'parameters' argument does not unpack to three expected values. + """ + interface, changed_properties, invalidated_properties = parameters + if "ActiveState" not in changed_properties: + return + + feature = self.object_paths[object_path] + status = self.GetStatus(feature) + if self.mrk_status[feature] != status: + self.mrk_status[feature] = status + self.StatusChanged(feature, status) + + def _get_unit_names(self, feature: str) -> List[str]: + if feature not in self.services: + raise ValueError(f"feature {feature} does not exist") + return self.services[feature] + + def Disable(self, feature: str) -> None: + """Disable the feature.""" + action = ServiceActions.DISABLE + unit_names = self._get_unit_names(feature) + + for unit_name in unit_names: + simple_systemd(action, unit_name) + + def Enable(self, feature: str) -> None: + """Enable the feature.""" + action = ServiceActions.ENABLE + unit_names = self._get_unit_names(feature) + + for unit_name in unit_names: + simple_systemd(action, unit_name) + + def GetStatus(self, feature: str) -> bool: + """Get feature status.""" + unit_names = self._get_unit_names(feature) + rc_status = True + + for unit_name in unit_names: + if not simple_systemd(ServiceActions.STATUS, unit_name): + rc_status = False + break + + return rc_status + + def GetAvailability(self, feature: str) -> bool: + """Get feature availability on the RevPi.""" + unit_names = self._get_unit_names(feature) + rc_available = True + + for unit_name in unit_names: + if not simple_systemd(ServiceActions.AVAILABLE, unit_name): + rc_available = False + break + + return rc_available + + @property + def available_features(self) -> list[str]: + return list(self.services.keys()) diff --git a/src/revpi_middleware/dbus_middleware1/system_config/revpi_config.py b/src/revpi_middleware/dbus_middleware1/system_config/revpi_config.py index 5cdbc3b..3e2fb29 100644 --- a/src/revpi_middleware/dbus_middleware1/system_config/revpi_config.py +++ b/src/revpi_middleware/dbus_middleware1/system_config/revpi_config.py @@ -435,18 +435,6 @@ class ConfigTxt: return self._config_txt_path -def configure_avahi_daemon(action: ConfigActions): - return_value = simple_systemd(action, "avahi-daemon.service") - - # Post actions for avahi-daemon - if action in (ConfigActions.ENABLE, ConfigActions.DISABLE): - # Apply the enable/disable action to the avahi socket AFTER the service - # unit, because a connected socket could interrupt stop - simple_systemd(action, "avahi-daemon.socket") - - return return_value - - def configure_bluetooth(action: ConfigActions): hci_device = join(LINUX_BT_CLASS_PATH, "hci0") bt_rfkill_index = get_rfkill_index(hci_device)