Moving to OpenRC's user services


This Sunday was highlighted by a sudden need to hack on my dotfiles. Specifically, trying to move as much as possible from my .xinitrc and my custom clean shutdown script to OpenRC's brand new user services (well, stable since May 2025).

Pipewire §

First obvious target and the main reason the OpenRC devs (well, mostly @navi-desu, sincere thanks) started working on it, since it was made with systemd in mind all along (so it provides service files for it).

As the wiki said, it was as simple as this:

$ rc-update add --user pipewire default
$ rc-update add --user pipewire-pulse default
$ rc-update add --user wireplumber default
$ sed -i '/gentoo-pipewire-launch/d' ~/.xinitrc

Just Werks™, one less hack needed.

D-Bus §

Not yet tested with a reboot and clearly less smooth than PipeWire:

$ rc-update --user add dbus default
$ edit ~/.xinitrc
# Replace `eval $(dbus-launch --sh-syntax --exit-with-session)`
# with
# export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/bus"
# https://wiki.gentoo.org/wiki/D-Bus#The_session_bus

FUSE unmounting §

Some stuff doesn't have any user script already premade, so open up your can of elbow grease! For example, orderly unmounting of FUSE mounts before shutdown (e.g. sshfs, gocryptfs):

$ cat ~/.config/rc/init.d/fuse-cleanup
#!/sbin/openrc-run

start() { true; }

stop()
{
    local status=0
    mount |
        awk '$5 ~ /^fuse\..*/ {print $3}'
        while IFS= read -r mp
        do
            ebegin "Unmounting $mp..."
            fusermount3 -u "$mp"
            eend $? || status=1
        done
    return $status
}

Easy peasy lemon squeezy.

Music player §

Last item and a bit more painful for me, because I use my own and this migration required me to improve the IPC method used between it and its graphical interface (a lemonbar script) to allow fully asynchronous message sending.

But what relatively portable UNIX tool (available from the shell) is there for a message queue that can buffer writes before the reader is online? UNIX sockets are out. Good ol' files (preferably on tmpfs) locked via flock is a sensible solution I've used in the past, but because I wanted efficient blocking instead of polling on the listener side, I stayed with my named pipes.

The key is then to always have a dummy reader (NB: that doesn't actually read anything) on the pipe to avoid blocking writes/ EPIPE/SIGPIPE when there's no reader on the receiving end and a dummy writer to never get EOF when the last writer is gone.

Quite simple, the side that appears first creates the pipe and the dummy process. In sh:

#!/bin/sh
set -eu

fifo=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/mus_status.fifo
! [ -e "$fifo" ] && {
    mkfifo -m 600 "$fifo"
    sh -c '
        exec 3<>"$1"
        while [ -e "$1" ]; do sleep 1; done
    ' "$fifo dummy reader/writer" "$fifo" &
}

cat >"$fifo"

And then it's just a matter of using OpenRC's supervise-daemon to babysit mus_daemon:

$ cat ~/.config/rc/init.d/mus
#!/sbin/openrc-run

depend(){
    need pipewire
}

description="mus daemon"
supervisor=supervise-daemon
supervise_daemon_args="-I 2:0 -N -10 -2 ${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/mus.log"
command="/usr/local/bin/mus_daemon"
$ cat ~/.config/rc/conf.d/mus
command_args="-s ~/Programming/lemonbar-tcl/mus_status.sh"