# 92-daemontools.test -- daemontools 0.76 (meta-oe recipes-support, D. J. Bernstein). # # daemontools is a PROCESS SUPERVISOR, so this suite is only incidentally about # 17 small programs: it is a test of the wasm32 kernel's process-lifecycle path, # with supervise as the instrument. supervise forks a service, holds it open # through a flock'd lockfile, takes commands on a FIFO, signals the child on # request, reaps it with wait(2) and restarts it. Every one of those is a # separate platform question, and this image answers them differently. # # Sits at 92 next to 92-dumb-init.test on purpose -- dumb-init is the minimal # init/reaper, daemontools is the same subject with a control channel and a # restart policy. 76-daemonize.test measured DETACHMENT (fork/setsid/execvp) on # this kernel; this suite measures what happens to a child AFTER it is running. # # --------------------------------------------------------------------------- # THREE PLATFORM CONSTRAINTS SHAPE EVERY CASE BELOW # --------------------------------------------------------------------------- # * mkfifo(2) IS REFUSED ON THE VIRTIOFS ROOT -- the host-directory store # reports "only regular files are representable" (a stated limitation of that # backend, not a defect; first measured by 85-psmisc). supervise creates # supervise/control and supervise/ok as FIFOs, so a service directory can # only live on tmpfs. Everything here is staged under ${TMPDIR:-/tmp}, and # the refusal itself is asserted as its own case so the constraint is # recorded rather than merely worked around. # * sleep(1) IS A NO-OP on this target (and timeout(1) never fired before the # brintos/linux#77 fix), so nothing here sleeps. Waiting is a bounded poll # of /proc/uptime through shell builtins, the idiom 76-daemonize.test # established (an iteration forks nothing). # * `cmd & wait` HANGS on this image (job control, not this package -- see # 99-inotify-tools.test), so supervise is backgrounded and NOTHING here calls # wait. supervise is shut down through its own control FIFO (`svc -x`), which # needs no signal at all. # # --------------------------------------------------------------------------- # WHY THE SIGNAL CASES ARE SHAPED THE WAY THEY ARE # --------------------------------------------------------------------------- # brintos/linux#21 is open: signal delivery is observed only when the target # task crosses a dispatch boundary, so a task that blocks is not reliably # signalled, and a lost SIGCHLD leaves a supervisor believing a dead child is # still up (that exact shape is recorded on #21 from inetd's dgram re-arm). # supervise depends on SIGCHLD for its restart trigger and on kill(2) for # `svc -d`, so both are on the line here. # # Rather than guess which way this base answers, the suite MEASURES IT FIRST and # attributes afterwards. `daemontools/plat-signal-control` is a NULL CONTROL that # involves no daemontools code at all -- a plain backgrounded reader, one # SIGTERM, a bounded wait. It is the case to read first: if it is red, every # kill-dependent case below is held by #21 and says nothing about this port, and # if it is GREEN then a red in svc -d is daemontools' own and worth chasing. # # The restart property is measured twice for the same reason, and the pair is # the point: # daemontools/restart-async -- a service exits; is it restarted with no # prompting? This is the SIGCHLD question. # daemontools/restart-nudged -- the same, after one idempotent control byte # has forced supervise through a loop iteration. # This is the SUPERVISION LOGIC question, and it # is answerable whatever #21 does, because a # write to the control FIFO is ordinary I/O. # A supervisor whose logic is sound on a kernel that drops its wakeups gives # green/red; one that is broken in itself gives red/red. One case could not tell # those apart. # # WHAT THAT PAIR ACTUALLY FOUND, on 2026-08-16, is worth recording because it is # not what the paragraph above expected. The null control came back GREEN -- this # base signals a blocked task fine -- and both restart cases came back RED # together, which is the "broken in itself" answer rather than the #21 one. It # was: sig.h declares the handler type unprototyped, daemontools passes # zero-argument handlers to sigaction, and wasm32's type-checked indirect call # traps on the first SIGCHLD, killing supervise outright. Fixed in this port as # patch 0002 (BRINTOS-UPSTREAM-FIX). Every case in sections D and E is green # after it. The three-way discrimination is kept exactly as it stands, because # it is what produced that answer instead of a plausible misattribution to #21 -- # which is what a suite with only `restart-async` in it would have shipped. # # --------------------------------------------------------------------------- # WHAT IS NOT COVERED, AND WHY # --------------------------------------------------------------------------- # setuidgid and envuidgid are present and exercised only as far as their # argument handling: brintos/linux#19 has the whole credential-setter family # unwired (setgid(2) is ENOSYS; nothing on this image can drop privilege, busybox # `su` fails identically), so a case asserting that they DO switch user would be # asserting the platform gap rather than the package. svscanboot is installed but # not run -- it is the /etc/inittab entry, hardcodes /command and /service, and # would take over the machine's process table mid-suite. # # BST-TIMEOUT: 600 # ~14 bounded waits; the default 300 leaves no headroom on a # # loaded host, and every wait here is a poll rather than a sleep . "$BST_LIB" require supervise daemontools/present W=${TMPDIR:-/tmp}/bst-dt.$$ mkdir -p "$W" 2>/dev/null || { bst_notok daemontools/workdir "cannot mkdir $W"; exit 0; } # --- helpers --------------------------------------------------------------- # wait_for : block until exists, bounded. Builtin-only. wait_for() { read _u _r < /proc/uptime; _s0=${_u%.*} while [ ! -e "$1" ]; do read _u _r < /proc/uptime; _n=${_u%.*} [ $((_n - _s0)) -ge "$2" ] && return 1 done return 0 } # wait_data : like wait_for, but waits for non-empty CONTENT. wait_data() { read _u _r < /proc/uptime; _s0=${_u%.*} while [ ! -s "$1" ]; do read _u _r < /proc/uptime; _n=${_u%.*} [ $((_n - _s0)) -ge "$2" ] && return 1 done return 0 } # wait_gone : wait until has exited (gone, or a zombie -- # an orphan is not reaped on this target, brintos/linux#25, so its directory # outlives it and "until /proc/ disappears" would burn the whole budget). # kill -0 is NOT used as the liveness test: it answers 0 for a pid that never # existed on this kernel (brintos/linux#21), so it cannot detect death at all. wait_gone() { read _u _r < /proc/uptime; _s0=${_u%.*} while [ -r "/proc/$1/stat" ]; do read _p _c _st _rest < "/proc/$1/stat" 2>/dev/null [ "$_st" = Z ] && return 0 read _u _r < /proc/uptime; _n=${_u%.*} [ $((_n - _s0)) -ge "$2" ] && return 1 done return 0 } # wait_svstat : poll svstat until its output contains # . SV_OUT holds the last reading either way, so a failing case can # report what it actually saw rather than "timed out". wait_svstat() { read _u _r < /proc/uptime; _s0=${_u%.*} while :; do SV_OUT=$(svstat "$1" 2>&1) case "$SV_OUT" in *"$2"*) return 0 ;; esac read _u _r < /proc/uptime; _n=${_u%.*} [ $((_n - _s0)) -ge "$3" ] && return 1 done } # svpid : print the pid svstat reports, or nothing when down. svpid() { svstat "$1" 2>/dev/null | sed -n 's/.*up (pid \([0-9]*\)).*/\1/p'; } # nlines : line count without forking wc. nlines() { _n=0 # BST-REDIRECT-OK: its callers pass $W/*, under $TMPDIR. while read _l; do _n=$((_n + 1)); done < "$1" 2>/dev/null echo "$_n" } # =========================================================================== # A. PRESENCE -- all 17 commands package/commands names # =========================================================================== bst_ok daemontools/present _missing= for _c in svscan svscanboot supervise svc svok svstat fghack pgrphack \ readproctitle multilog tai64n tai64nlocal softlimit setuidgid \ envuidgid envdir setlock; do [ -x "/usr/bin/$_c" ] || _missing="$_missing $_c" done if [ -z "$_missing" ]; then bst_ok daemontools/all-commands else bst_notok daemontools/all-commands "not installed:$_missing"; fi # The tools take no --version and no --help, so the cheapest proof that a # ~2 MB wasm module loads and runs at all is its usage path. WHICH INVOCATION # REACHES IT DIFFERS PER TOOL, and getting that wrong is a case that passes or # fails for reasons having nothing to do with the port: # supervise dies on a missing argument -> a bare call reaches usage # svc only prints usage on a BAD OPTION; a bare `svc` parses no # options, iterates an empty directory list and exits 0 SILENTLY # svstat has no usage path at all in 0.76 -- `while (dir = *argv++)` # with no argv is simply an empty loop, exit 0, no output # So svc is driven with an invalid option, and svstat's genuine no-argument # behaviour is pinned as itself rather than asserted to be something it is not. check_out daemontools/supervise-usage "supervise: usage" sh -c 'supervise 2>&1; exit 0' check_out daemontools/svc-usage "svc options" sh -c 'svc -Z 2>&1; exit 0' check_rc daemontools/svc-rc 100 sh -c 'svc -Z >/dev/null 2>&1' _sv=$(svstat 2>&1); _svrc=$? if [ "$_svrc" -eq 0 ] && [ -z "$_sv" ]; then bst_ok daemontools/svstat-no-args else bst_notok daemontools/svstat-no-args "rc=$_svrc out='$(_bst_1line "$_sv")' (0.76 exits 0 silently on no argument)"; fi # =========================================================================== # B. THE FIFO CONSTRAINT, STATED RATHER THAN ASSUMED # =========================================================================== # supervise cannot run anywhere mkfifo cannot. Both halves are asserted so the # constraint is a recorded measurement of this image and not a suite convention. if mkfifo "$W/probe.fifo" 2>/dev/null; then bst_ok daemontools/fifo-on-tmpfs rm -f "$W/probe.fifo" else bst_notok daemontools/fifo-on-tmpfs "mkfifo refused on tmpfs at $W -- supervise cannot run anywhere" fi # /var is the virtiofs root. A refusal here is EXPECTED and is what confines # every service directory to tmpfs; a success would mean the backend gained # fifo support and this suite's staging could relax. if mkfifo /var/bst-dt-probe.fifo 2>/dev/null; then bst_notok daemontools/fifo-off-virtiofs "mkfifo SUCCEEDED on the virtiofs root -- re-read the staging constraint" rm -f /var/bst-dt-probe.fifo else bst_ok daemontools/fifo-off-virtiofs fi # =========================================================================== # C. NULL CONTROL -- can a blocked background task be signalled at all? # =========================================================================== # No daemontools code is involved. Read this case before any kill-dependent one # below: it is the discriminator between "this port is broken" and # "brintos/linux#21". mkfifo "$W/ctl.fifo" 2>/dev/null cat < "$W/ctl.fifo" > /dev/null 2>&1 & _ctlpid=$! kill -TERM "$_ctlpid" 2>/dev/null if wait_gone "$_ctlpid" 15; then bst_ok daemontools/plat-signal-control PLAT_SIGNALS=yes else bst_notok daemontools/plat-signal-control "a blocked task survived SIGTERM for 15s -- brintos/linux#21; every kill-dependent case below is held by it" PLAT_SIGNALS=no fi # =========================================================================== # D. THE SUPERVISION LIFECYCLE -- the ticket's pass criterion # =========================================================================== S1=$W/svc1 mkdir -p "$S1" mkfifo "$S1/hold" 2>/dev/null # The service records each start and then blocks on a fifo open -- a real # blocking syscall with no timer, since sleep is a no-op here. cat > "$S1/run" <> "$W/svc1.starts" exec cat < "$S1/hold" EOF chmod 755 "$S1/run" supervise "$S1" > "$W/svc1.super" 2>&1 & SUPER1=$! if wait_for "$S1/supervise/status" 20; then bst_ok daemontools/supervise-starts else bst_notok daemontools/supervise-starts "no $S1/supervise/status after 20s: $(_bst_1line "$(cat "$W/svc1.super" 2>&1)")"; fi # The control channel and the lock are supervise's own on-disk contract; each is # a distinct platform capability (mkfifo, flock) and worth its own case. check daemontools/control-fifo test -p "$S1/supervise/control" check daemontools/ok-fifo test -p "$S1/supervise/ok" check daemontools/lockfile test -f "$S1/supervise/lock" # svstat reports it up, with a pid. if wait_svstat "$S1" "up (pid " 20; then bst_ok daemontools/svstat-up else bst_notok daemontools/svstat-up "svstat never reported up: $(_bst_1line "$SV_OUT")"; fi PID1=$(svpid "$S1") # The pid svstat prints is read out of the 18-byte binary status file, so it is # an independent claim from "a process is running" -- assert it against procfs. if [ -n "$PID1" ] && [ -r "/proc/$PID1/stat" ]; then bst_ok daemontools/svstat-pid-real else bst_notok daemontools/svstat-pid-real "svstat pid='$PID1' has no /proc entry"; fi # The service really did execute -- supervise forked AND exec'd ./run, rather # than merely recording an intention to. if wait_data "$W/svc1.starts" 20; then bst_ok daemontools/service-ran else bst_notok daemontools/service-ran "$W/svc1.starts still empty after 20s"; fi # svok: exit 0 while supervise holds the directory. check_rc daemontools/svok-up 0 svok "$S1" # The status file is exactly 18 bytes -- 12 of TAI64N, 4 of pid, 1 paused, 1 # want. That is the wire format svstat, svc and any third-party reader share, # and it is fixed by the format rather than by the target's word size, so a # 32-bit arm that got it wrong would ship a status file nothing else can read. check_eq daemontools/status-18-bytes 18 "$(wc -c < "$S1/supervise/status" 2>/dev/null | tr -d ' ')" # --- svc -d : take it down -------------------------------------------------- # The control byte reaches supervise through the FIFO (ordinary I/O, no signal), # but supervise then SIGTERMs the child -- so this case is held by #21 exactly # as the null control above predicts. svc -d "$S1" if wait_svstat "$S1" "down" 25; then bst_ok daemontools/svc-down else if [ "$PLAT_SIGNALS" = no ]; then bst_notok daemontools/svc-down "still not down after 25s (brintos/linux#21 -- the null control failed too): $(_bst_1line "$SV_OUT")" else bst_notok daemontools/svc-down "still not down after 25s though the platform CAN signal: $(_bst_1line "$SV_OUT")" fi fi # "normally up" is the other half of the reading and is NOT redundant with # "down": it says supervise stat'd the service directory, found no `down` file, # and therefore knows this service is one it would start on its own. A supervisor # that had lost track of the directory would say "down" just the same. # # The `want down` flag is deliberately NOT asserted, though it looks like the # obvious thing to check. svstat prints it only when `pid && want == 'd'` # (svstat.c) -- that is, only in the window between the control byte landing and # the child actually dying. Asserting it would be racing supervise, and the case # would flip on host load rather than on anything about the port. check_out daemontools/svc-down-normally-up "normally up" sh -c "svstat '$S1' 2>&1; exit 0" # --- svc -u : bring it back ------------------------------------------------- svc -u "$S1" if wait_svstat "$S1" "up (pid " 25; then PID2=$(svpid "$S1") bst_ok daemontools/svc-up # A NEW pid is the whole claim: same pid would mean nothing ever went down and # svstat is reporting a stale status file. if [ -n "$PID2" ] && [ "$PID2" != "$PID1" ]; then bst_ok daemontools/svc-up-new-pid else bst_notok daemontools/svc-up-new-pid "pid unchanged across down/up (was '$PID1', now '$PID2')"; fi else bst_notok daemontools/svc-up "not up 25s after svc -u: $(_bst_1line "$SV_OUT")" bst_notok daemontools/svc-up-new-pid "service never came back up" fi svc -x "$S1" 2>/dev/null # =========================================================================== # E. RESTART ON EXIT -- measured twice, async and nudged # =========================================================================== S2=$W/svc2 mkdir -p "$S2" cat > "$S2/run" <> "$W/svc2.starts" exit 0 EOF chmod 755 "$S2/run" supervise "$S2" > "$W/svc2.super" 2>&1 & SUPER2=$! wait_for "$S2/supervise/status" 20 wait_data "$W/svc2.starts" 20 _n1=$(nlines "$W/svc2.starts") # The service exits immediately every time, so a supervisor that notices is # restarting it continuously. Give it a bounded window with NO prompting: this # is the SIGCHLD question and nothing else. read _u _r < /proc/uptime; _s0=${_u%.*} while :; do read _u _r < /proc/uptime; _n=${_u%.*} [ $((_n - _s0)) -ge 15 ] && break _n2=$(nlines "$W/svc2.starts") [ "$_n2" -gt "$_n1" ] && break done _n2=$(nlines "$W/svc2.starts") if [ "$_n2" -gt "$_n1" ]; then bst_ok daemontools/restart-async else bst_notok daemontools/restart-async "service exited and was not restarted in 15s unprompted ($_n1 start(s)); brintos/linux#21 -- see restart-nudged for whether the supervision logic itself works" fi # Now force supervise through one loop iteration with an idempotent control byte # ('u' when it already wants up changes no state). The wakeup is a FIFO write -- # ordinary I/O -- so this measures the reap-and-restart logic itself, which is # answerable however #21 behaves. svc -u "$S2" read _u _r < /proc/uptime; _s0=${_u%.*} while :; do read _u _r < /proc/uptime; _n=${_u%.*} [ $((_n - _s0)) -ge 20 ] && break _n3=$(nlines "$W/svc2.starts") [ "$_n3" -gt "$_n2" ] && break svc -u "$S2" done _n3=$(nlines "$W/svc2.starts") if [ "$_n3" -gt "$_n2" ]; then bst_ok daemontools/restart-nudged else bst_notok daemontools/restart-nudged "supervise did not restart an exited service even when driven through its control fifo ($_n2 -> $_n3 starts) -- this one is the package's, not the platform's"; fi svc -x "$S2" 2>/dev/null # svok against a directory nobody supervises: exit 100, the documented status. S3=$W/svc3; mkdir -p "$S3" check_rc daemontools/svok-absent 100 svok "$S3" check_out daemontools/svstat-absent "unable to open" sh -c "svstat '$S3' 2>&1; exit 0" # =========================================================================== # F. THE REST OF THE PACKAGE # =========================================================================== # multilog -- the log pipe the ticket names. It reads stdin and writes to a # rotating directory log, which is the mode svscan wires every service's stderr # into, so it is the one worth covering rather than multilog's filter language. ML=$W/log; mkdir -p "$ML" printf 'alpha\nbravo\ncharlie\n' | multilog "$ML" 2>"$W/ml.err" if [ -s "$ML/current" ]; then bst_ok daemontools/multilog-current else bst_notok daemontools/multilog-current "no $ML/current: $(_bst_1line "$(cat "$W/ml.err" 2>&1)")"; fi check_out daemontools/multilog-content "bravo" cat "$ML/current" # t: prefix each line with a TAI64N stamp. The stamp is @4000... and is the # format tai64nlocal reads back, so the pair is tested as a round trip rather # than as two independent spellings. ML2=$W/log2; mkdir -p "$ML2" printf 'stamped\n' | multilog t "$ML2" 2>/dev/null check_out daemontools/multilog-tai64n "@4000" cat "$ML2/current" check_out daemontools/tai64nlocal "stamped" sh -c "tai64nlocal < '$ML2/current' 2>&1" # tai64n on its own: it timestamps its stdin, so a line in gives a stamped line # out. External-format TAI64N is 8 bytes of seconds + 4 of nanoseconds rendered # as 24 hex digits after '@'. _t=$(printf 'x\n' | tai64n 2>/dev/null) case "$_t" in @????????????????????????' 'x) bst_ok daemontools/tai64n-format ;; *) bst_notok daemontools/tai64n-format "unexpected: $(_bst_1line "$_t")" ;; esac # envdir: one file per variable, contents are the value. ED=$W/envdir; mkdir -p "$ED" printf 'from-envdir' > "$ED/BST_DT_VAR" check_out daemontools/envdir "from-envdir" envdir "$ED" sh -c 'echo "$BST_DT_VAR"' # An empty file UNSETS the variable rather than setting it empty -- the # behaviour that distinguishes envdir from a naive `export $(cat)` loop. : > "$ED/BST_DT_UNSET" check_out daemontools/envdir-unset "UNSET-OK" \ env BST_DT_UNSET=present envdir "$ED" sh -c 'test -z "${BST_DT_UNSET+x}" && echo UNSET-OK' # setlock: -n makes it non-blocking, so a second holder fails immediately. Two # nested invocations on one lockfile is the whole contract, and it exercises the # same flock(2) the supervise lockfile above depends on -- from a second caller. LK=$W/lock check_rc daemontools/setlock-acquires 0 setlock "$LK" true check_rc daemontools/setlock-contended 111 \ setlock "$LK" sh -c "setlock -n '$LK' true" # softlimit: sets rlimits then execs. LEFT FAILING, attributed to # brintos/linux#20 (no rlimit syscalls wired -- getrlimit returns ENOSYS while # /proc/self/limits reports real values). softlimit calls getrlimit before every # setrlimit, so it dies on the read rather than the write and NO limit of any # kind can be applied. Asserted rather than skipped so it flips green by itself # when #20 lands; the expected diagnostic is pinned too, because "it failed" and # "it failed for the reason we think" are different findings. check_out daemontools/softlimit "8192" softlimit -m 8388608 sh -c 'ulimit -v' check_out daemontools/softlimit-gap "getrlimit failed" \ sh -c 'softlimit -m 8388608 true 2>&1; exit 0' # fghack: holds a daemon in the foreground by keeping a pipe open. With a # command that simply exits it is a pass-through, which is the assertable half # without depending on detachment behaviour #35 governs. check_out daemontools/fghack "fg-ok" fghack sh -c 'echo fg-ok' # pgrphack: runs its child in a NEW process group. The child reads its own pgid # out of procfs -- field 5 of /proc/self/stat -- and it must differ from the # suite's. _mypgrp=$(awk '{print $5}' /proc/self/stat 2>/dev/null) _childpgrp=$(pgrphack sh -c 'awk "{print \$5}" /proc/self/stat' 2>/dev/null) if [ -n "$_childpgrp" ] && [ "$_childpgrp" != "$_mypgrp" ]; then bst_ok daemontools/pgrphack else bst_notok daemontools/pgrphack "child pgid '$_childpgrp' vs suite pgid '$_mypgrp'"; fi # setuidgid / envuidgid: argument handling only. brintos/linux#19 has the # credential setters unwired, so what they DO cannot be asserted here; that an # unknown account is rejected before any setgid(2) is reached can be. check_out daemontools/setuidgid-unknown "unknown account" \ sh -c 'setuidgid bst-no-such-user true 2>&1; exit 0' check_out daemontools/envuidgid-unknown "unknown account" \ sh -c 'envuidgid bst-no-such-user true 2>&1; exit 0' # svscan needs a directory of service directories; run it against an EMPTY one # so it starts, finds nothing to supervise and can be shut down immediately. # Anything more would leave supervise processes behind for the rest of the run. SC=$W/scan; mkdir -p "$SC" svscan "$SC" > "$W/svscan.out" 2>&1 & _scanpid=$! if wait_for "/proc/$_scanpid/stat" 10; then bst_ok daemontools/svscan-starts else bst_notok daemontools/svscan-starts "svscan did not start: $(_bst_1line "$(cat "$W/svscan.out" 2>&1)")"; fi kill -TERM "$_scanpid" 2>/dev/null rm -rf "$W" 2>/dev/null