# Building a D-Bus service in Python

I'm a big fan of [D-Bus](https://www.freedesktop.org/wiki/Software/dbus/) for implementing services and their clients on Linux. According to its website:

> D-Bus is an inter-process communication mechanism—a medium for local communication between processes running on the same host.

Essentially, clients connect to the system- or session-level bus where they can then communicate with associated services by calling their methods or listening to their signals, which looks something like:

![D-Bus_method_invocation.svg.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1663027024128/A6PDPNm1C.png align="left")

This is the approach I used with [sessiond](https://twiddlingbits.net/sessiond) and one I decided to implement in my monitor brightness controller, [lighten](https://twiddlingbits.net/lighten)—written in Python.

First, let's address *why* D-Bus is an appropriate choice for such systems:

* Using a standardized, full-featured protocol means we don't have to reinvent a client-server communication paradigm.
    
* Its ubiquity on the Linux desktop means access to a greater ecosystem of tools, language bindings, and learning resources.
    

Now, let's go into detail about the D-Bus architecture and explore the *how* of building a service in Python!

# D-Bus architecture

There are typically at least two D-Bus *buses* running in a given Linux environment. One operates at the system-level, independent of logged-in users. This is where system services, such as **systemd** and **systemd-logind** interact. Additionally, each user gets their own bus, where session-level services communicate.

When a service connects to one of these buses, it registers using a *well-known name*. This is the address by which other services or clients on the bus refer to it. These names conventionally look like reversed domain names. In the case of **lighten**, this name is: `com.github.jcrd.lighten`.

Services expose communication endpoints called *objects*, identified by an object path like `/com/github/jcrd/lighten`. These objects implement interfaces, where methods and signals are defined. `lighten` has two such interfaces:

* `com.github.jcrd.lighten.Backlight`, with methods for interacting with the monitor's backlight;
    
* `com.github.jcrd.lighten.Sensor`, with a method to get sensor data.
    

Each of these interfaces is implemented by a single object, but this isn't always the case. For example, **sessiond** has an interface for audio sinks, with an object created for each existing audio device.

Interfaces define methods with signatures denoting the type of in and out parameters. These parameters can be thought of as arguments and return values, respectively. Supported types include:

* integers
    
* booleans
    
* strings
    
* arrays
    
* dictionaries
    

See this [document](https://pythonhosted.org/txdbus/dbus_overview.html) for a complete list.

Methods are *1:1* modes of communication, returning data to the requesting client.

Interfaces also define signals with type signatures. Signals are *1:n* modes of communication, publishing data to all subscribed clients.

This overview grants an understanding of the flow of data through the D-Bus architecture sufficient to create our own service. Let's go!

# Python implementation

There are [numerous](https://wiki.python.org/moin/DbusExamples) Python libraries for building D-Bus services. Perhaps historically the most popular, [dbus-python](https://dbus.freedesktop.org/doc/dbus-python/) now describes itself as a legacy API and advises the use of alternatives.

I think the best alternative is GDBus, the D-Bus subsystem integrated into [GLib](https://docs.gtk.org/glib/). I used the C library in **sessiond**. It's usable in Python via [PyGObject](https://pygobject.readthedocs.io) which also provides access to much of **GLib** itself.

The biggest hurdle was the lack of Python-specific documentation. Thankfully, I found [this](https://discourse.gnome.org/t/minimal-example-of-gdbus-in-python/3165/26) discussion, wherein user *J Arun Mani* (thanks!) provides working examples of both client and server code.

Follow [this](https://pygobject.readthedocs.io/en/latest/getting_started.html) tutorial to install **PyGObject** and let's get to work!

To build out the D-Bus service in Python, first import the relevant libraries:

```python
from gi.repository import Gio, GLib
```

Next, use inline XML to specify the service's interfaces:

```xml
xml = f"""
<node>
  <interface name='com.github.jcrd.lighten.Backlight'>
      <method name='SetBrightness'>
          <arg name='value' type='u' direction='in'/>
          <arg name='success' type='b' direction='out'/>
      </method>
      <method name='AddBrightness'>
          <arg name='value' type='i' direction='in'/>
          <arg name='success' type='b' direction='out'/>
      </method>
      <method name='RestoreBrightness'>
          <arg name='success' type='b' direction='out'/>
      </method>
      <method name='GetBrightness'>
          <arg name='value' type='i' direction='out'/>
      </method>
  </interface>
</node>
"""
```

[This](https://developer-old.gnome.org/gio/stable/ch35s05.html) page provides more information about the D-Bus Introspection XML.

Every interface needs an accompanying *interface handler function* to dispatch Python code based on the called D-Bus method. The handler's function signature is:

```python
def handler(self, conn, sender, path, iname, method, params, invo):
```

The useful arguments are:

* `method`: the name of the called method;
    
* `params`: the parameters to the called method;
    
* `invo`: the invocation object used to return values.
    

`params` must be converted to Python types and accessed as an array. Get the first D-Bus method parameter with:

```python
params.unpack()[0]
```

Return values must be provided to `invo` as a **GLib** [variant](https://docs.gtk.org/glib/struct.Variant.html):

```python
invo.return_value(GLib.Variant("(b)", (True,)))
```

The D-Bus signature, `(b)`, is always a structure in this use case, so parentheses are required. Refer to the Signature Encoding table [here](https://pythonhosted.org/txdbus/dbus_overview.html) to determine the appropriate type character.

To wire everything together, three different *bus handler functions* can be defined. These are called when:

1. the service connects to its bus;
    
2. the service is assigned its name;
    
3. the service loses its name.
    

There are some nuances in the operation of these handlers. The first two serve nearly the same purpose, but it could be the case that bus connection succeeds yet a service with the same name already exists, so the second handler might not be called. It is important to note that a service can replace one with the same name provided the proper initialization flag. It's in this case that the replaced service's third handler is called. Disconnection from the bus itself would also trigger it.

Discretion is advised in choosing which to fully implement. For **sessiond**, which must exhibit robust behavior, I implemented handlers 2 and 3, forgoing 1 entirely. However, for **lighten**, I only used the first!

The general approach is to register objects when the service is connected and available, and, if appropriate, tear them down should the service become unavailable.

I recommend encapsulating all the service logic in a Python class, like so:

```python
class Service:
    def __init__(self):
        # Parse the XML interfaces:
        self.node = Gio.DBusNodeInfo.new_for_xml(xml)
        # Reference the GLib main loop:
        self.loop = GLib.MainLoop()

        # Connect to a bus:
        self.owner_id = Gio.bus_own_name(
            # Specify connection to the session bus:
            Gio.BusType.SESSION,
            # Set the well-known name:
            "com.github.jcrd.lighten",
            # Provide any flags
            # (for example, to allow replacement):
            Gio.BusNameOwnerFlags.NONE,
            # Provide handler 1 (defined below):
            self.on_bus_acquired,
            # Provide handler 2:
            None,
            # Provide handler 3:
            None,
        )

    def __del__(self):
        # Disconnect when the class is destroyed:
        Gio.bus_unown_name(self.owner_id)

    # Define handler 1:
    def on_bus_acquired(self, conn, name):
        # Register an object:
        conn.register_object(
            # Set the object path:
            "/com/github/jcrd/lighten",
            # Specify the interface via index
            # (as defined above in the XML):
            self.node.interfaces[0],
            # Provide the interface handler function:
            self.on_handle_backlight,
            None,
            None,
        )

  # Define the interface handler function
  # (abbreviated here with ...):
  def on_handle_backlight(self, conn, sender, path, iname, method, params, invo):
    if method == "SetBrightness":
        v = params.unpack()[0]
        r = ...(v)
        invo.return_value(GLib.Variant("(b)", (r,)))
    elif method == "AddBrightness":
        ...
```

Now, instantiate the class and run the D-Bus service with:

```python
Service().loop.run()
```

Finally, use a tool such as [d-feet](https://wiki.gnome.org/Apps/DFeet) to admire the fruits of your labor!

![Screenshot from 2022-09-13 01-03-00.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1663045646750/7brC1NseO.png align="left")
