Skip to content

Instantly share code, notes, and snippets.

@alexlarsson
Last active December 6, 2022 15:00
Show Gist options
  • Save alexlarsson/bed968e0043f5ba3b22637be08bf19ac to your computer and use it in GitHub Desktop.
Save alexlarsson/bed968e0043f5ba3b22637be08bf19ac to your computer and use it in GitHub Desktop.

Proxying Services

This daemon + template service file creates the ability to start a "proxy" service. This service will when started, either start or pick up an already running service of the given name and proxy its state (i.e. wheter its started or not, and whether activation succeeded).

So, starting test-proxy@foobar.service will start foobar.service if needed, and the proxy will be considered active when foobar becomes active, and will become deactivated when foobar stops. Stopping the proxy will not stop the main service.

This isn't really useful as is as you can just start the main service. However, the point is that this could be extended such that the proxy mirrors the state of a remote service so that you can set up remote dependencies.

To test

Install test-proxy.conf in /etc/dbus-1/system.d and run systemctl reload dbus-broker.service. This allows root to own the org.test.Proxy dbus name and others to talk to it.

Install sleep-some.service and test-proxy@.service to /etc/systemd/system/ and run systemctl daemon-reload. `sleep-some.service is a sample service that sleeps for 10 sec then exists.

As root, start test-proxy.py, this is the main proxy service that the test-proxy service talks to.

As root, run: systemctl start test-proxy@sleep-some.service, or systemctl start test-proxy@nosuch.service to test successfil

[Unit]
[Service]
ExecStart=/bin/sleep 10
<!-- This configuration file specifies the required security policies
for Bluetooth core daemon to work. -->
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<!-- ../system.conf have denied everything, so we just punch some holes -->
<policy user="root">
<allow own_prefix="org.test.Proxy"/>
</policy>
<policy context="default">
<allow send_destination="org.test.Proxy"/>
<allow send_destination_prefix="org.test.Proxy"/>
</policy>
</busconfig>
#!/usr/bin/python3
from gi.repository import GLib
from gi.repository import Gio
import sys
SYSTEMD_BUS_NAME = "org.freedesktop.systemd1"
SYSTEMD_MANAGER_PATH = "/org/freedesktop/systemd1"
SYSTEMD_MANAGER_IFACE = "org.freedesktop.systemd1.Manager"
SYSTEMD_UNIT_IFACE = "org.freedesktop.systemd1.Unit"
PROPERTIES_IFACE = "org.freedesktop.DBus.Properties"
introspection_xml = """
<node>
<interface name="org.test.ProxyManager">
<method name='EnsureProxy'>
<arg type='s' name='proxyservice' direction='in'/>
<arg type='s' name='service' direction='in'/>
<arg type='s' name='res' direction='out'/>
</method>
</interface>
</node>
"""
def active_state_is_active(state):
return state == "active" or state == "reloading"
class Proxy(object):
proxies = []
def stop(self):
if self.subscribe_id:
bus.signal_unsubscribe(self.subscribe_id)
assert self in Proxy.proxies
Proxy.proxies.remove(self)
print("Proxy done")
def fail_ensure(self, error, message):
print("Ensure failed: ", message)
assert self.ensure_invocation != None
self.ensure_invocation.return_dbus_error(error, message)
self.ensure_invocation = None
self.stop()
def complete_ensure(self):
assert self.ensure_invocation != None
self.ensure_invocation.return_value(GLib.Variant("(s)", ("done",)))
self.ensure_invocation = None
def __init__(self, proxyservicename, servicename, invocation):
print("New service ", proxyservicename, servicename)
self.service = proxyservicename # Name of the proxy service
self.proxied_service = servicename # Name of the service whos state we proxy
self.ensure_invocation = invocation
self.proxied_service_path = None # Object path of proxied service in systemd
self.initialized = False # On init we do a bunch of async stuff, then set this to true
self.proxy_started = False
self.waiting_for_activation = True
self.subscribe_id = None
self.properties = {}
Proxy.proxies.append(self)
bus.call(SYSTEMD_BUS_NAME, SYSTEMD_MANAGER_PATH,
SYSTEMD_MANAGER_IFACE, "LoadUnit",
GLib.Variant("(s)", ( self.proxied_service, )),
GLib.VariantType.new("(o)"),
0, -1, None,
self.unit_loaded_cb)
def unit_loaded_cb(self, obj, res):
try:
reply = bus.call_finish(res)
except:
return self.fail_ensure("org.freedesktop.DBus.Error.Failed", "LoadUnit failed")
self.proxied_service_path = reply[0]
self.subscribe_id = bus.signal_subscribe(SYSTEMD_BUS_NAME, PROPERTIES_IFACE,
"PropertiesChanged", self.proxied_service_path, None, 0,
self._properties_changed_cb)
bus.call(SYSTEMD_BUS_NAME, self.proxied_service_path,
PROPERTIES_IFACE, "GetAll",
GLib.Variant("(s)", (SYSTEMD_UNIT_IFACE, )),
GLib.VariantType.new("(a{sv})"),
0, -1, None,
self.get_all_props_cb)
def get_all_props_cb(self, obj, res):
try:
reply = bus.call_finish(res)
except:
return self.fail_ensure("org.freedesktop.DBus.Error.Failed", "GetAll failed")
self.properties = reply.unpack()[0]
print("Initialized")
# We got initial GetAll, now start listening on property changes
self.initialized = True
# Check if service already active, then we don't need to start
if active_state_is_active(self.get_active_state()):
self.service_activated()
else:
# Otherwise we need to start it
print("Starting service ", self.proxied_service)
bus.call(SYSTEMD_BUS_NAME, self.proxied_service_path,
SYSTEMD_UNIT_IFACE, "Start",
GLib.Variant("(s)", ("replace", )),
GLib.VariantType.new("(o)"),
0, -1, None,
self.start_service_cb)
def service_activated(self):
print("Service is now active")
self.proxy_started = True
self.complete_ensure()
def start_service_cb(self, obj, res):
try:
reply = bus.call_finish(res)
except:
(te, e, tracback) = sys.exc_info()
Gio.dbus_error_strip_remote_error(e)
return self.fail_ensure("org.freedesktop.DBus.Error.Failed", "Start failed: %s" % (e.message, ))
print("Service started")
# Maybe we were activated started while running the start job
if active_state_is_active(self.get_active_state()):
return self.service_activated()
else:
# Else wait for it to change to an active state via property change
self.waiting_for_activation = True
def _properties_changed_cb(self, connection, sender, path, iface, signal, parameters):
prop_iface = parameters[0]
changes = parameters[1]
if not self.initialized or prop_iface != SYSTEMD_UNIT_IFACE:
return
last_properties = self.properties
self.properties = {**self.properties, **changes}
self.properties_changed(last_properties)
def properties_changed(self, last_properties):
old_active = last_properties.get("ActiveState")
new_active = self.properties.get("ActiveState")
if old_active != new_active:
self.active_state_changed(new_active, old_active)
def get_active_state(self):
return self.properties.get("ActiveState")
def active_state_changed(self, new_active_state, old_active_state):
print("Active changed %s -> %s" % (old_active_state, new_active_state))
if self.waiting_for_activation:
if active_state_is_active(new_active_state):
self.waiting_for_activation = False
self.service_activated()
elif new_active_state == "failed":
self.waiting_for_activation = False
self.fail_ensure("org.freedesktop.DBus.Error.Failed", "Starting target failed")
if self.proxy_started and not active_state_is_active(new_active_state):
print("Stopping service ", self.service)
bus.call(SYSTEMD_BUS_NAME, SYSTEMD_MANAGER_PATH,
SYSTEMD_MANAGER_IFACE, "StopUnit",
GLib.Variant("(ss)", (self.service, "replace", )),
GLib.VariantType.new("(o)"),
0, -1, None,
self.stop_service_cb)
def stop_service_cb(self, obj, res):
try:
reply = bus.call_finish(res)
except:
pass
print("stop failed", sys.exc_info())
self.stop()
def handle_method_call(connection, sender, object_path, interface_name,
method_name, parameters, invocation):
if method_name == "EnsureProxy":
proxyservicename = parameters.unpack()[0]
servicename = parameters.unpack()[1]
proxy = Proxy(proxyservicename, servicename, invocation)
else:
invocation.return_dbus_error("org.freedesktop.DBus.Error.UnknownMethod",
"No such method")
def handle_get_property(connection, sender, object_path, interface, value):
pass
def handle_set_property(connection, sender, object_path, interface_name, key,
value):
pass
def on_bus_acquired(connection, name, *args):
reg_id = Gio.DBusConnection.register_object(
connection,
"/org/test/Proxy",
introspection_data.interfaces[0],
handle_method_call,
handle_get_property,
handle_set_property)
if reg_id == 0:
print('Error while registering object!')
sys.exit(1)
def on_name_lost(connection, name, *args):
sys.exit(1)
def on_name_acquired(connection, name, *args):
pass
bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
bus.call(SYSTEMD_BUS_NAME, SYSTEMD_MANAGER_PATH,
SYSTEMD_MANAGER_IFACE, "Subscribe",
GLib.Variant("()", None), None,
0, -1, None)
introspection_data = Gio.DBusNodeInfo.new_for_xml(introspection_xml)
owner_id = Gio.bus_own_name(Gio.BusType.SYSTEM,
"org.test.Proxy",
Gio.BusNameOwnerFlags.NONE,
on_bus_acquired,
on_name_acquired,
on_name_lost)
try:
GLib.MainLoop().run()
except KeyboardInterrupt:
pass
sys.exit(0)
[Unit]
Description=Test proxy
[Service]
ExecStart=gdbus call --timeout 999999 --system --dest org.test.Proxy --object-path /org/test/Proxy --method org.test.ProxyManager.EnsureProxy %n %i.service
Type=oneshot
RemainAfterExit=yes
KillMode=mixed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment