From 0eb83b19ed075a07b86549d2938c4224ca1d5df6 Mon Sep 17 00:00:00 2001 From: Luke Shumaker Date: Sun, 28 Feb 2016 03:02:34 -0500 Subject: write an article about my X11/systemd configuration --- public/assets/style.css | 42 +++++- public/x11-systemd.md | 372 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 public/x11-systemd.md diff --git a/public/assets/style.css b/public/assets/style.css index 60e7f73..e653c21 100644 --- a/public/assets/style.css +++ b/public/assets/style.css @@ -42,7 +42,7 @@ var { color: #008800; } -pre { +pre, ul.tree { margin: auto 2em; padding: .5em; overflow: auto; @@ -55,6 +55,46 @@ pre hr { border-top: solid 1px #AAAAAA; } +/* lists that look like `tree` output */ +ul.tree { + font-family: monospace; + background: #DDDDFF; +} +ul.tree li { + list-style-type: none; +} +ul.tree ul { + padding-left: 0; +} +ul.tree ul li { + /* draw the vertical lines */ + margin-left: calc(0.5ch - 1px); + border-left: solid 1px black; + /* and indent 4 chars */ + padding-left: 3.5ch; /* 4ch - 0.5ch for the margin-left above */ +} +ul.tree ul li::before { + /* make a non-empty inline-block element */ + display: inline-block; + content: " "; /* a unicode non-breaking space */ + /* un-indent */ + margin-left: calc(-3.5ch - 1px); /* to match the padding-left above */ + border-left: solid 1px transparent; + /* draw the horizontal lines */ + border-bottom: solid 1px black; + top: 0; + height: 1ex; + width: 2.5ch; + margin-right: 1ch; +} +ul.tree ul li:last-child { + /* let the li::before psuedo-element draw the last part of the vertical line. */ + border-left: solid 1px transparent; +} +ul.tree ul li:last-child::before { + border-left: solid 1px black; +} + /* table elements */ table, td, th { diff --git a/public/x11-systemd.md b/public/x11-systemd.md new file mode 100644 index 0000000..6c37895 --- /dev/null +++ b/public/x11-systemd.md @@ -0,0 +1,372 @@ +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: + + + +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`: + + + +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 -- cgit v1.2.3