A runit-style service supervisor for Linux in ~700 lines of C. One daemon
(hsmd) supervises everything from a single poll loop; one client (hsm)
controls it over a unix socket. Configuration is compile-time in config.h,
services are directories on disk. Run by a user it supervises that user's
session; run by root it supervises the system, and as pid 1 it is the init
system (hos boots this way).
A service is a subdirectory of the service directory (default
~/.config/hsm/sv) with an executable run script that runs the daemon in
the foreground:
~/.config/hsm/sv/syncthing/run:
#!/bin/sh
exec syncthing serve --no-browser
execso signals reach the real process, not a leftover shell.- A file named
downin the service directory means "don't start at boot". - An executable
finishruns after every exit with runit's arguments (exit code or -1, signal or 0) and holds the restart until it is done. - Service stdout/stderr land in
login the service directory, rotated tolog.0,log.1, ... pastlogsizebytes (logkeepold files are kept). The system supervisor writes them under/var/log/hsm/NAME/instead, because/etc/sv/*/logis a directory in runit's layout. - Existing runit run scripts work as-is, and so do runit's tools: hsmd
keeps runit's
supervise/directory per service, sosv status|check| up|down|restart|pause|cont NAMEfrom the runit package drive it unchanged (see below).
make && make install # symlinks into ~/.local/bin
hsmd # run in the foreground, e.g. from .xinitrc: hsmd &
hsm # status of all services
hsm up NAME # start (and keep restarting) a service
hsm down NAME # stop a service: SIGTERM, then SIGKILL after killwait
hsm restart NAME
hsm check NAME # exit 0 once NAME runs, waiting up to checkwait seconds
hsm rescan # pick up added/removed service directories (or: pkill -HUP hsmd)
hsm poweroff | reboot | halt # when hsmd is init
Status states: run (running), down (stopped on purpose), wait (died,
restart pending — a crash-looping service sits here between throttled
restarts), term (SIGTERM sent, waiting for it to die), finish (its
finish script is running).
Started by root, hsmd supervises /var/service (runit's layout: one
symlink per enabled service into /etc/sv) and listens on
/run/hsm.sock; hsm as root talks there. That is enough to replace
runsvdir as runit's stage 2. Void's run scripts wait for dependencies
with sv check dbus; runit's own sv reads and writes each service's
supervise/ directory (runsv's status file, the ok fifo that says a
supervisor is alive, the control fifo commands go into), and hsmd
maintains exactly that, so sv — and anything else built on runit's
layout — needs no shim. hsm check is the same wait over the socket.
Started as pid 1 (init=/usr/bin/hsmd on the kernel command line), hsmd
runs /etc/hsm/boot first (mounts, udev, fsck — hos reuses Void's
core-services), then supervises. hsm poweroff|reboot|halt, ctrl-alt-del
(reboot), SIGUSR1 (halt) and SIGUSR2/SIGTERM (poweroff) stop every service,
run /etc/hsm/shutdown with the action as its argument, and call
reboot(2). So do Void's own halt, poweroff, reboot and shutdown:
they run runit-init 0|6, which makes /etc/runit/stopit (and
/etc/runit/reboot for a reboot) executable and sends pid 1 SIGCONT —
runit's stage 3 hand-off, which hsmd honours. As init it never exits: a
fatal error leaves you in a rescue shell instead of a kernel panic. The
two scripts live in the hos overlay.
- Each service runs in its own session (
setsid), sodownsignals the whole process group — children included. - Signals arrive over a
signalfd, so there are no async signal handlers: everything is sequential in onepollloop over the signalfd and the control socket. - A service that exits is restarted, but never more than once per
throttleseconds, so a crashing service can't spin the CPU. hsmdis a child subreaper (prctl), so orphans of double-forking services are reaped instead of becoming init's problem.- Service output flows through a pipe held by hsmd rather than straight
into a file: that is what makes rotation possible while the service
runs (a child writing to its own fd would follow the renamed file
forever). The pipe outlives restarts, so grandchild output keeps
landing in the same log. Rotation itself lives in
log.c. - Stopping hsmd (SIGTERM/SIGINT) stops every service and waits for them before exiting.
- runit compatibility is a side effect of the state machine: after every
round
tick()rewrites a service'ssupervise/statusif the bytes derived from its state changed, and thecontrolfifo is just another fd in the poll loop.
Everything is in hsmd.c; the interesting parts are spawn()/start()
(fork/exec into a fresh session), reap() (the waitpid loop, restart
throttle and finish), tick() (the state machine: enforce
want-vs-state, compute the next poll timeout), handleclient() (the whole
wire protocol — it's just text lines), runscript() (the pid 1 boot
and shutdown scripts), and the runit side: opensupervise(),
writestatus(), handlecontrol(), handlestopit(). Knobs live in
config.h.
hsmd as pid 1 can be tried without a machine: run it as the init of a
user+pid namespace (unshare -Urm for the mounts, then
unshare -pf --mount-proc hsmd); reboot(2) there ends the namespace
instead of the host. hos's vmtest.py boots the real ISO under qemu with
a freshly built hsmd injected and drives it over the serial console.
Deliberately not here (yet): service dependencies, readiness notification, restart backoff.