Skip to content

D-Bus and systemd for Desktop Systems

The Warmwind job posting lists D-Bus as a requirement. This article covers the D-Bus message bus architecture, practical exploration with busctl/gdbus, how Sway and logind use D-Bus, systemd socket activation, and writing systemd units for a Wayland kiosk session -- all the plumbing that connects Warmwind's compositor, browser, and AI agent processes.

D-Bus Architecture

graph LR
    A["Process A"] --> SB["Session Bus"]
    B["Process B"] --> SB
    C["Process C"] --> SB
    D["Root Service"] --> YB["System Bus"]
    E["logind"] --> YB
    F["NetworkManager"] --> YB

D-Bus is an IPC (inter-process communication) system with two persistent bus instances on a typical Linux desktop:

Bus Socket Purpose
System bus /run/dbus/system_bus_socket System-wide services (logind, NetworkManager, udev)
Session bus $DBUS_SESSION_BUS_ADDRESS Per-user services (desktop portals, notification daemon, PulseAudio)

Message Types

Every D-Bus message is one of four types:

Type Direction Purpose
method_call Client -> Service Request an action
method_return Service -> Client Successful response
error Service -> Client Failure response
signal Service -> All subscribers Broadcast notification

Addressing Model

D-Bus uses a three-level addressing scheme:

Bus name          → Object path         → Interface.Method
org.freedesktop.login1  /org/freedesktop/login1  org.freedesktop.login1.Manager.ListSessions
  • Well-known name: Reverse-DNS identifier (e.g. org.freedesktop.login1). A service claims this name on the bus.
  • Object path: Tree-structured path to a specific object (e.g. /org/freedesktop/login1/session/c2).
  • Interface: Groups related methods and signals (e.g. org.freedesktop.login1.Session).

Practical D-Bus Exploration

