Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

hsm — hackable service manager

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).

Service contract (same as runit)

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
  • exec so signals reach the real process, not a leftover shell.
  • A file named down in the service directory means "don't start at boot".
  • An executable finish runs 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 log in the service directory, rotated to log.0, log.1, ... past logsize bytes (logkeep old files are kept). The system supervisor writes them under /var/log/hsm/NAME/ instead, because /etc/sv/*/log is 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, so sv status|check| up|down|restart|pause|cont NAME from the runit package drive it unchanged (see below).

Usage

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).

As the system supervisor and as init

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.

How it works

  • Each service runs in its own session (setsid), so down signals the whole process group — children included.
  • Signals arrive over a signalfd, so there are no async signal handlers: everything is sequential in one poll loop over the signalfd and the control socket.
  • A service that exits is restarted, but never more than once per throttle seconds, so a crashing service can't spin the CPU.
  • hsmd is 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's supervise/status if the bytes derived from its state changed, and the control fifo is just another fd in the poll loop.

Hacking

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.

About

Runit-style service supervisor in C (daemon hsmd + client hsm)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages