My X11 setup with systemd
=========================
---
date: "2016-02-28"
---
Somewhere along the way, I decided to use systemd user sessions to
manage the various parts of my X11 environment would be a good idea.
If that was a good idea or not... we'll see.
I've sort-of been running this setup as my daily-driver for
[a bit over a year][firstcommit], continually tweaking it though.
My setup is substantially different than the one on [ArchWiki],
because the ArchWiki solution assumes that there is only ever one X
server for a user; I like the ability to run `Xorg` on my real
monitor, and also have `Xvnc` running headless, or start my desktop
environment on a remote X server. Though, I would like to figure out
how to use systemd socket activation for the X server, as the ArchWiki
solution does.
This means that all of my graphical units take `DISPLAY` as an `@`
argument. To get this to all work out, this goes in each `.service`
file, unless otherwise noted:
[Unit]
After=X11@%i.target
Requisite=X11@%i.target
[Service]
Environment=DISPLAY=%I
We'll get to `X11@.target` later, what it says is "I should only be
running if X11 is running".
I eschew complex XDMs or `startx` wrapper scripts, opting for the more
simple `xinit`, which I either run on login for some boxes (my media
station), or type `xinit` when I want X11 on others (most everything
else). Essentially, what `xinit` does is run `~/.xserverrc` (or
`/etc/X11/xinit/xserverrc`) to start the server, then once the server
is started (which it takes a substantial amount of magic to detect) it
runs run `~/.xinitrc` (or `/etc/X11/xinit/xinitrc`) to start the
clients. Once `.xinitrc` finishes running, it stops the X server and
exits. Now, when I say "run", I don't mean execute, it passes each
file to the system shell (`/bin/sh`) as input.
Xorg requires a TTY to run on; if we log in to a TTY with `logind`, it
will give us the `XDG_VTNR` variable to tell us which one we have, so
I pass this to `X` in [my `.xserverrc`][X11/serverrc]:
#!/hint/sh
if [ -z "$XDG_VTNR" ]; then
exec /usr/bin/X -nolisten tcp "$@"
else
exec /usr/bin/X -nolisten tcp "$@" vt$XDG_VTNR
fi
This was the default for [a while][arch-addvt] in Arch, to support
`logind`, but was [later removed][arch-delvt] in part because `startx`
(which calls `xinit`) started adding it as an argument as well, so
`vt$XDG_VTNR` was being listed as an argument twice, which is an
error. IMO, that was a problem in `startx`, and they shouldn't have
removed it from the default system `xserverrc`, but that's just me.
So I copy/pasted it into my user `xserverrc`.
That's the boring part, though. Where the magic starts happening is
in [my `.xinitrc`][X11/clientrc]:
#!/hint/sh
if [ -z "$XDG_RUNTIME_DIR" ]; then
printf "XDG_RUNTIME_DIR isn't set\n" >&2
exit 6
fi
_DISPLAY="$(systemd-escape -- "$DISPLAY")"
trap "rm -f $(printf '%q' "${XDG_RUNTIME_DIR}/x11-wm@${_DISPLAY}")" EXIT
mkfifo "${XDG_RUNTIME_DIR}/x11-wm@${_DISPLAY}"
cat < "${XDG_RUNTIME_DIR}/x11-wm@${_DISPLAY}" &
systemctl --user start "X11@${_DISPLAY}.target" &
wait
systemctl --user stop "X11@${_DISPLAY}.target"
There are two contracts/interfaces here: the `X11@DISPLAY.target`
systemd target, and the `${XDG_RUNTIME_DIR}/x11-wm@DISPLAY` named
pipe. The systemd `.target ` should be pretty self explanatory; the
most important part is that it starts the window manager. The named
pipe is just a hacky way of blocking until the window manager exits
("traditional" `.xinitrc` files end with the line `exec
your-window-manager`, so this mimics that behavior). It works by
assuming that the window manager will open the pipe at startup, and
keep it open (without necessarily writing anything to it); when the
window manager exits, the pipe will get closed, sending EOF to the
`wait`ed-for `cat`, allowing it to exit, letting the script resume.
The window manager (WMII) is made to have the pipe opened by executing
it this way in [its `.service` file][wmii@.service]:
ExecStart=/usr/bin/env bash -c 'exec 8>${XDG_RUNTIME_DIR}/x11-wm@%I; exec wmii'
which just opens the file on file descriptor 8, then launches the
window manager normally. The only further logic required by the
window manager with regard to the pipe is that in the window manager
[configuration][wmii/config.sh], I should close that file descriptor
after forking any process that isn't "part of" the window manager:
runcmd() (
...
exec 8>&- # xinit/systemd handshake
...
)
So, back to the `X11@DISPLAY.target`; I configure what it "does" with
symlinks in the `.requires` and `.wants` directories:
- [.config/systemd/user/][systemd/user]
* [X11@.target][]
* [X11@.target.requires][]/
+ wmii@.service -> ../[wmii@.service][]
* [X11@.target.wants][]/
+ xmodmap@.service -> ../[xmodmap@.service][]
+ xresources-dpi@.service -> ../[xresources-dpi@.service][]
+ xresources@.service -> ../[xresources@.service][]
The `.requires` directory is how I configure which window manager it
starts. This would allow me to configure different window managers on
different displays, by creating a `.requires` directory with the
`DISPLAY` included, e.g. `X11@:2.requires`.
The `.wants` directory is for general X display setup; it's analogous
to `/etc/X11/xinit/xinitrc.d/`. All of the files in it are simple
`Type=oneshot` service files. The [xmodmap][xmodmap@.service] and
[xresources][xresources@.service] files are pretty boring, they're
just systemd versions of the couple lines that just about every
traditional `.xinitrc` contains, the biggest difference being that
they look at [`~/.config/X11/modmap`][X11/modmap] and
[`~/.config/X11/resources`][X11/resources] instead of the traditional
locations `~/.xmodmap` and `~/.Xresources`.
What's possibly of note is
[`xresources-dpi@.service`][xresources-dpi@.service]. In X11, there
are two sources of DPI information, the X display resolution, and the
XRDB `Xft.dpi` setting. It isn't defined which takes precedence (to
my knowledge), and even if it were (is), application authors wouldn't
be arsed to actually do the right thing. For years, Firefox (well,
Iceweasel) happily listened to the X display resolution, but recently
it decided to only look at `Xft.dpi`, which objectively seems a little
silly, since the X display resolution is always present, but `Xft.dpi`
isn't. Anyway, Mozilla's change drove me to to create a
[script][xrdb-set-dpi] to make the `Xft.dpi` setting match the X
display resolution. Disclaimer: I have no idea if it works if the X
server has multiple displays (with possibly varying resolution).
#!/usr/bin/env bash
dpi=$(LC_ALL=C xdpyinfo|sed -rn 's/^\s*resolution:\s*(.*) dots per inch$/\1/p')
xrdb -merge <<<"Xft.dpi: ${dpi}"
Since we want XRDB to be set up before any other programs launch, we
give both of the `xresources` units `Before=X11@%i.target` (instead of
`After=` like everything else). Also, two programs writing to `xrdb`
at the same time has the same problem as two programs writing to the
same file; one might trash the other's changes. So, I stuck
`Conflicts=xresources@:i.service` into `xresources-dpi.service`.
And that's the "core" of my X11 systemd setup. But, you generally
want more things running than just the window manager, like a desktop
notification daemon, a system panel, and an X composition manager
(unless your window manager is bloated and has a composition manager
built in). Since these things are probably window-manager specific,
I've stuck them in a directory `wmii@.service.wants`:
- [.config/systemd/user/][systemd/user]
* [wmii@.service.wants][]/
+ dunst@.service -> ../[dunst@.service][] # a notification daemon
+ lxpanel@.service -> ../[lxpanel@.service][] # a system panel
+ rbar@97_acpi.service -> ../[rbar@.service][] # wmii stuff
+ rbar@99_clock.service -> ../[rbar@.service][] # wmii stuff
+ xcompmgr@.service -> ../[xcompmgr@.service][] # an X composition manager
For the window manager `.service`, I _could_ just say `Type=simple`
and call it a day (and I did for a while). But, I like to have
`lxpanel` show up on all of my WMII tags (desktops), so I have
[my WMII configuration][wmii/config.sh] stick this in the WMII
[`/rules`][wmii/rules]:
/panel/ tags=/.*/ floating=always
Unfortunately, for this to work, `lxpanel` must be started _after_
that gets inserted into WMII's rules. That wasn't a problem
pre-systemd, because `lxpanel` was started by my WMII configuration,
so ordering was simple. For systemd to get this right, I must have a
way of notifying systemd that WMII's fully started, and it's safe to
start `lxpanel`. So, I stuck this in
[my WMII `.service` file][wmii@.service]:
# This assumes that you write READY=1 to $NOTIFY_SOCKET in wmiirc
Type=notify
NotifyAccess=all
and this in [my WMII configuration][wmii/wmiirc]:
systemd-notify --ready || true
Now, this setup means that `NOTIFY_SOCKET` is set for all the children
of `wmii`; I'd rather not have it leak into the applications that I
start from the window manager, so I also stuck `unset NOTIFY_SOCKET`
after forking a process that isn't part of the window manager:
runcmd() (
...
unset NOTIFY_SOCKET # systemd
...
exec 8>&- # xinit/systemd handshake
...
)
Unfortunately, because of a couple of [bugs][sd-slash] and
[race conditions][sd-esrch] in systemd, `systemd-notify` isn't
reliable. If systemd can't receive the `READY=1` signal from my WMII
configuration, there are two consequences:
1. `lxpanel` will never start, because it will always be waiting for
`wmii` to be ready, which will never happen.
2. After a couple of minutes, systemd will consider `wmii` to be
timed out, which is a failure, so then it will kill `wmii`, and
exit my X11 session. That's no good!
Using `socat` to send the message to systemd instead of
`systemd-notify` "should" always work, because it tries to read from
both ends of the bi-directional stream, and I can't imagine that
getting EOF from the `UNIX-SENDTO` end will ever be faster than the
systemd manager from handling the datagram that got sent. Which is to
say, "we work around the race condition by being slow and shitty."
socat STDIO UNIX-SENDTO:"$NOTIFY_SOCKET" <<&- # xinit/systemd handshake
exec systemd-run --user --scope -- sh -c "$*"
)
I run them as a scope instead of a service so that they inherit
environment variables, and don't have to mess with getting `DISPLAY`
or `XAUTHORITY` into their units (as I _don't_ want to make them
global variables in my systemd user session).
I'd like to get `lxpanel` to also use `systemd-run` when launching
programs, but it's a low priority because I don't really actually use
`lxpanel` to launch programs, I just have the menu there to make sure
that I didn't break the icons for programs that I package (I did that
once back when I was Parabola's packager for Iceweasel and IceCat).
And that's how I use systemd with X11.
[ArchWiki]: https://wiki.archlinux.org/index.php/Systemd/User
[interfaces]: http://blog.robertelder.org/interfaces-most-important-software-engineering-concept/
[sd-esrch]: https://github.com/systemd/systemd/issues/2737
[sd-slash]: https://github.com/systemd/systemd/issues/2739
[arch-addvt]: https://projects.archlinux.org/svntogit/packages.git/commit/trunk/xserverrc?h=packages/xorg-xinit&id=f9f5de58df03aae6c8a8c8231a83327d19b943a1
[arch-delvt]: https://projects.archlinux.org/svntogit/packages.git/commit/trunk/xserverrc?h=packages/xorg-xinit&id=5a163ddd5dae300e7da4b027e28c37ad3b535804
[firstcommit]: https://lukeshu.com/git/dotfiles.git/commit/?id=a9935b7a12a522937d91cb44a0e138132b555e16
[X11/clientrc]: https://lukeshu.com/git/dotfiles.git/tree/.config/X11/clientrc
[X11/modmap]: https://lukeshu.com/git/dotfiles.git/tree/.config/X11/modmap
[X11/resources]: https://lukeshu.com/git/dotfiles.git/tree/.config/X11/resources
[X11/serverrc]: https://lukeshu.com/git/dotfiles.git/tree/.config/X11/serverrc
[wmii/config.sh]: https://lukeshu.com/git/dotfiles.git/tree/.config/wmii-hg/config.sh
[wmii/rules]: https://lukeshu.com/git/dotfiles.git/tree/.config/wmii-hg/rules
[wmii/wmiirc]: https://lukeshu.com/git/dotfiles.git/tree/.config/wmii-hg/wmiirc
[wmii/workarounds.sh]: https://lukeshu.com/git/dotfiles.git/tree/.config/wmii-hg/workarounds.sh
[xrdb-set-dpi]: https://lukeshu.com/git/dotfiles/tree/.local/bin/xrdb-set-dpi
[X11@.target.requires]: https://lukeshu.com/git/dotfiles/tree/.config/systemd/user/X11@.target.requires
[X11@.target.wants]: https://lukeshu.com/git/dotfiles/tree/.config/systemd/user/X11@.target.wants
[X11@.target]: https://lukeshu.com/git/dotfiles/tree/.config/systemd/user/X11@.target
[dunst@.service]: https://lukeshu.com/git/dotfiles/tree/.config/systemd/user/dunst@.service
[lxpanel@.service]: https://lukeshu.com/git/dotfiles/tree/.config/systemd/user/lxpanel@.service
[rbar@.service]: https://lukeshu.com/git/dotfiles/tree/.config/systemd/user/rbar@.service
[systemd/user]: https://lukeshu.com/git/dotfiles/tree/.config/systemd/user
[wmii@.service.wants]: https://lukeshu.com/git/dotfiles/tree/.config/systemd/user/wmii@.service.wants
[wmii@.service]: https://lukeshu.com/git/dotfiles/tree/.config/systemd/user/wmii@.service
[xcompmgr@.service]: https://lukeshu.com/git/dotfiles/tree/.config/systemd/user/xcompmgr@.service
[xmodmap@.service]: https://lukeshu.com/git/dotfiles/tree/.config/systemd/user/xmodmap@.service
[xresources-dpi@.service]: https://lukeshu.com/git/dotfiles/tree/.config/systemd/user/xresources-dpi@.service
[xresources@.service]: https://lukeshu.com/git/dotfiles/tree/.config/systemd/user/xresources@.service
[wm-running@.target]: https://lukeshu.com/git/dotfiles/tree/.config/systemd/user/wm-running@.target