busctl (systemd's D-Bus tool)

# List all services on the session bus
busctl --user list

# List all services on the system bus
busctl list

# Introspect an object -- shows interfaces, methods, properties, signals
busctl introspect org.freedesktop.login1 /org/freedesktop/login1

# Call a method: list active sessions
busctl call org.freedesktop.login1 \
    /org/freedesktop/login1 \
    org.freedesktop.login1.Manager \
    ListSessions

# Monitor signals in real time
busctl monitor org.freedesktop.login1

# Get a property
busctl get-property org.freedesktop.login1 \
    /org/freedesktop/login1/session/c2 \
    org.freedesktop.login1.Session \
    Active

gdbus (GLib's D-Bus tool)

# Introspect
gdbus introspect --system \
    --dest org.freedesktop.login1 \
    --object-path /org/freedesktop/login1

# Call a method
gdbus call --system \
    --dest org.freedesktop.login1 \
    --object-path /org/freedesktop/login1 \
    --method org.freedesktop.login1.Manager.ListSessions

# Watch signals
gdbus monitor --system --dest org.freedesktop.login1

dbus-send (low-level, no systemd dependency)

# Inhibit idle (e.g. during VNC session)
dbus-send --session --print-reply \
    --dest=org.freedesktop.ScreenSaver \
    /org/freedesktop/ScreenSaver \
    org.freedesktop.ScreenSaver.Inhibit \
    string:"wayvnc" string:"VNC session active"

How Sway Uses D-Bus

Sway itself is minimal about D-Bus -- it delegates to external tools. But the Sway ecosystem touches D-Bus in several critical ways:

Idle Inhibit and Screen Lock

graph LR
    Sway["Sway"] --> Idle["swayidle"]
    Idle --> Lock["swaylock"]
    Idle --> DPMS["Output DPMS"]
    App["Application"] --> Inhibit["idle-inhibit protocol"]
    Inhibit --> Sway

swayidle listens for the ext-idle-notify-v1 Wayland protocol and triggers actions (lock screen, DPMS off). Applications inhibit idle via the idle-inhibit-unstable-v1 Wayland protocol -- but some applications (Firefox, Chromium) also use the D-Bus org.freedesktop.ScreenSaver.Inhibit interface.

Sway config integration:

# ~/.config/sway/config
exec swayidle -w \
    timeout 300 'swaylock -f -c 000000' \
    timeout 600 'swaymsg "output * dpms off"' \
    resume 'swaymsg "output * dpms on"' \
    before-sleep 'swaylock -f -c 000000'

XDG Desktop Portal Backends

Sway uses xdg-desktop-portal-wlr (or the newer xdg-desktop-portal-gtk fallback) for portal backends. These expose D-Bus interfaces for:

  • Screen sharing (org.freedesktop.portal.ScreenCast): PipeWire-based screen capture triggered by WebRTC or OBS.
  • File picker (org.freedesktop.portal.FileChooser): GTK file dialog.
  • Screenshot (org.freedesktop.portal.Screenshot): One-shot capture.

Portal D-Bus service runs as a user service:

# Check portal status
busctl --user list | grep portal
# org.freedesktop.impl.portal.desktop.wlr
# org.freedesktop.portal.Desktop

The logind D-Bus API

logind (systemd-logind) manages user sessions, seats, and device access. It is the gatekeeper for GPU, input, and DRM device access on modern Linux desktops.

Key Interfaces

Manager (org.freedesktop.login1.Manager):

# List sessions
busctl call org.freedesktop.login1 /org/freedesktop/login1 \
    org.freedesktop.login1.Manager ListSessions

# List seats
busctl call org.freedesktop.login1 /org/freedesktop/login1 \
    org.freedesktop.login1.Manager ListSeats

Session (org.freedesktop.login1.Session):

The session object is where device access mediation happens.

# Check if a session is active
busctl get-property org.freedesktop.login1 \
    /org/freedesktop/login1/session/c2 \
    org.freedesktop.login1.Session Active

# Get the seat
busctl get-property org.freedesktop.login1 \
    /org/freedesktop/login1/session/c2 \
    org.freedesktop.login1.Session Seat

TakeDevice / ReleaseDevice

This is how Wayland compositors get GPU and input device access without running as root:

Compositor                          logind (D-Bus)
  |                                      |
  |-- TakeControl() ------------------> |   (claim session controller)
  |<-- success ------------------------ |
  |                                      |
  |-- TakeDevice(major, minor) -------> |   (request /dev/dri/card0)
  |<-- fd, inactive ------------------- |   (fd + pause/resume capability)
  |                                      |
  |   (session goes inactive - VT switch)|
  |<-- PauseDevice(major, minor, type) - |   (compositor must release GPU)
  |-- PauseDeviceComplete(major, minor)> |
  |                                      |
  |   (session becomes active again)     |
  |<-- ResumeDevice(major, minor, fd) -- |   (new fd, resume rendering)
  |                                      |
  |-- ReleaseDevice(major, minor) ----> |   (give up device)
  |-- ReleaseControl() ---------------> |   (release session controller)

Why this matters for Warmwind: The compositor (Sway) uses TakeDevice to acquire the DRM device fd without root privileges. When running headless (no real GPU), logind still mediates access. Understanding this flow is essential for debugging "permission denied" errors on /dev/dri/*.

Reference: org.freedesktop.login1 man page


systemd Socket Activation

Socket activation lets systemd create the listening socket and only start the service when a connection arrives. This is how D-Bus services are lazy-started.

How It Works

graph LR
    systemd["systemd"] -- "creates socket" --> Socket["Unix/TCP Socket"]
    Client["Client connects"] --> Socket
    Socket -- "starts service" --> Service["Service Process"]
    Service -- "sd_listen_fds()" --> FDs["Inherited FDs"]

sd_listen_fds() and sd_notify()

#include <systemd/sd-daemon.h>

int main(void) {
    // How many fds did systemd pass us?
    int n = sd_listen_fds(0);  // 0 = don't unset env vars
    if (n < 0) {
        fprintf(stderr, "sd_listen_fds failed: %s\n", strerror(-n));
        return 1;
    }

    // First passed fd is always SD_LISTEN_FDS_START (3)
    int listen_fd = SD_LISTEN_FDS_START;

    // Verify it's the right type
    if (sd_is_socket_unix(listen_fd, SOCK_STREAM, 1, "/run/myapp.sock", 0) <= 0) {
        fprintf(stderr, "unexpected socket type\n");
        return 1;
    }

    // Tell systemd we're ready (Type=notify)
    sd_notify(0, "READY=1");

    // Main loop: accept connections on listen_fd
    // ...

    // Report status
    sd_notify(0, "STATUS=Processing 42 connections");

    // Clean shutdown
    sd_notify(0, "STOPPING=1");
    return 0;
}

Compile with: gcc -lsystemd myapp.c -o myapp

Socket Unit + Service Unit

# /etc/systemd/system/myapp.socket
[Unit]
Description=My Application Socket

[Socket]
ListenStream=/run/myapp.sock
SocketMode=0660
SocketUser=myapp
SocketGroup=myapp

[Install]
WantedBy=sockets.target
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
Requires=myapp.socket

[Service]
Type=notify
ExecStart=/usr/bin/myapp
NotifyAccess=main
WatchdogSec=30

Writing systemd Units for a Wayland Kiosk

Session Target Chain

graph LR
    Multi["multi-user.target"] --> Sway["sway.service"]
    Sway --> ST["sway-session.target"]
    ST --> GST["graphical-session.target"]
    GST --> Kiosk["kiosk-chromium.service"]
    GST --> VNC["wayvnc.service"]

The Sway Session Target

Sway does not ship a systemd target by default. You must wire it up manually (or use sway-systemd):

# ~/.config/systemd/user/sway-session.target
[Unit]
Description=Sway Wayland Compositor Session
Documentation=man:sway(5)
BindsTo=graphical-session.target
Wants=graphical-session-pre.target
After=graphical-session-pre.target

In your Sway config, activate it:

# ~/.config/sway/config
exec systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP SWAYSOCK
exec systemctl --user start sway-session.target

Import before start

The import-environment and start commands must be in the correct order. If the target starts before WAYLAND_DISPLAY is imported, child services will fail to connect to the compositor.

Kiosk Chromium Service

# ~/.config/systemd/user/kiosk-chromium.service
[Unit]
Description=Kiosk Chromium on Sway
After=sway-session.target
BindsTo=sway-session.target

[Service]
Type=simple
ExecStart=/usr/bin/chromium \
    --kiosk \
    --ozone-platform=wayland \
    --noerrdialogs \
    --disable-extensions \
    --disable-translate \
    --no-first-run \
    --user-data-dir=/tmp/chromium-kiosk \
    https://agent.warmwind.internal
Restart=on-failure
RestartSec=2

# Hardening
NoNewPrivileges=yes
ProtectHome=read-only
PrivateTmp=yes
SystemCallFilter=@system-service @resources

[Install]
WantedBy=sway-session.target

WayVNC Service

# ~/.config/systemd/user/wayvnc.service
[Unit]
Description=WayVNC server
After=sway-session.target
BindsTo=sway-session.target

[Service]
Type=simple
ExecStart=/usr/bin/wayvnc --output=HEADLESS-1 0.0.0.0 5900
Restart=on-failure
RestartSec=2

[Install]
WantedBy=sway-session.target

Service Management

# Enable and start the full kiosk stack
systemctl --user enable kiosk-chromium.service wayvnc.service
systemctl --user start sway-session.target

# Check status
systemctl --user status kiosk-chromium.service
journalctl --user -u kiosk-chromium.service -f

# Reload after editing units
systemctl --user daemon-reload

Why D-Bus Matters for Warmwind

Warmwind's architecture has multiple interacting processes:

graph LR
    Agent["AI Agent"] -- "D-Bus signals" --> Broker["Session Broker"]
    Broker -- "Sway IPC" --> Sway["Sway"]
    Broker -- "D-Bus method_call" --> Chromium["Chromium"]
    Broker -- "logind" --> Login["logind"]
    Sway -- "Wayland protocols" --> VNC["WayVNC"]
Communication path Mechanism Why
Agent <-> Broker D-Bus session bus Structured IPC with introspection, signal subscription
Broker -> Sway Sway IPC ($SWAYSOCK) Window management commands
Broker -> logind D-Bus system bus Session lifecycle, device access
Compositor <-> VNC Wayland protocols Frame capture, input injection
Broker -> Chromium DevTools Protocol (CDP) or D-Bus Page navigation, JS injection

D-Bus provides the glue layer: the session broker subscribes to logind signals (session active/inactive), sends commands to the compositor, and coordinates the AI agent's browser session. Without D-Bus, you would need custom IPC for each pair of processes.

What's new (2025--2026)
  • systemd 256+ added ImportCredential= for injecting secrets into services without environment variables or files in the unit.
  • xdg-desktop-portal 1.19 improved the ScreenCast portal with better damage tracking and DMA-BUF support for zero-copy capture.
  • sd-bus (systemd's C D-Bus library) continues to be the recommended D-Bus implementation for new C code, replacing libdbus.

Glossary

D-Bus
Desktop Bus -- Linux IPC system using a message bus daemon. Two instances: system bus (root services) and session bus (per-user).
Well-known name
Reverse-DNS identifier claimed by a D-Bus service (e.g. org.freedesktop.login1). Allows clients to find services by name.
Object path
Hierarchical path to a specific D-Bus object within a service (e.g. /org/freedesktop/login1/session/c2).
logind
systemd's login manager. Manages sessions, seats, device access (TakeDevice/ReleaseDevice), and VT switching.
TakeDevice
logind D-Bus method that returns a file descriptor for a device (DRM, input). Enables rootless compositor operation.
Socket activation
systemd feature where the init system creates listening sockets and starts services on-demand when connections arrive.
sd_notify()
C function from libsystemd for services to report readiness (READY=1), status, and watchdog keepalives to systemd.
graphical-session.target
systemd user target representing an active graphical session. Compositor-specific targets (e.g. sway-session.target) bind to it.
XDG Desktop Portal
D-Bus service providing sandboxed access to desktop features (screen sharing, file picker, notifications) for Flatpak and other sandboxed apps.