Starting systemd services sharing a session D-Bus on headless system

21,859

You need several things to make this work.

  1. Enable user services to run at boot time without user login (systemd linger).
  2. A systemd socket file to specify the D-Bus socket for systemd to allocate.
  3. A systemd service to launch the D-Bus session bus that launches, then sets the DBUS_SESSION_BUS_ADDRESS env var for other systemd services.
  4. Ensure your systemd my-dbus-client.service files are of Type=dbus or depend on the dbus.socket unit to make sure they allocate the dbus session bus socket and start the dbus session service if it hasn't already been started.

First, to make Systemd services for a given user start at boot-time without login, you need to enable systemd user lingering - this only needs to be done once as root when configuring to enable it for a user:

# loginctl enable-linger otheruser

Next, if you're on a Debian-based system, for the next two steps, you can simply install the package dbus-user-session package:

# apt-get install dbus-user-session

If you're using some other distribution, want to do this manually, or just want to understand how it works continue on. Otherwise skip over the creation of dbus.service and dbus.socket.

Create the file /usr/lib/systemd/user/dbus.socket (note, on some distributions the user directory may be under /lib instead of /usr/lib) with the following content:

[Unit]
Description=D-Bus User Message Bus Socket

[Socket]
ListenStream=%t/bus
ExecStartPost=-/bin/systemctl --user set-environment DBUS_SESSION_BUS_ADDRESS=unix:path=%t/bus

[Install]
WantedBy=sockets.target
Also=dbus.service

Propagation of DBUS_SESSION_BUS_ADDRESS to all services, which was your main concern, is addressed by the ExecPostStart line below - all following services will have that set.

%t gets replaced with the XDG_RUNTIME_DIR - a transient directory under /run created by systemd specific to the user session that you can stuff files. If you wish to create this socket somewhere else, there's no reason you can't. Just make sure it's somewhere transient, or it gets cleaned up on reboot/session teardown.

I did have some issues trying to make the dbus unix socket an abstract one - systemd didn't seem to like the form unix:abstract= or @ prefix for some reason.

Now create the file /usr/lib/systemd/user/dbus.service with the following content:

[Unit]
Description=D-Bus User Message Bus
Requires=dbus.socket

[Service]
ExecStart=/usr/bin/dbus-daemon --session --address=systemd: --nofork --nopidfile --systemd-activation
ExecReload=/usr/bin/dbus-send --print-reply --session --type=method_call --dest=org.freedesktop.DBus / org.freedesktop.DBus.ReloadConfig

[Install]
Also=dbus.socket

There is a little bit of magic that goes on here behind the scenes by systemd to pass in the already created unix socket to the dbus-daemon. Systemd uses the information from dbus.socket to create the socket, and it's file descriptor gets set in the environment variable LISTEN_FDS, which is passed into the dbus-daemon. Special options listed above make dbus-daemon use the file descriptor passed in instead of creating a new one. This allows dbus clients to start parallel to the dbus-daemon starting without worries of the socket not existing.

Finally, create your own systemd user services, making sure that you either set the type to Type=dbus, set BusName= to the name of one of the dbus service names that will be registered by this service, or by making sure Requires=dbus.socket is specified in the Unit section. Here is an example:

[Unit]
Description=Config Server Startup

[Service]
Type=dbus
BusName=com.example.app.configuree
ExecStart=/opt/example/app/configuration_server
Restart=on-failure

[Install]
WantedBy=default.target

You can place them in one of several places:

  • $HOME/.config/systemd/user
  • /usr/lib/systemd/user

Enable your services with systemctl --user enable <service name> and reboot, and everything should work.

For systemctl --user .. to work, you need to have a full systemd login environment for the /run/user/{uid} to exist. The lightweight environments created by su - .. --login or sudo do not set this up. You need to ssh in, log into a console or, if you run a properly set up systemd distribution, you can grab and use machinectl shell to create a full systemd environment in your current shell.


References:

  • man loginctl for linger
  • man pam_systemd for XDG_RUNTIME_DIR info
  • man systemd.service for Type=dbus, BusName=, and implicit dependency on dbus.socket
  • man sd_listen_fds for info about LISTEN_FDS environment variable
  • https://wiki.archlinux.org/index.php/Systemd/User - general information on systemd user sessions
Share:
21,859

Related videos on Youtube

Ole Wolf
Author by

Ole Wolf

Senior embedded systems consultant. Professional experience: project management, software development, electrical engineering, enterprise architecture, process optimization, IT infrastructure security. Heavy on DSP and embedded systems. Outside work hours: martial arts black belt, beer brewer, and electronics tinkerer.

Updated on September 18, 2022

Comments

  • Ole Wolf
    Ole Wolf over 1 year

    I need help starting services that communicate via a session (not system) D-Bus on a headless Linux system. The key is that no-one will be logged in on the headless system.

    So far I've been able to start a D-Bus daemon and test the D-Bus communication on behalf of a user ("otheruser") who is not logged in, in three different terminals:

    In the first terminal, I start a D-Bus daemon for the "otheruser":

    $ sudo -u otheruser dbus-daemon --session --print-address 1
    unix:abstract=/tmp/dbus-a5cU7r4IHc,guid=6c0a9bbfd02f5f68da0fe32f5a5e0a48
    

    In the second terminal I start the D-Bus server application using the above DBUS_SESSION_BUS_ADDRESS response:

    $ sudo -u otheruser DBUS_SESSION_BUS_ADDRESS="unix:abstract=/tmp/dbus-a5cU7r4IHc,guid=6c0a9bbfd02f5f68da0fe32f5a5e0a48" /usr/bin/my-dbus-service
    

    Then, in the third terminal, I can test the connection:

    $ sudo -u otheruser DBUS_SESSION_BUS_ADDRESS="unix:abstract=/tmp/dbus-a5cU7r4IHc,guid=6c0a9bbfd02f5f68da0fe32f5a5e0a48" gdbus introspect --session --dest com.mycompany.myappname --object-path /com/mycompany/interface
    

    But, I want to start the D-Bus server application as well as a few client D-Bus services via systemd. How do I start a D-Bus session via systemd so that its DBUS_SESSION_BUS_ADDRESS environment variable gets propagated to the D-Bus server and client services for "otheruser"?

    One possible solution might be to pipe the output of dbus-daemon into a "somefile," and then set DBUS_SESSION_BUS_ADDRESS=$(cat somefile) before starting the D-Bus server and clients. This just seems a little too awkward to me; especially because I'm aware there is some magic with a "Busname" directive in the systemd service file for system D-Bus connections. How do I properly start systemd services for "otheruser" so that these systemd services can communicate with a session D-Bus interface?

  • Bernardo Meurer
    Bernardo Meurer over 5 years
    Thank you so much for this. It has saved me so much work!
  • Keithel
    Keithel almost 5 years
    The documentation is a little scant on this, and it's hard to piece it all together. I'm glad it has helped someone! Maybe it'll help me again in the future. :D
  • Norman Ramsey
    Norman Ramsey over 4 years
    One of the best answers I have seen in a long time. Nice work!
  • Keithel
    Keithel over 2 years
    @mianos Could you edit my answer to clarify this? A better answer, without having to read through the comments would be most excellent.