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 <s.sager@kunbus.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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, []),
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
<node>
|
||||
<interface name="com.revolutionpi.middleware1.SoftwareServices">
|
||||
<method name="Disable">
|
||||
<arg name="feature" type="s" direction="in"/>
|
||||
</method>
|
||||
<method name="Enable">
|
||||
<arg name="feature" type="s" direction="in"/>
|
||||
</method>
|
||||
<method name="GetStatus">
|
||||
<arg name="feature" type="s" direction="in"/>
|
||||
<arg name="status" type="b" direction="out"/>
|
||||
</method>
|
||||
<method name="GetAvailability">
|
||||
<arg name="feature" type="s" direction="in"/>
|
||||
<arg name="available" type="b" direction="out"/>
|
||||
</method>
|
||||
<property name="available_features" type="as" access="read"/>
|
||||
<signal name="StatusChanged">
|
||||
<arg name="feature" type="s"/>
|
||||
<arg name="status" type="b"/>
|
||||
</signal>
|
||||
<signal name="AvailabilityChanged">
|
||||
<arg name="feature" type="s"/>
|
||||
<arg name="available" type="b"/>
|
||||
</signal>
|
||||
</interface>
|
||||
</node>
|
||||
"""
|
||||
|
||||
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())
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user