Using systemd for X sessions

Recently I wrote an article about starting X sessions in Debian. One of stages of Xsession is running ~/.xsessionrc script, which, according to xsession(5) manual should be a source of global environment variables. It can be, however, a convenient place for keeping all programs which should start together with X session - because, generally speaking, it is executed for every session type, no matter what. I used this approach for several years and, for reasons highlighted later, I find it suboptimal. In this article I’d like to explore another possibility - using systemd user mode for managing user X sessions.

xsessionrc approach

Our xsessionrc can look like this:

#!/bin/bash
xrdb - merge "$HOME/.Xresources

mpd &
xterm -e ncmpcpp &

firefox &
thunderbird &

There are several problems with such approach (and, to be fair, with any simple script used to run a session):

  1. Programs often depend on each other. For example ncmpcpp (client of MPD) doesn’t make sense to start before MPD itself, but MPD might delay its start for any reason or even not start at all e.g. due to configuration error.
  2. Some programs (like MPD) won’t automatically exit with user session. If session is restarted, it will cause problems, because they might use resources (like sockets) which are already used their other instances.
  3. If programs break you have to manually restart it - which usually means grepping through xsessionrc in search of exact set of switches used for its invocation. I’m sorry, I don’t remember my longitude and latitude passed to redshift.
  4. Programs start simultaneously and they all write their output to ~/.xsession-errors at the same time. If that’s not enough, GTK programs are typically VERY verbose and soon finding anything in xsession-errors becomes a horror story. Sure, you can do something like mpd >mpd.out 2>mpd.err, but in practice I’ve never heard about anyone doing something like that.
  5. Probably it’s yet another mechanism which manages session. For example, i3 can also start programs from inside its configuration file and most desktop environments autostarts programs depending on their own sets of rules. I like having a choice, but I hate using several mechanisms at once, each with its own set of quirks and design choices.

That’s why in the back of my had I had this idea of creating a uniform system for managing my login sessions. I already used systemd for some programs (especially for some in-house scripts at work, which tend to break every now and then so systemd auto-restarts them) and was quite happy with it, so I thougt to give it a try and manage all of my session startup this way.

systemd approach

First of all, I don’t want to reconfigure my whole system and right now Xsession script is rather tightly coupled with the rest of Debian. If I ever have to configure X session on a new computer, I want to simply symlink some files with GNU Stow (you can read about how I approach my configuration in dotfiles series). Besides, I’d be rewriting big parts of Xsession anyway, because it out-of-the-box contains some very handy bits (like injecting environment to systemd via DBus, which usually is a separate topic and in Debian we have it for free).

Long story short: I modified ~/.xsession to contain exactly 2 lines:

#!/bin/sh
systemctl --user start --wait xsession.target

Above invocation of systemctl can be of course placed in a *.desktop file. Just keep in mind that it’s important to use --wait flag, which disallows immediate exit, which in turn would stop X session right after it.

xsession.target looks like this:

[Unit]
Description=X session managed by systemd
BindsTo=graphical-session.target

xsession.target is a synchronization point. User units (started with systemctl --user) cannot wait for targets from system-wide systemctl calls, like graphical.target, sound.target etc. That’s because user units are spawned in a separate daemon, spawned for each user, which has no knowledge of other systemd instances. By hooking into built-in Xsession mechanism we ensured that when xsession.target becomes active, all resources needed by graphical programs ($DISPLAY and $XAUTHORITY) are available.

xsession.target binds to a special target: graphical-session.target, which is active as long as any other active unit requires it. It also acts as an alias for any graphical session (such as GNOME, KDE, i3, awesome, …): other units, which are part of X session should contain PartOf=graphical-session.target. This way they’ll be stopped when graphical-session.target stops. They also don’t need to be changed if i3wm were to be replaced with e.g. some other window manager.

Example unit started in session looks like this:

[Unit]
Description=Compton compositor for X11
PartOf=graphical-session.target

[Service]
ExecStart=/usr/bin/compton --config "%h/.config/compton/compton.conf"

[Install]
WantedBy=graphical-session.target

xsession.target is explicitly required by only one other unit: i3wm.service, which handles starting and stopping window manager:

[Unit]
Description=i3 Window Manager
PartOf=graphical-session.target

[Service]
ExecStart=/usr/local/bin/i3
ExecStopPost=/bin/systemctl --user stop graphical-session.target
Restart=on-failure

[Install]
RequiredBy=xsession.target

Note that xsession.target itself doesn’t require anything by itself (via Requires=). That’s because I prefer to add and remove programs to autostart via systemctl enable and systemctl disable instead of editing systemd unit files.

Another interesting part is ExecStopPost=, which stops graphical-session (and all of its parts) whenever i3 quits. To quit a session, graphical-session.target must be stopped one way or another and I decided to keep a behaviour that I’m familiar with: window manager acts as a session’s master and whenever it quits, the whole session is killed as well - it didn’t always work for me before, but it’s one of the features of systemd.

tmux

Killing the whole session has some quirks though. When I log out, systemd kills tmux server.

I like the way systemd works because it’s easy enough to mark specific programs which should be kept after session ends and by default I don’t want most of daemons to survive logging out. For instance: if I run HTTP server, then it’s purposefully run to display some personal pages and it has no purpose to exist after I log out. If I run syncthing, I want it to operate only when I’m logged to my system. If I wanted a daemon to run all the time, then it’s a system service and should be run via systemctl without --user switch (syncthing for example ships with systemd units which support exactly that use case - useful for infrastructure with a single “master” syncthing server).

However, programs like tmux and screen are special, because they’re specifically designed to daemonize to survive user sessions. This can be achieved by running them through systemd-run --remain-after-exit or even systemd-run --scope --user (which should finally work in recent systemd versions).

To do this automatically, tmux can be aliased:

tmux() {
    systemd-run --scope --user tmux "$@"
}

And basically this is everything. Just put all _.service_ files in ~/.config/systemd/user* and enable/disable the ones which you want to autostart by systemctl --user enable/disable. It’s super convienient, because you don’t have to remember exact commands, just run systemctl --user enable redshift.service.

Source code presented in this article is also available in git repository, so take a look if it makes more sense to you.