summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Shumaker <lukeshu@sbcglobal.net>2016-02-28 03:02:34 -0500
committerLuke Shumaker <lukeshu@sbcglobal.net>2016-02-28 03:02:34 -0500
commit0eb83b19ed075a07b86549d2938c4224ca1d5df6 (patch)
tree20d2531d23ebc1b58d79188ebb5e7ddfbd394bdf
parent8f65e334b48fe892db0bd47eff4886b9e4db54c5 (diff)
write an article about my X11/systemd configuration
-rw-r--r--public/assets/style.css42
-rw-r--r--public/x11-systemd.md372
2 files changed, 413 insertions, 1 deletions
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:
+
+<ul class=tree><li>[.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][]
+ - doodle
+ + xresources@.service -> ../[xresources@.service][]
+
+</li></ul>
+
+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`:
+
+<ul class=tree><li>[.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
+
+</li></ul>
+
+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" <<<READY=1 || true
+
+But, I don't like that. I'd rather write my WMII configuration to the
+world as I wish it existed, and have workarounds encapsulated
+elsewhere;
+["If you have to cut corners in your project, do it inside the implementation, and wrap a very good interface around it."][interfaces].
+So, I wrote a `systemd-notify` compatible
+[function][wmii/workarounds.sh] that ultimately calls `socat`:
+
+ ##
+ # Just like systemd-notify(1), but slower, which is a shitty
+ # workaround for a race condition in systemd.
+ ##
+ systemd-notify() {
+ local args
+ args="$(getopt -n systemd-notify -o h -l help,version,ready,pid::,status:,booted -- "$@")"
+ ret=$?; [[ $ret == 0 ]] || return $ret
+ eval set -- "$args"
+
+ local arg_ready=false
+ local arg_pid=0
+ local arg_status=
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -h|--help) command systemd-notify --help; return $?;;
+ --version) command systemd-notify --version; return $?;;
+ --ready) arg_ready=true; shift 1;;
+ --pid) arg_pid=${2:-$$}; shift 2;;
+ --status) arg_status=$2; shift 2;;
+ --booted) command systemd-notify --booted; return $?;;
+ --) shift 1; break;;
+ esac
+ done
+
+ local our_env=()
+ if $arg_ready; then
+ our_env+=("READY=1")
+ fi
+ if [[ -n "$arg_status" ]]; then
+ our_env+=("STATUS=$arg_status")
+ fi
+ if [[ "$arg_pid" -gt 0 ]]; then
+ our_env+=("MAINPID=$arg_pid")
+ fi
+ our_env+=("$@")
+ local n
+ printf -v n '%s\n' "${our_env[@]}"
+ socat STDIO UNIX-SENDTO:"$NOTIFY_SOCKET" <<<"$n"
+ }
+
+So, one day when the systemd bugs have been fixed (and presumably the
+Linux kernel supports passing the cgroup of a process as part of its
+credentials), I can remove that from `workarounds.sh`, and not have to
+touch anything else in my WMII configuration (I do use `systemd-notify` in a
+couple of other, non-essential, places too; this wasn't to avoid
+having to change just 1 line).
+
+So, now that `wmii@.service` properly has `Type=notify`, I can just
+stick `After=wmii@.service` into my `lxpanel@.service`, right? Wrong!
+Well, I _could_, but my `lxpanel` service has nothing to do with WMII;
+why should I couple them? Instead, I create
+[`wm-running@.target`][wm-running@.target] that can be used as a
+synchronization point:
+
+ # wmii@.service
+ Before=wm-running@%i.target
+
+ # lxpanel@.service
+ After=X11@%i.target wm-running@%i.target
+ Requires=wm-running@%i.target
+
+Finally, I have my desktop started and running. Now, I'd like for
+programs that aren't part of the window manager to not dump their
+stdout and stderr into WMII's part of the journal, like to have a
+record of which graphical programs crashed, and like to have a
+prettier cgroup/process graph. So, I use `systemd-run` to run
+external programs from the window manager:
+
+ runcmd() (
+ ...
+ unset NOTIFY_SOCKET # systemd
+ ...
+ exec 8>&- # 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