Files
firmware/qa
Oleksandr ShyshatskyiandClaude Opus 5 057a3a11d7 The boot was seven scripts in filename order. It is one compiled graph now.
firmwareExe ran $MODDIR/init.d/S* one at a time in the foreground, and that
loop WAS the boot order. Three of the seven were already thin wrappers over a
supervisor that was already running, so what the loop still carried was not
supervision, it was sequencing -- expressed as filenames, in a directory
nothing but the loop read.

This ships s6-rc and moves the sequencing into a database compiled at build
time. firmwareExe now starts the scanner as its own child, initialises s6-rc,
asks for one machine state, and blocks on `wait "$SCANNER_PID"`.

WHAT THE GRAPH ACTUALLY SAYS

    mcu-bringup -> klipper -> ff-startup -> ui
                                  ^
                              moonraker

    ok-all = wifi nginx moonraker camera klipper ff-startup ui

Those are the first real edges the plan has ever had; it predicted they would
not arrive until phase 7. bin/ff-startup.py became two services, which is what
made them possible: the MCU bring-up has to precede klipper and the readiness
wait has to follow it, and both facts now live where a program can read them.

Deliberately still absent: nginx before moonraker. S60nginx's header wants it,
but dependencies.d/nginx under moonraker means `s6-rc -d change nginx` stops
moonraker too -- the exact failure splitting S60web into two scripts prevented.
The cost of not writing it is a second where a reloading browser gets a refused
connection instead of a 502.

AND WHAT THEY DO NOT SAY. klipper has no notification-fd, so s6-rc calls it up
when it is forked, not when klippy is usable. ff-startup keeps its own poll of
moonraker's API. moonraker and camera DO carry one, so their readiness is real.
The asymmetry is written down at each site rather than glossed.

init.d/ IS DELETED, REVERSING PHASE 4

Phase 4 promised the S* scripts would survive as thin wrappers, because
`S60nginx restart` is in people's hands. They did not survive, and the reason
is the argument nginx/run already made: a service that only works when a shell
script happened to run first is not supervised, it is lucky. Every prerequisite
a wrapper did belongs in the run script anyway, because s6 restarts a service
without going near a shell. Once they moved, the wrappers were verb translation
in a directory whose S<NN> numbers meant nothing. s6-rc is the interface now.

anvil-service.sh goes with them -- 625 lines whose last caller was firmwareExe,
and a shared library for one consumer is the redundancy the wrappers were.

So do the fallbacks. S60nginx and S62moonraker each carried a second,
unsupervised implementation reachable only via MOD_S6=0; that switch is gone,
and with it the argument for a code path exercised only on broken printers. The
cost is stated plainly in anvil.conf: a printer whose s6 fails now has nothing.

klipperDaemon is replaced with a no-op `start`. It is FlashForge's script and
off-repo callers remain -- stock start.sh, the UI's restart path, habits over
ssh -- and each of them would fork a second klippy beside the supervised one.

WHAT SUPERVISES THE SUPERVISOR, ANSWERED

Phase 3 left this open and guessed a dead scanner was a dead printer either
way. It is not. firmwareExe's tail used to be `while true; do sleep 3600; done`
purely to satisfy app_startup.sh's watchdog, which greps ps for our name 5s in.
It waits on the scanner instead, so exiting IS the recovery: app_startup.sh --
FlashForge's own init, which we never patch -- re-execs us and the tree comes
back. Not a respawn loop in ash, which phase 3 rightly refused. It restarts
everything rather than the scanner alone, and that is written down.

MEASURED, NOT READ OFF THE MANUAL

Host- and target-compiled databases are byte-identical (db md5
80f5261437191fa591744ada6087c6f7 both ways), so the database is built here and
shipped. /run is tmpfs and writable, so the live directory needs no fallback.
Both were BLOCKING UNKNOWNs in the note and are now settled sections.

Three traps the replica found that reading could not:

  * s6-svscan resolves s6-supervise through PATH. Without $MODDIR/bin on it the
    scanner answers s6-svscanctl -a perfectly and then fails every spawn with
    ENOENT for ever, and s6-rc-init HANGS rather than failing.
  * s6-rc-init creates one symlink per service in the scandir and dies with
    "File exists" if the name is taken. So the cutover is not additive, and
    run-append.sh sweeps.
  * s6-rc-compile does not reject a `down` file. It accepts it and SILENTLY
    DROPS it -- the servicedir is byte-identical. "Down by default" can only
    mean absent from the bundle, which is why mcu-bringup is the one service
    outside ok-all and arrives as klipper's dependency.

execline turned out not to be optional: s6-rc GENERATES two servicedirs whose
run scripts are execline, and every oneshot's `up` goes through one of them.
The binary list was read off a real compile, not guessed -- seven execline
binaries of 53, plus six s6 ones we built and did not ship.

Two build bugs fell out. fetch-assets.sh compared against a stamp patch.sh
never wrote, so every build pulled the ~100MB toolchain the skip exists to
avoid; S6_STAMP is one string in common.sh now. And `chmod +x .../bin/s6-*`
would have missed all seven execline binaries.

qa: 83 static. The suite gained the s6-rc run scripts and klipperDaemon, which
no glob had ever covered, and lost a dead payload/init.d/S* glob -- exactly the
silently-retired check that file's own docstring warns about. The oneshots'
up/down are NOT in it: they are execline carrying shell inside a /bin/sh -c
argument, so both checkers would parse the wrapper and pass.

Not verified here: no config.env and no stock package on this machine, so the
mipsel cross-build of execline/s6-rc and the ABI gate over 33 binaries have
never run. The native compile and the database are proven; that half is not.
The replica gate is the next commit, and it still owes an answer on whether
helix-screen runs under s6-supervise at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 12:09:58 +03:00
..