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):
- 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.
- 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.
- 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.
- 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. - 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.