mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-19 03:49:50 -05:00
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>
This commit is contained in:
1 parent
16d00be525
commit
057a3a11d7
68 files changed
+1938
-3146
No files matched your search
+15
-1
@@ -73,10 +73,24 @@ MIPS_TOOLCHAIN_TGZ="${MIPS_TOOLCHAIN_TGZ:-$ROOT/vendor/${MIPS_TOOLCHAIN_FILE:-mi
|
||||
# downloading when that cache is missing or stale.
|
||||
SKALIBS_TGZ="${SKALIBS_TGZ:-$ROOT/vendor/skalibs-${SKALIBS_VERSION:-unpinned}.tar.gz}"
|
||||
S6_TGZ="${S6_TGZ:-$ROOT/vendor/s6-${S6_VERSION:-unpinned}.tar.gz}"
|
||||
EXECLINE_TGZ="${EXECLINE_TGZ:-$ROOT/vendor/execline-${EXECLINE_VERSION:-unpinned}.tar.gz}"
|
||||
S6RC_TGZ="${S6RC_TGZ:-$ROOT/vendor/s6-rc-${S6RC_VERSION:-unpinned}.tar.gz}"
|
||||
MUSL_TOOLCHAIN_TGZ="${MUSL_TOOLCHAIN_TGZ:-$ROOT/vendor/${MUSL_TOOLCHAIN_FILE:-mips32r5el--musl--stable.tar.xz}}"
|
||||
S6_BUILD="${S6_BUILD:-$ROOT/work/.s6}"
|
||||
# THE CACHE KEY, defined once because two files test it: patch.sh writes it to
|
||||
# $S6_BUILD/.version after a successful build, and fetch-assets.sh compares
|
||||
# against it to decide whether the ~100MB toolchain is worth downloading. They
|
||||
# disagreed for as long as both existed -- fetch-assets knew only the two
|
||||
# source versions while patch.sh already stamped the toolchain file -- so the
|
||||
# skip never fired. Every input that changes the OUTPUT belongs in here: four
|
||||
# source versions, because s6-rc's generated servicedirs embed paths from the
|
||||
# execline it was compiled against, and the toolchain file, because the
|
||||
# toolchain determines the ABI as much as the sources do (a checkout that had
|
||||
# already built s6 once would otherwise read its old legacy-NaN tree as
|
||||
# current and never rebuild it -- a failure mode that shipped once).
|
||||
S6_STAMP="$SKALIBS_VERSION $EXECLINE_VERSION $S6_VERSION $S6RC_VERSION $MUSL_TOOLCHAIN_FILE"
|
||||
export MAINSAIL_ZIP HELIX_TGZ MOONRAKER_TGZ KLIPPER_TGZ MIPS_TOOLCHAIN_TGZ
|
||||
export SKALIBS_TGZ S6_TGZ MUSL_TOOLCHAIN_TGZ S6_BUILD
|
||||
export SKALIBS_TGZ S6_TGZ EXECLINE_TGZ S6RC_TGZ MUSL_TOOLCHAIN_TGZ S6_BUILD S6_STAMP
|
||||
|
||||
# CPython and the seven C libraries it is linked against, all pinned in
|
||||
# versions.env and all cross-built by bin/patch.sh section 5c. Eight tarballs
|
||||
|
||||
+13
-1
@@ -123,8 +123,20 @@ get "https://skarnet.org/software/skalibs/skalibs-$SKALIBS_VERSION.tar.gz" \
|
||||
"$SKALIBS_TGZ" "$SKALIBS_SHA256"
|
||||
get "https://skarnet.org/software/s6/s6-$S6_VERSION.tar.gz" \
|
||||
"$S6_TGZ" "$S6_SHA256"
|
||||
# execline and s6-rc, on the same terms: small, always fetched, never vendored.
|
||||
# See versions.env for why execline is here at all when our own `run` scripts
|
||||
# are plain #!/bin/sh.
|
||||
get "https://skarnet.org/software/execline/execline-$EXECLINE_VERSION.tar.gz" \
|
||||
"$EXECLINE_TGZ" "$EXECLINE_SHA256"
|
||||
get "https://skarnet.org/software/s6-rc/s6-rc-$S6RC_VERSION.tar.gz" \
|
||||
"$S6RC_TGZ" "$S6RC_SHA256"
|
||||
# The stamp compared here MUST be the one bin/patch.sh writes, character for
|
||||
# character. It was "$SKALIBS_VERSION $S6_VERSION" while patch.sh already
|
||||
# stamped the toolchain file too, so this test never matched and every build
|
||||
# pulled the ~100MB toolchain it was written to skip. The stamp is a single
|
||||
# string in one place now; if it moves, it moves in both files at once.
|
||||
if [ "$ALL" = 1 ] \
|
||||
|| [ "$(cat "$S6_BUILD/.version" 2>/dev/null || true)" != "$SKALIBS_VERSION $S6_VERSION" ]; then
|
||||
|| [ "$(cat "$S6_BUILD/.version" 2>/dev/null || true)" != "$S6_STAMP" ]; then
|
||||
# Bootlin's own release layout: one directory per target architecture,
|
||||
# one versioned tarball per release inside it. See versions.env for why
|
||||
# this is Bootlin and not musl.cc.
|
||||
|
||||
+210
-74
@@ -29,17 +29,12 @@ rm -rf "$MOD_PAYLOAD" "$SOFTWARE_DIR/mod" # $SOFTWARE_DIR/mod: leftover from a
|
||||
# etc/ is the same idea one directory further on: a --prefix root keeps the
|
||||
# mod's own configuration in etc/, and etc/s6/ is the s6 SCANDIR -- the
|
||||
# directory s6-svscan watches, one subdirectory per supervised service. It is
|
||||
# created here rather than at runtime by payload/init.d/S40s6, and the reason
|
||||
# is the install manifest: the manifest is read off this staged tree, so a
|
||||
# directory that only ever appeared on the printer would be a path the mod
|
||||
# creates and no update can ever account for. It was empty when S40s6 first
|
||||
# landed and is not any more -- the service directories themselves are copied
|
||||
# in further down, next to init.d -- but the mkdir stays, because a payload
|
||||
# that happens to ship no services still needs somewhere for the scanner to
|
||||
# look. S40s6 still does its own
|
||||
# mkdir -p on top of this -- see the comment there -- because the manifest
|
||||
# pass runs BEFORE the new tarball is extracted, and because hand-made
|
||||
# installs exist.
|
||||
# created here rather than at runtime, and the reason is the install manifest:
|
||||
# the manifest is read off this staged tree, so a directory that only ever
|
||||
# appeared on the printer would be a path the mod creates and no update can
|
||||
# ever account for. It ships EMPTY -- s6-rc-init lays the live servicedirs down
|
||||
# at boot from the compiled database -- and the mkdir stays anyway, because the
|
||||
# scanner still needs somewhere to look.
|
||||
#
|
||||
# lib/ completes the prefix root and is CPython's, in the same way libexec/ is
|
||||
# s6's: `--prefix=/usr/data/anvil` puts the interpreter in bin/ and its whole
|
||||
@@ -50,7 +45,8 @@ rm -rf "$MOD_PAYLOAD" "$SOFTWARE_DIR/mod" # $SOFTWARE_DIR/mod: leftover from a
|
||||
# still names the directory it would have used.
|
||||
mkdir -p "$MOD_PAYLOAD/bin" "$MOD_PAYLOAD/lib" "$MOD_PAYLOAD/libexec" \
|
||||
"$MOD_PAYLOAD/nginx" \
|
||||
"$MOD_PAYLOAD/www" "$MOD_PAYLOAD/config" "$MOD_PAYLOAD/etc/s6"
|
||||
"$MOD_PAYLOAD/www" "$MOD_PAYLOAD/config" "$MOD_PAYLOAD/etc/s6" \
|
||||
"$MOD_PAYLOAD/etc/s6-rc"
|
||||
|
||||
# ---------------------------------------------------------------- 1. Klipper
|
||||
# BUILD_KLIPPER=fork (the default) ships the creator5 Klipper tree; =stock
|
||||
@@ -409,12 +405,6 @@ mips_abi_gate() {
|
||||
# c_helper.so above, and for the same reason: nothing binary is vendored in
|
||||
# this repo, so anything the printer executes is built from a hash we pinned.
|
||||
#
|
||||
# NOTHING STARTS s6 YET. This step only puts it in the payload. The init
|
||||
# scripts still hand-roll their supervision in payload/anvil-service.sh and
|
||||
# will keep doing so until the scanner lands (docs/notes/80-s6-migration.md,
|
||||
# phase 3). Shipping it first is what makes that a one-file change instead of
|
||||
# a build change and a boot change at once.
|
||||
#
|
||||
# WHY NOT IN A CONTAINER OF ITS OWN. tools/supervisor/Dockerfile builds this
|
||||
# in a debian:bookworm of its own -- that was the measurement harness, and it
|
||||
# also had to build runit and execline for the comparison. The real build
|
||||
@@ -446,20 +436,36 @@ S6_TOOLCHAIN_DIR=work/.musl-toolchain/$S6_HOST-cross
|
||||
# and s6-svwait are unusable without them
|
||||
# mkfifodir/cleanfifodir create and tidy the fifodirs those listen on
|
||||
# notifyoncheck readiness for a service that cannot notify for itself
|
||||
# rc/rc-init/rc-db the boot: compile-time graph, one transition, one query
|
||||
# ipcserver-*/sudod/fdholder-daemon/ipcclient NOT ours and not optional --
|
||||
# s6-rc-compile GENERATES two servicedirs that exec these
|
||||
# (s6rc-oneshot-runner and s6rc-fdholder), and every
|
||||
# oneshot's `up` runs through the first. Read off the
|
||||
# generated servicedirs/*/run of a real compile, in the
|
||||
# replica, rather than guessed from the manual.
|
||||
S6_BINS="s6-svscan s6-svscanctl s6-supervise s6-svc s6-svstat s6-svwait s6-svok
|
||||
s6-svlisten s6-svlisten1 s6-ftrig-listen1 s6-mkfifodir s6-cleanfifodir
|
||||
s6-notifyoncheck"
|
||||
# Not in bin/ and not optional: s6-svlisten spawns this by absolute path out of
|
||||
# the compiled-in libexecdir, and without it every waiting verb dies with
|
||||
# "unable to ftrigr_startf: No such file or directory".
|
||||
S6_LIBEXEC="s6-ftrigrd"
|
||||
# MUSL_TOOLCHAIN_FILE is IN the stamp, not just the two source versions: the
|
||||
# toolchain that builds s6 determines its ABI as much as the sources do, and
|
||||
# work/.s6/.version has no other way to notice that the pin moved from a
|
||||
# legacy-NaN toolchain to a nan2008 one. Without this, a checkout that had
|
||||
# already built s6 once would read its old e_flags=0x1007 tree as current
|
||||
# and never rebuild it -- exactly the failure mode that shipped once.
|
||||
S6_STAMP="$SKALIBS_VERSION $S6_VERSION $MUSL_TOOLCHAIN_FILE"
|
||||
s6-notifyoncheck
|
||||
s6-ipcserver-socketbinder s6-ipcserverd s6-ipcserver-access s6-sudod
|
||||
s6-fdholder-daemon s6-ipcclient
|
||||
s6-rc s6-rc-init s6-rc-db"
|
||||
|
||||
# Seven of execline's 53. Not a subset we chose for taste: it is exactly what
|
||||
# the two generated servicedirs exec, and nothing else. s6-rc-compile itself is
|
||||
# NOT shipped -- the database is compiled here, on the build host (see 5b-2).
|
||||
EXECLINE_BINS="execlineb fdmove pipeline if forstdin exit redirfd"
|
||||
# Not in bin/ and not optional, all three, and each for the same reason: they
|
||||
# are spawned by ABSOLUTE PATH out of the compiled-in libexecdir, so PATH
|
||||
# cannot rescue them. Without s6-ftrigrd every waiting verb dies with "unable
|
||||
# to ftrigr_startf: No such file or directory"; without the two s6-rc helpers
|
||||
# the generated servicedirs the oneshot runner and the fdholder live in fail
|
||||
# the same way, at the moment the first oneshot runs.
|
||||
S6_LIBEXEC="s6-ftrigrd s6-rc-oneshot-run s6-rc-fdholder-filler"
|
||||
# S6_STAMP -- the cache key -- is defined in bin/common.sh, because
|
||||
# fetch-assets.sh compares against the same string to decide whether the
|
||||
# toolchain download is worth doing. It was defined HERE and duplicated
|
||||
# (differently) there, so the two never agreed. See common.sh for what is in
|
||||
# it and why.
|
||||
|
||||
if [ "$(cat "$S6_BUILD/.version" 2>/dev/null || true)" != "$S6_STAMP" ]; then
|
||||
if [ ! -x "$S6_TOOLCHAIN_DIR/bin/$S6_HOST-gcc" ]; then
|
||||
@@ -483,14 +489,17 @@ if [ "$(cat "$S6_BUILD/.version" 2>/dev/null || true)" != "$S6_STAMP" ]; then
|
||||
[ -d "$1" ] || { echo " !! musl toolchain archive unpacked no directory" >&2; exit 1; }
|
||||
mv "$1" "$S6_TOOLCHAIN_DIR"
|
||||
fi
|
||||
for t in "${SKALIBS_TGZ:-}" "${S6_TGZ:-}"; do
|
||||
for t in "${SKALIBS_TGZ:-}" "${EXECLINE_TGZ:-}" "${S6_TGZ:-}" "${S6RC_TGZ:-}"; do
|
||||
[ -f "$t" ] || { echo " !! no s6 sources at '$t' -- run ./bin/fetch-assets.sh" >&2; exit 1; }
|
||||
done
|
||||
say "s6: cross-compiling skalibs $SKALIBS_VERSION + s6 $S6_VERSION for $MODDIR"
|
||||
rm -rf work/.s6-src work/.s6-sysroot work/.s6-stage work/.s6-xw "$S6_BUILD"
|
||||
say "s6: cross-compiling skalibs $SKALIBS_VERSION + execline $EXECLINE_VERSION + s6 $S6_VERSION + s6-rc $S6RC_VERSION for $MODDIR"
|
||||
rm -rf work/.s6-src work/.s6-sysroot work/.s6-stage work/.s6-xw \
|
||||
work/.s6-native "$S6_BUILD"
|
||||
mkdir -p work/.s6-src work/.s6-xw/bin
|
||||
tar -xzf "$SKALIBS_TGZ" -C work/.s6-src
|
||||
tar -xzf "$EXECLINE_TGZ" -C work/.s6-src
|
||||
tar -xzf "$S6_TGZ" -C work/.s6-src
|
||||
tar -xzf "$S6RC_TGZ" -C work/.s6-src
|
||||
(
|
||||
# A subshell so the cross-compiler's CC/CFLAGS/PATH cannot leak into
|
||||
# anything patch.sh does afterwards.
|
||||
@@ -576,31 +585,56 @@ if [ "$(cat "$S6_BUILD/.version" 2>/dev/null || true)" != "$S6_STAMP" ]; then
|
||||
make -j"$JOBS" >/dev/null
|
||||
make install >/dev/null
|
||||
|
||||
# execline. It was --disable-execline'd out of the s6 build until
|
||||
# phase 8, on the true grounds that our own `run` scripts are plain
|
||||
# #!/bin/sh -- but s6-rc GENERATES servicedirs whose run scripts are
|
||||
# execline, so the choice is no longer ours to make. It installs to
|
||||
# $MODDIR like s6 (its --shebangdir defaults to $bindir and is baked
|
||||
# into every #! line s6-rc-compile later writes) and stages into the
|
||||
# same DESTDIR, from which only EXECLINE_BINS is taken.
|
||||
cd "$SRC/execline-$EXECLINE_VERSION"
|
||||
./configure --host="$S6_HOST" --prefix="$MODDIR" \
|
||||
--with-sysdeps="$SK/lib/skalibs/sysdeps" \
|
||||
--with-include="$SK/include" --with-lib="$SK/lib" \
|
||||
--disable-shared --enable-static --enable-static-libc >/dev/null
|
||||
make -j"$JOBS" >/dev/null
|
||||
make install DESTDIR="$STAGE" >/dev/null
|
||||
|
||||
# --prefix is $MODDIR and NOT the staging directory, because s6 bakes
|
||||
# the prefix into the binaries: this is the path they will look for
|
||||
# s6-ftrigrd under at runtime on the printer. DESTDIR is how the tree
|
||||
# lands somewhere we can read it here without needing /usr/data/anvil to
|
||||
# exist on the build machine.
|
||||
#
|
||||
# --disable-execline is not an optimisation either. s6 links against
|
||||
# execline by DEFAULT -- src/libs6 and the ftrig tools #include
|
||||
# <execline/execline.h> and the build simply stops without it -- and
|
||||
# execline is 53 more binaries and 2.1MB we would have to ship and
|
||||
# nothing would run: our `run` scripts are plain #!/bin/sh. Turning it
|
||||
# off here is what makes "we do not ship execline" true rather than
|
||||
# aspirational.
|
||||
# s6 needs execline's headers and .a to link now that it is no longer
|
||||
# --disable-execline'd, and they are inside the DESTDIR tree at the
|
||||
# prefix -- $STAGE$MODDIR, not $STAGE.
|
||||
cd "$SRC/s6-$S6_VERSION"
|
||||
./configure --host="$S6_HOST" --prefix="$MODDIR" \
|
||||
--with-sysdeps="$SK/lib/skalibs/sysdeps" \
|
||||
--with-include="$SK/include" --with-lib="$SK/lib" \
|
||||
--disable-execline \
|
||||
--with-include="$STAGE$MODDIR/include" \
|
||||
--with-lib="$STAGE$MODDIR/lib" \
|
||||
--disable-shared --enable-static --enable-static-libc >/dev/null
|
||||
make -j"$JOBS" >/dev/null
|
||||
make install DESTDIR="$STAGE" >/dev/null
|
||||
|
||||
# s6-rc, last: it links against all three.
|
||||
cd "$SRC/s6-rc-$S6RC_VERSION"
|
||||
./configure --host="$S6_HOST" --prefix="$MODDIR" \
|
||||
--with-sysdeps="$SK/lib/skalibs/sysdeps" \
|
||||
--with-include="$SK/include" --with-lib="$SK/lib" \
|
||||
--with-include="$STAGE$MODDIR/include" \
|
||||
--with-lib="$STAGE$MODDIR/lib" \
|
||||
--bootdb="$MODDIR/etc/s6-rc/compiled/current" \
|
||||
--livedir=/run/s6-rc \
|
||||
--disable-shared --enable-static --enable-static-libc >/dev/null
|
||||
make -j"$JOBS" >/dev/null
|
||||
make install DESTDIR="$STAGE" >/dev/null
|
||||
)
|
||||
# Take only what we ship, out of the DESTDIR tree at its real prefix.
|
||||
mkdir -p "$S6_BUILD/bin" "$S6_BUILD/libexec"
|
||||
for b in $S6_BINS; do
|
||||
for b in $S6_BINS $EXECLINE_BINS; do
|
||||
cp -f "work/.s6-stage$MODDIR/bin/$b" "$S6_BUILD/bin/$b"
|
||||
done
|
||||
for b in $S6_LIBEXEC; do
|
||||
@@ -620,21 +654,130 @@ fi
|
||||
# flag did not reach one link line, looks like a clean build here and like a
|
||||
# printer that cannot exec its own supervisor there -- the kernel says
|
||||
# ENOEXEC, or worse, execs it with the wrong FPU mode, and explains neither.
|
||||
S6_WANT=$(($(echo $S6_BINS $EXECLINE_BINS $S6_LIBEXEC | wc -w)))
|
||||
S6_ELF=$(mips_abi_gate "$S6_BUILD/bin" "$S6_BUILD/libexec") || exit 1
|
||||
[ "$S6_ELF" = "$(($(echo $S6_BINS | wc -w) + 1))" ] || {
|
||||
echo " !! s6: expected $(($(echo $S6_BINS | wc -w) + 1)) gated ELF objects, mips_abi_gate saw $S6_ELF" >&2
|
||||
[ "$S6_ELF" = "$S6_WANT" ] || {
|
||||
echo " !! s6: expected $S6_WANT gated ELF objects, mips_abi_gate saw $S6_ELF" >&2
|
||||
exit 1; }
|
||||
for b in $S6_BINS $S6_LIBEXEC; do
|
||||
for b in $S6_BINS $EXECLINE_BINS $S6_LIBEXEC; do
|
||||
case " $S6_LIBEXEC " in *" $b "*) f="$S6_BUILD/libexec/$b" ;; *) f="$S6_BUILD/bin/$b" ;; esac
|
||||
[ -s "$f" ] || { echo " !! s6: $b is missing or empty in $S6_BUILD" >&2; exit 1; }
|
||||
done
|
||||
say "s6: $S6_ELF ELF objects are nan2008/o32/mips32r2 -- good"
|
||||
cp -f "$S6_BUILD/bin"/* "$MOD_PAYLOAD/bin/"
|
||||
cp -f "$S6_BUILD/libexec"/* "$MOD_PAYLOAD/libexec/"
|
||||
chmod +x "$MOD_PAYLOAD/bin"/s6-* "$MOD_PAYLOAD/libexec"/*
|
||||
# NOT s6-* any more. execlineb, fdmove, pipeline, if, forstdin, exit and
|
||||
# redirfd are in bin/ too now, and a glob that misses them ships seven files
|
||||
# the scanner cannot exec -- which surfaces as the first oneshot failing and
|
||||
# nothing else, in s6's own log.
|
||||
for b in $S6_BINS $EXECLINE_BINS; do chmod +x "$MOD_PAYLOAD/bin/$b"; done
|
||||
chmod +x "$MOD_PAYLOAD/libexec"/*
|
||||
du -sh "$S6_BUILD/bin" | awk '{print " "$1"\tbin/"}'
|
||||
du -sh "$S6_BUILD/libexec" | awk '{print " "$1"\tlibexec/"}'
|
||||
|
||||
# --------------------------------------------- 5b-2. the s6-rc database
|
||||
# The boot order, compiled. payload/etc/s6-rc/source/ holds the definition
|
||||
# directories (one per service, plus the ok-all bundle) and this turns them
|
||||
# into the binary database s6-rc-init reads at boot.
|
||||
#
|
||||
# COMPILED HERE, ON THE BUILD HOST, NOT ON THE PRINTER. That needs a
|
||||
# s6-rc-compile that runs on x86, which means a second, native build of the
|
||||
# same four tarballs -- which is why this step exists at all and is not two
|
||||
# lines. The alternative was shipping the target s6-rc-compile (131KB) and
|
||||
# compiling at install time, and it was rejected for the reason phase 6 of
|
||||
# docs/notes/80-s6-migration.md states as "do the gate first, not the
|
||||
# installer": a database compiled on the printer can fail on a printer in a
|
||||
# way CI never sees, and the ABI gate above cannot look at a database.
|
||||
#
|
||||
# THAT THIS WORKS AT ALL WAS MEASURED, not reasoned from the manual. On
|
||||
# 2026-08-28, in the replica: the same source tree compiled by the native
|
||||
# s6-rc-compile and by the target one under qemu produced byte-identical
|
||||
# db, n and resolve.cdb, and `diff -r` found no difference in the generated
|
||||
# servicedirs. The database files are host-neutral by construction (every
|
||||
# integer goes through uint32_pack_big) and the servicedirs are neutral
|
||||
# BECAUSE both builds are configured --prefix=$MODDIR -- the #! line
|
||||
# s6-rc-compile writes into the oneshot runner comes from the execline the
|
||||
# COMPILER was linked against, not from the one we ship. Configure the native
|
||||
# stack with a different prefix and every oneshot on the printer dies with
|
||||
# ENOENT while every longrun keeps working.
|
||||
S6RC_SRC="payload/etc/s6-rc/source"
|
||||
S6RC_NATIVE="$PWD/work/.s6-native"
|
||||
[ -d "$S6RC_SRC" ] || { echo " !! no s6-rc source tree at $S6RC_SRC" >&2; exit 1; }
|
||||
|
||||
if [ ! -x "$S6RC_NATIVE/bin/s6-rc-compile" ] \
|
||||
|| [ "$(cat "$S6RC_NATIVE/.version" 2>/dev/null || true)" != "$S6_STAMP" ]; then
|
||||
say "s6-rc: building a native compiler ($SKALIBS_VERSION/$EXECLINE_VERSION/$S6_VERSION/$S6RC_VERSION)"
|
||||
rm -rf work/.s6-native work/.s6-native-src
|
||||
mkdir -p work/.s6-native-src
|
||||
for t in "$SKALIBS_TGZ" "$EXECLINE_TGZ" "$S6_TGZ" "$S6RC_TGZ"; do
|
||||
tar -xzf "$t" -C work/.s6-native-src
|
||||
done
|
||||
(
|
||||
# A subshell for the same reason the cross build has one: nothing
|
||||
# about this compiler's environment may leak into the rest of the
|
||||
# build. Note the ABSENCE of --host: this one runs here.
|
||||
set -e
|
||||
NSRC="$PWD/work/.s6-native-src"
|
||||
ND="$PWD/work/.s6-native-stage"
|
||||
rm -rf "$ND"
|
||||
export CFLAGS="-Os"
|
||||
JOBS=$(nproc 2>/dev/null || echo 4)
|
||||
for pkg in "skalibs-$SKALIBS_VERSION" "execline-$EXECLINE_VERSION" \
|
||||
"s6-$S6_VERSION" "s6-rc-$S6RC_VERSION"; do
|
||||
cd "$NSRC/$pkg"
|
||||
if [ "$pkg" = "skalibs-$SKALIBS_VERSION" ]; then
|
||||
# No --with-sysdep answers here: configure can compile and RUN
|
||||
# its probes, because the target is this machine.
|
||||
./configure --prefix="$MODDIR" \
|
||||
--disable-shared --enable-static >/dev/null
|
||||
else
|
||||
./configure --prefix="$MODDIR" \
|
||||
--with-sysdeps="$ND$MODDIR/lib/skalibs/sysdeps" \
|
||||
--with-include="$ND$MODDIR/include" \
|
||||
--with-lib="$ND$MODDIR/lib" \
|
||||
--disable-shared --enable-static >/dev/null
|
||||
fi
|
||||
make -j"$JOBS" >/dev/null
|
||||
make install DESTDIR="$ND" >/dev/null
|
||||
done
|
||||
)
|
||||
mkdir -p "$S6RC_NATIVE/bin"
|
||||
cp -f "work/.s6-native-stage$MODDIR/bin/s6-rc-compile" "$S6RC_NATIVE/bin/"
|
||||
rm -rf work/.s6-native-src work/.s6-native-stage
|
||||
echo "$S6_STAMP" > "$S6RC_NATIVE/.version"
|
||||
else
|
||||
skip "s6-rc: native compiler already built for $S6_STAMP"
|
||||
fi
|
||||
|
||||
# The database goes to etc/s6-rc/compiled/<stamp>, with `current` a symlink to
|
||||
# it -- skarnet's own advice, so the boot command never changes when the
|
||||
# database does. Not /etc/s6-rc/, which is s6-rc-init's default and is inside
|
||||
# the read-only squashfs.
|
||||
S6RC_DB_NAME="db-$MOD_VER"
|
||||
rm -rf "$MOD_PAYLOAD/etc/s6-rc/compiled"
|
||||
mkdir -p "$MOD_PAYLOAD/etc/s6-rc/compiled"
|
||||
"$S6RC_NATIVE/bin/s6-rc-compile" \
|
||||
"$MOD_PAYLOAD/etc/s6-rc/compiled/$S6RC_DB_NAME" "$S6RC_SRC" || {
|
||||
echo " !! s6-rc-compile refused $S6RC_SRC" >&2; exit 1; }
|
||||
ln -sfn "$S6RC_DB_NAME" "$MOD_PAYLOAD/etc/s6-rc/compiled/current"
|
||||
|
||||
# The shebang the compiler baked in is the one assertion that catches a native
|
||||
# stack built with the wrong prefix, and it is worth making here rather than
|
||||
# discovering on a printer: this file is generated, so it is the only place
|
||||
# the build can see what the database will ask the printer to exec.
|
||||
S6RC_RUNNER="$MOD_PAYLOAD/etc/s6-rc/compiled/$S6RC_DB_NAME/servicedirs/s6rc-oneshot-runner/run"
|
||||
[ -f "$S6RC_RUNNER" ] || {
|
||||
echo " !! s6-rc-compile produced no oneshot runner -- has the source tree no oneshots?" >&2
|
||||
exit 1; }
|
||||
S6RC_SHEBANG=$(head -1 "$S6RC_RUNNER")
|
||||
case "$S6RC_SHEBANG" in
|
||||
"#!$MODDIR/bin/execlineb"*) ;;
|
||||
*) echo " !! the s6-rc database asks for '$S6RC_SHEBANG', not $MODDIR/bin/execlineb --" >&2
|
||||
echo " the native stack was configured with the wrong --prefix" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
say "s6-rc: database $S6RC_DB_NAME compiled -- $(ls "$S6RC_SRC" | wc -l) definitions"
|
||||
|
||||
# -------------------------------------------------- 5c. CPython 3.13 (shipped)
|
||||
# A second Python for the printer, cross-compiled here from the sources pinned
|
||||
# in versions.env with the SAME Ingenic glibc toolchain that builds c_helper.so
|
||||
@@ -650,13 +793,12 @@ du -sh "$S6_BUILD/libexec" | awk '{print " "$1"\tlibexec/"}'
|
||||
# # libnacl -- is cross-built into $MODDIR/lib/python3.13/site-packages by #
|
||||
# # step 4 below, and Moonraker has been measured SERVING on this #
|
||||
# # interpreter through the real boot path on the replica #
|
||||
# # (test/integration/printer/case-moonraker313-s6.sh): S40s6's scandir, #
|
||||
# # S62moonraker, readiness gating on :7125 actually listening, a kill -9 #
|
||||
# # respawn, and a stop that stays stopped. #
|
||||
# # (test/integration/printer/case-moonraker313-s6.sh): the scandir, #
|
||||
# # readiness gating on :7125 actually listening, a kill -9 respawn, and a #
|
||||
# # stop that stays stopped. #
|
||||
# # #
|
||||
# # klippy is NOT among FF_PYTHON's callers and does not run on this #
|
||||
# # interpreter -- it is started separately, by FlashForge's own #
|
||||
# # /usr/prog/klipper/start.sh, hardcoded to 3.8.2 (see init.d/S70klipper). #
|
||||
# # interpreter -- it is the `klipper` s6-rc service, on FlashForge's 3.8.2. #
|
||||
# # klippy's numpy gap is therefore a separate, smaller item, not a #
|
||||
# # precondition of this switch. #
|
||||
# ############################################################################
|
||||
@@ -1613,34 +1755,28 @@ say "firmwareExe: installing wrapper (replaces the stock binary)"
|
||||
cp -f payload/firmwareExe "$SOFTWARE_DIR/firmwareExe"
|
||||
chmod +x "$SOFTWARE_DIR/firmwareExe"
|
||||
|
||||
# ----------------------------------------------------- 10. mod service dir
|
||||
mkdir -p "$MOD_PAYLOAD/init.d"
|
||||
# ------------------------------------------------------- 10. mod scripts
|
||||
# The shared environment. Sourced by run-append.sh, firmwareExe, start.sh and
|
||||
# every init.d script -- one library path and one interpreter for the whole
|
||||
# every s6-rc run script -- one library path and one interpreter for the whole
|
||||
# mod, because carrying a private copy in each of them is how the installer's
|
||||
# check and the boot script came to disagree. Not chmod +x: it is sourced.
|
||||
cp -f payload/anvil-env.sh "$MOD_PAYLOAD/anvil-env.sh"
|
||||
# The shared service shape. Sourced by every init.d script for svc_say,
|
||||
# svc_start_daemon, svc_stop_daemon and the start|stop|restart|status block --
|
||||
# one answer to "is it alive?" and one busybox correction for
|
||||
# start-stop-daemon, instead of a different one per script. Every converted
|
||||
# script exits at once if this file is missing, so leaving it out of the
|
||||
# payload is a printer with no services at all. Not chmod +x: it is sourced.
|
||||
cp -f payload/anvil-service.sh "$MOD_PAYLOAD/anvil-service.sh"
|
||||
[ -d payload/bin ] && cp -f payload/bin/* "$MOD_PAYLOAD/bin/" && chmod +x "$MOD_PAYLOAD/bin"/*
|
||||
cp -f payload/init.d/S* "$MOD_PAYLOAD/init.d/"
|
||||
chmod +x "$MOD_PAYLOAD/init.d"/S*
|
||||
# The s6 service directories: one per supervised service, each holding a `run`
|
||||
# script and whatever s6 control files it needs beside it (`down` to start in
|
||||
# the down state, `notification-fd` to say which descriptor readiness arrives
|
||||
# on). cp -a rather than cp -f because those control files are not scripts and
|
||||
# a plain glob of *.sh would miss them -- and because a `run` that arrives
|
||||
# without its executable bit is a service s6 can never start, which it reports
|
||||
# only in its own log. The chmod is belt and braces for exactly that.
|
||||
if [ -d payload/etc/s6 ]; then
|
||||
cp -a payload/etc/s6/. "$MOD_PAYLOAD/etc/s6/"
|
||||
chmod +x "$MOD_PAYLOAD"/etc/s6/*/run 2>/dev/null || true
|
||||
fi
|
||||
# THE SCANDIR SHIPS EMPTY, and that is the phase 8 change in one line.
|
||||
#
|
||||
# It used to be populated here: payload/etc/s6/{nginx,moonraker,camera} were
|
||||
# copied in, each with a `run`, a `down` and sometimes a `notification-fd`, and
|
||||
# the S* scripts drove them with s6-svc. Those directories have moved to
|
||||
# payload/etc/s6-rc/source/, where they are DEFINITIONS rather than live
|
||||
# service directories, and 5b-2 compiles them into the database. s6-rc-init
|
||||
# lays the live copies down itself, at boot, into this directory.
|
||||
#
|
||||
# Anything left here is now actively harmful rather than merely stale:
|
||||
# s6-rc-init creates one symlink per service in the scandir it is given and
|
||||
# fails outright -- "unable to supervise service directories: File exists" --
|
||||
# if the name is taken. That is why payload/run-append.sh sweeps the scandir on
|
||||
# upgrade, and why this block does not put anything back.
|
||||
[ -d "$MOD_PAYLOAD/etc/s6" ] || mkdir -p "$MOD_PAYLOAD/etc/s6"
|
||||
sed -e "s/^MOD_WEB=.*/MOD_WEB=${MOD_WEB:-1}/" \
|
||||
-e "s/^MOD_CAM=.*/MOD_CAM=${MOD_CAM:-1}/" \
|
||||
-e "s/^MOD_UI=.*/MOD_UI=${MOD_UI:-1}/" \
|
||||
|
||||
+8
-5
@@ -132,10 +132,13 @@ if [ -f "$WORK/anvil.tar.xz" ]; then
|
||||
# 141 -- a race that stayed invisible until the fork klippy tree tripled
|
||||
# the listing, then intermittently reported Mainsail/Moonraker missing
|
||||
# from packages that carried both.
|
||||
grep -q 'init.d/S80ui' <<<"$LIST" && ok "mod payload has the service scripts" \
|
||||
|| bad "mod payload missing init.d services"
|
||||
grep -q 'init.d/S70klipper' <<<"$LIST" && ok "mod payload owns Klipper startup" \
|
||||
|| bad "mod payload missing S70klipper -- Klipper would never start"
|
||||
# The compiled s6-rc database IS the service set now: there are no init.d
|
||||
# scripts, and a payload without this database boots to a scanner
|
||||
# supervising nothing.
|
||||
grep -q 'etc/s6-rc/compiled/' <<<"$LIST" && ok "mod payload has the compiled s6-rc database" \
|
||||
|| bad "mod payload has no s6-rc database -- no service would ever start"
|
||||
grep -q 'servicedirs/klipper/run' <<<"$LIST" && ok "mod payload owns Klipper startup" \
|
||||
|| bad "mod payload has no klipper service -- Klipper would never start"
|
||||
# The manifest is what the NEXT update deletes before it extracts. A
|
||||
# payload shipping without one still installs fine -- run-append.sh falls
|
||||
# back to the old seven-directory rm -rf -- but every printer that takes
|
||||
@@ -159,7 +162,7 @@ if [ -f "$WORK/anvil.tar.xz" ]; then
|
||||
# ff-startup.py, ffscreen.py and ff_mcu_bringup.py all run on it -- so a
|
||||
# package missing it breaks the boot, not just a future release. klippy is
|
||||
# not among FF_PYTHON's callers: it stays on FlashForge's 3.8.2, started by
|
||||
# /usr/prog/klipper/start.sh independently (see init.d/S70klipper).
|
||||
# the klipper s6-rc service independently.
|
||||
#
|
||||
# Two checks, not one, and the second is the one worth having. The
|
||||
# interpreter can be present and perfectly runnable while _sqlite3 is
|
||||
|
||||
+2
-4
@@ -199,11 +199,9 @@ belongs to. Nothing from the test lane can reach a printer.
|
||||
payload/ POSIX sh, busybox ash -- runs ON the printer
|
||||
firmwareExe the wrapper that replaces the stock binary
|
||||
start.sh replaces the stock Klipper launcher (priority, MCU bring-up)
|
||||
init.d/ S50wifi, S60nginx, S62moonraker, S65camera, S70klipper, S80ui
|
||||
etc/s6-rc/source/ the s6-rc service definitions, compiled at build time
|
||||
anvil-env.sh PATH/LD_LIBRARY_PATH/FF_PYTHON -- sourced, not executable
|
||||
anvil-service.sh the start/stop/status/liveness shape every service shares
|
||||
-- sourced, not executable
|
||||
bin/ ff_mcu_bringup.py, wifi-action.sh
|
||||
bin/ ff_mcu_bringup.py, wifi-action.sh, klipperDaemon
|
||||
klipper/ extras/ff_*.py and config/ff-*.cfg + printer.base.cfg
|
||||
helixscreen/ the printer-database entry that makes it a toolchanger
|
||||
anvil.conf runtime switches, preserved across mod updates
|
||||
|
||||
@@ -102,11 +102,11 @@ Understand the safety net first, because FlashForge's UI is gone from the
|
||||
screen and there is nothing to fall back to:
|
||||
|
||||
- ssh and Mainsail do not depend on the screen. `/etc/init.d/S50dropbear` is
|
||||
stock and runs long before the UI, and `init.d/S60nginx` and
|
||||
`init.d/S62moonraker` start the web stack independently of it — and of each
|
||||
other, so `S62moonraker restart` over ssh leaves Mainsail served. They are
|
||||
your way in when the screen is dark.
|
||||
- `init.d/S70klipper` owns Klipper startup, because on stock firmware it was
|
||||
stock and runs long before the UI, and the `nginx` and `moonraker` services
|
||||
are independent of it — and of each other, so restarting moonraker over ssh
|
||||
(`s6-svc -r /usr/data/anvil/etc/s6/moonraker`) leaves Mainsail served. They
|
||||
are your way in when the screen is dark.
|
||||
- The `klipper` service owns Klipper startup, because on stock firmware it was
|
||||
`firmwareExe` — not any init script — that ran `/usr/prog/klipper/start.sh`.
|
||||
Without this the printer would boot to a working screen and be unable to
|
||||
move.
|
||||
@@ -254,9 +254,10 @@ machine will make it light up again:
|
||||
|
||||
```sh
|
||||
ssh root@PRINTER
|
||||
/usr/data/anvil/init.d/S80ui status # what did it choose, and why
|
||||
# chose "none" -> MOD_UI=0, or HelixScreen is not installed
|
||||
# chose "helix" -> it was started and failed on its own; see its log
|
||||
s6-svstat /usr/data/anvil/etc/s6/ui # is the UI service up?
|
||||
grep MOD_UI /usr/data/anvil/anvil.conf # was it asked for at all?
|
||||
# no such servicedir / "unable to open" -> MOD_UI=0, so it was never started
|
||||
# "down" -> it started and failed; see its log
|
||||
```
|
||||
|
||||
`MOD_UI=0` only stops it being started at all — useful to silence a UI that
|
||||
@@ -270,7 +271,7 @@ package for your model to go back to FlashForge's own interface.
|
||||
|
||||
| Symptom | Do this |
|
||||
|---|---|
|
||||
| Printer boots, screen blank | ssh in; `/usr/data/anvil/init.d/S80ui status` says whether the UI was even started. No on-device repair — reflash the mod, or flash the stock package to get FlashForge's UI back. ssh, Mainsail and printing are unaffected |
|
||||
| Printer boots, screen blank | ssh in; `s6-svstat /usr/data/anvil/etc/s6/ui` says whether the UI was even started. No on-device repair — reflash the mod, or flash the stock package to get FlashForge's UI back. ssh, Mainsail and printing are unaffected |
|
||||
| No ssh, no screen | flash the stock package for your model |
|
||||
| Recovery stick does not help | try a newer stock FlashForge package for your model |
|
||||
| Still broken | factory package (`Creator5Pro-factory-*.tgz` **plus** the separate `Creator5Pro-factory.tar.xz` on the same stick; needs 800 MB free) |
|
||||
|
||||
+41
-36
@@ -38,49 +38,54 @@ Two properties come for free from keeping that name and that place:
|
||||
|
||||
## Services
|
||||
|
||||
Services are ordinary init.d-style scripts in `/usr/data/anvil/init.d/`, run in
|
||||
filename order and individually restartable over ssh:
|
||||
Services are **s6-rc services**, defined in `payload/etc/s6-rc/source/`,
|
||||
compiled into a database at build time and supervised by `s6-svscan`:
|
||||
|
||||
```
|
||||
S50wifi wlan0 + wpa_supplicant + udhcpc
|
||||
S60nginx nginx (Mainsail) on :80
|
||||
S62moonraker moonraker on :7125
|
||||
S65camera mjpg-streamer on :8080 (nginx proxies it at /webcam/)
|
||||
S70klipper Klipper
|
||||
S80ui decides whether the UI runs (MOD_UI, HelixScreen installed?)
|
||||
mcu-bringup oneshot toolhead boards out of their bootloaders
|
||||
klipper longrun klippy (depends on mcu-bringup)
|
||||
wifi oneshot wlan0 + wpa_supplicant + udhcpc
|
||||
nginx longrun nginx (Mainsail) on :80
|
||||
moonraker longrun moonraker on :7125
|
||||
camera longrun mjpg-streamer on :8080 (nginx proxies it at /webcam/)
|
||||
ff-startup oneshot waits until the printer is usable
|
||||
(depends on klipper, moonraker)
|
||||
ui longrun HelixScreen (depends on ff-startup)
|
||||
ok-all bundle everything except mcu-bringup, which klipper pulls in
|
||||
```
|
||||
|
||||
nginx and moonraker were one script, `S60web`, until they were split. They fail
|
||||
separately and are debugged separately — nginx not starting is a missing config
|
||||
or a port already bound, and you know within a second; moonraker not starting
|
||||
is a component that failed to import, and it takes a background check and a log
|
||||
tail to even notice. One script had to say `web: stopped` for both, and
|
||||
`status` reported on two unrelated things at once. Now `S62moonraker restart`
|
||||
over ssh — the one you run over and over while chasing a moonraker config — no
|
||||
longer takes the web UI down with it. The numbering keeps the order: nginx
|
||||
first, because it proxies the other two, so a browser gets a page saying the
|
||||
backend is not up yet rather than a refused connection.
|
||||
`firmwareExe` starts `s6-svscan`, runs `s6-rc-init`, and asks for one machine
|
||||
state — that transition is the whole boot order. There is no `init.d/`
|
||||
directory and no wrapper scripts: **s6-rc is the command line**.
|
||||
|
||||
Two sourced libraries sit beside them, both installed to `/usr/data/anvil` and
|
||||
neither executable:
|
||||
```sh
|
||||
s6-rc -u change moonraker # start it (and its dependencies)
|
||||
s6-rc -d change moonraker # stop it (and whatever depends on it)
|
||||
s6-svc -r /usr/data/anvil/etc/s6/moonraker # restart, leaving s6-rc's view intact
|
||||
s6-svstat /usr/data/anvil/etc/s6/moonraker # is it up, and for how long
|
||||
s6-rc -a list # what s6-rc believes is up
|
||||
```
|
||||
|
||||
Two things are worth knowing before you type them. `-d change` follows
|
||||
dependencies, so stopping moonraker also stops `ff-startup` and therefore the
|
||||
screen — that is correct, there is nothing for a printer UI to show, and
|
||||
`-u change` brings them back. And `moonraker` and `camera` are the only
|
||||
services with a `notification-fd`, so only their "up" means *usable* rather
|
||||
than *forked*; `klipper`'s does not, because klippy's readiness is a state on
|
||||
moonraker's API rather than something klippy reports.
|
||||
|
||||
nginx is deliberately **not** a dependency of moonraker even though it proxies
|
||||
it: an edge would mean stopping nginx also stopped the API, which is the thing
|
||||
splitting the old single `S60web` script in two was meant to prevent.
|
||||
|
||||
One sourced library sits beside them, installed to `/usr/data/anvil` and not
|
||||
executable:
|
||||
|
||||
* **`anvil-env.sh`** — `PATH`, `LD_LIBRARY_PATH` and `FF_PYTHON`, in the one
|
||||
place that defines them. `run-append.sh`, `firmwareExe`, `start.sh` and every
|
||||
service script source it. A private copy per script is how the installer's
|
||||
check and the boot script came to disagree about the library list, with
|
||||
moonraker dying on `libpython3.8.so.1.0: cannot open shared object file`.
|
||||
* **`anvil-service.sh`** — the shape every service script has: `svc_say` /
|
||||
`svc_warn` for the boot log, `svc_pid_alive` and `svc_proc_alive` for
|
||||
liveness, `svc_start_daemon` / `svc_stop_daemon` around busybox
|
||||
`start-stop-daemon`, `svc_detach` for a wait that must not hold the boot up,
|
||||
and `svc_dispatch` for the `start|stop|restart|status` block. The scripts
|
||||
were written months apart and had arrived at four different answers to "is it
|
||||
still running?" and two to `restart`; none of those differences were
|
||||
decisions. The library also carries the two corrections busybox needs —
|
||||
`start-stop-daemon -K` returns before the process is dead, and does not
|
||||
remove the pidfile, which is how `restart` raced its own start and how
|
||||
`status` came to report a recycled pid as a running service. A script that
|
||||
cannot find this file says so and exits rather than half-starting.
|
||||
|
||||
Everything the mod installs lives under `/usr/data/anvil` on the **data**
|
||||
partition, which a FlashForge OTA cannot delete. The software component itself
|
||||
@@ -104,7 +109,7 @@ payload unpacks to `/usr/data/anvil`, and that is the whole installation —
|
||||
there is no separate Moonraker step that can fail on its own. Nothing is
|
||||
written to `/usr/prog`: FlashForge's tree at `/usr/prog/moonraker/moonraker/`
|
||||
is left exactly where it is and simply never used again, and
|
||||
`S62moonraker` starts ours by absolute path with no fallback.
|
||||
the moonraker service starts ours by absolute path with no fallback.
|
||||
|
||||
An earlier release copied the same tree over `/usr/prog/moonraker` as well.
|
||||
That is gone. It put a second, byte-identical Moonraker on the one partition
|
||||
@@ -116,14 +121,14 @@ Both things thought to require that location turned out not to: the
|
||||
`moonraker-env` virtualenv beside it was never on `sys.path` (checked by
|
||||
running the printer's own interpreter against the real image), and
|
||||
`moonrakerDaemon`, the one thing that did exec that tree by absolute path, is
|
||||
never invoked — `S62moonraker` starts the server itself.
|
||||
never invoked — the moonraker service starts the server itself.
|
||||
|
||||
**FlashForge's python 3.8.2 is not what this runs on any more.** `FF_PYTHON`
|
||||
in `anvil-env.sh` names a CPython 3.13 of our own, cross-built for mipsel
|
||||
(`bin/patch.sh` section 5c) with every third-party C extension Moonraker
|
||||
needs beside it in `$MODDIR/lib/python3.13/site-packages` and libsodium in
|
||||
`$MODDIR/lib` — none of it borrowed from `/usr/prog`. Measured through the
|
||||
real boot path (S40s6's scandir, `S62moonraker`, readiness gating on `:7125`
|
||||
real boot path (the scandir, the moonraker service, readiness gating on `:7125`
|
||||
actually listening, a `kill -9` respawn, a stop that stays stopped) in
|
||||
`test/integration/printer/case-moonraker313-s6.sh`. klippy is not part of
|
||||
this: it stays on FlashForge's 3.8.2, started independently by
|
||||
@@ -213,7 +218,7 @@ One thing it does not cover, worth knowing before you rely on it:
|
||||
|
||||
* **Moonraker.** The mod's Moonraker lives on the data partition at
|
||||
`/usr/data/anvil/moonraker`, and it is started by
|
||||
`/usr/data/anvil/init.d/S62moonraker` — which a stock flash removes along
|
||||
the mod's own moonraker service — which a stock flash removes along
|
||||
with the rest of the mod's boot path. So a stock reflash goes back to
|
||||
FlashForge's own 2022 tree at `/usr/prog/moonraker/moonraker/`, which was
|
||||
never touched and is still exactly what the factory image shipped. The mod's
|
||||
|
||||
@@ -141,6 +141,15 @@ directory under `etc/s6/` with a `run` script, and a `down` file when its
|
||||
people's hands; the script becomes a thin wrapper over `s6-svc`. Changing the
|
||||
supervisor is not a reason to change the interface.
|
||||
|
||||
> **REVERSED IN PHASE 8, deliberately.** The rule was right while a wrapper
|
||||
> still did something -- prerequisites, the `MOD_*` gate, a liveness check.
|
||||
> Once every one of those moved into the `run` script (where it has to live
|
||||
> anyway, because s6 restarts a service without going near a shell script),
|
||||
> the wrapper was verb translation and nothing else, in a directory whose
|
||||
> `S<NN>` numbering encoded an ordering that no longer existed. `init.d/` is
|
||||
> deleted and `s6-rc` is the interface. The cost is real and is paid once:
|
||||
> `S60nginx restart` is gone from people's hands and from the docs.
|
||||
|
||||
Camera is the interesting one: its 30-second `/dev/video0` poll becomes a
|
||||
readiness notification, so whatever waits for the camera waits for *ready*
|
||||
rather than for *forked*.
|
||||
@@ -301,6 +310,451 @@ mod that depends on no FlashForge version string, so a stock OTA cannot break a
|
||||
modded printer between our releases, and a single place to look when something
|
||||
does not start. That is worth having. "Survives a flash" is not the reason.
|
||||
|
||||
## Phase 8 -- hand the boot to s6-rc
|
||||
|
||||
**Numbered last because it was written last, not because it must run last.**
|
||||
Its hard predecessors are phases 1, 3, 4 and 5: it needs the manifest (shipped
|
||||
-- `payload/run-append.sh` is on the manifest path today), a scanner (shipped
|
||||
-- `payload/init.d/S40s6`), and the three services already living in the
|
||||
scandir. It needs nothing from phase 6 and nothing from phase 7, and phase 7
|
||||
does not need it. It can go before either, after either, or between them.
|
||||
**Klipper came in after all, and phase 7 did not get bigger.** The plan said
|
||||
Klipper was explicitly out of this phase. It is in it: an ordinary longrun
|
||||
whose `run` script is `klipperDaemon`'s command line with the `-b` removed,
|
||||
exactly as the fact list above predicted it would be. What phase 7 was actually
|
||||
about -- shipping the klippy tree into `$MODDIR`, owning the Python, moving
|
||||
`/usr/data/config` -- is untouched and still ahead. Supervising a program is
|
||||
not the same as owning where it lives, and conflating the two is what kept
|
||||
Klipper out of a phase it belonged in.
|
||||
|
||||
**What it is for.** `firmwareExe` runs seven `init.d/S*` scripts one at a time
|
||||
in the foreground and that loop IS the boot order (`payload/firmwareExe` lines
|
||||
63-69). Three of those seven are already thin -- `S60nginx`, `S62moonraker` and
|
||||
`S65camera` are `svc_s6_*` wrappers over a supervisor that is already running.
|
||||
What is left in the loop is not supervision, it is *sequencing*, and it is
|
||||
sequencing expressed as filenames. This phase moves the sequencing into a
|
||||
compiled s6-rc database, so that `firmwareExe` asks for a machine STATE
|
||||
(`s6-rc -u change ok-all`) instead of running seven programs and hoping.
|
||||
|
||||
### The graph, and what its edges do NOT promise
|
||||
|
||||
**There is a real dependency graph now, and it is small.** Written out, with
|
||||
the reason for every edge that exists and every one that does not:
|
||||
|
||||
mcu-bringup oneshot no dependencies
|
||||
klipper longrun dependencies.d/mcu-bringup
|
||||
wifi oneshot no dependencies
|
||||
nginx longrun no dependencies
|
||||
moonraker longrun no dependencies
|
||||
camera longrun no dependencies
|
||||
ff-startup oneshot dependencies.d/{klipper,moonraker}
|
||||
ui longrun dependencies.d/ff-startup
|
||||
ok-all bundle contents.d/{wifi,nginx,moonraker,camera,klipper,
|
||||
ff-startup,ui}
|
||||
|
||||
`mcu-bringup` is not in the bundle: it is pulled in as klipper's dependency,
|
||||
which is the point of having one.
|
||||
|
||||
**The three edges are ORDERING, and only one of them is also readiness.** This
|
||||
distinction is the whole honesty of the section and it is easy to lose:
|
||||
|
||||
* `s6-rc` counts a **longrun** up as soon as it is forked, *unless* its
|
||||
servicedir carries a `notification-fd`, in which case it waits for the
|
||||
service to say it is ready.
|
||||
* `moonraker` and `camera` have one. Their readiness is real: `moonraker` is
|
||||
counted up when it has BOUND :7125, the camera when the streamer has bound
|
||||
:8080 with a device behind it.
|
||||
* `klipper` does **not**. klippy's "ready" is a state on Moonraker's API --
|
||||
there is no "Printer is ready" line even in `printer.log`, which is why
|
||||
`S70klipper` used to tail for `Stats <n>:` -- so `ff-startup` depending on
|
||||
`klipper` means "after klippy was LAUNCHED", not "after klippy is usable".
|
||||
`bin/ff-startup.py` therefore keeps its own poll of `klippy_state` and its
|
||||
own retry loop. Giving klipper a real `notification-fd` means teaching
|
||||
klippy to write to one, and that is a separate change; it is not attempted
|
||||
here and it must not be claimed.
|
||||
|
||||
So `ui` depending on `ff-startup` genuinely does mean "the screen appears when
|
||||
the printer is ready", because `ff-startup` is a oneshot and a oneshot is not
|
||||
done until its `up` has returned -- and its `up` is the program that waits.
|
||||
That is the edge the phase was worth doing for.
|
||||
|
||||
**nginx before moonraker is still NOT an edge, for the reason it never was.**
|
||||
`S60nginx`'s header argues that nginx should start first because it proxies
|
||||
both moonraker and the camera, and a browser then gets a page saying the
|
||||
backend is not answering rather than a refused connection. That is a
|
||||
PREFERENCE, not a dependency, and encoding it as one has a price: a
|
||||
`dependencies.d/nginx` under moonraker means `s6-rc -d change nginx` also stops
|
||||
moonraker, so `S60nginx stop` would take the API down with the UI -- the exact
|
||||
thing splitting `S60web` into two scripts was done to prevent. So the edge is
|
||||
not written, s6-rc brings those two up in parallel, and the cost is a second or
|
||||
two in which a reloading browser gets a refused connection instead of a 502.
|
||||
|
||||
**What the edges cost, said plainly.** `s6-rc -d change moonraker` now also
|
||||
stops `ff-startup` and therefore the UI, because both depend on it. That is
|
||||
correct -- there is nothing for a printer UI to show when there is no printer
|
||||
-- but it is a behaviour change from the days when each `S*` script stopped one
|
||||
thing, and both `S62moonraker` and `S70klipper` say so in their `stop()`.
|
||||
|
||||
**And a boot that waits.** The transition is what `firmwareExe` blocks on, so
|
||||
readiness that used to be reported by a detached background job is now
|
||||
something the boot pays for: moonraker's up to `timeout-up` = 120s, the
|
||||
camera's 40s on a printer that has no camera, `ff-startup`'s up to 300s. Every
|
||||
one of those services has a `timeout-up`, and that is not tidiness -- without
|
||||
one, a camera that never appears would hold the transition open for ever and
|
||||
the UI would never start.
|
||||
|
||||
### Does s6-rc need execline? Yes, and here is the bill
|
||||
|
||||
Phase 2 says "execline is **not** needed: `run` scripts are plain `#!/bin/sh`".
|
||||
That survives for longruns and fails for everything else, and both halves were
|
||||
read out of the s6-rc 0.7.0.0 source rather than assumed.
|
||||
|
||||
* **Longruns keep plain `#!/bin/sh`.** `s6-rc-compile` only wraps a `run`
|
||||
script in an execlineb wrapper when the service is *pipelined*
|
||||
(`src/s6-rc/s6-rc-compile.c` lines 1375-1379, guarded on `ispipelined`).
|
||||
Pipelines are s6-rc's logger mechanism and we ship no loggers, so our three
|
||||
existing `run` scripts are copied verbatim. Nothing about phase 4 or 5 has to
|
||||
be rewritten.
|
||||
* **execline is a build-time AND run-time requirement anyway.** skarnet says so
|
||||
outright on <https://skarnet.org/software/s6-rc/>, `package/deps-build` lists
|
||||
`execline 2.9.9.2`, and `s6-rc-compile` links `-lexecline`
|
||||
(`package/deps.mak` line 136). The runtime half is concrete rather than
|
||||
theoretical: `write_oneshot_runner` is called unconditionally
|
||||
(`s6-rc-compile.c` line 1592) and emits a servicedir whose `run` begins
|
||||
`#!<execlinebprefix>execlineb -s1` and then execs `fdmove`,
|
||||
`s6-ipcserver-socketbinder`, `s6-ipcserverd`, `s6-ipcserver-access`,
|
||||
`s6-sudod` and `libexec/s6-rc-oneshot-run` (lines 1083-1092). Every oneshot's
|
||||
`up` runs through that service. We are adding a oneshot, so we are adding
|
||||
execline.
|
||||
* **The oneshot `up` file itself does not have to be execline.** It is lexed by
|
||||
execlineb at compile time and stored as an argv, and skarnet documents
|
||||
`/bin/sh -c "script"` and "just call a script stored somewhere else" as
|
||||
supported shapes. Every `up` we write will be the latter, so no execline
|
||||
command beyond `execlineb` and `fdmove` is ever exec'd.
|
||||
|
||||
The bill, against the ~813KB + 116KB s6 already costs: two more pinned tarballs
|
||||
with sha256s (`execline`, `s6-rc`), two more cross-builds in `bin/patch.sh`
|
||||
section 5b -- the same toolchain, the same wrapper discipline, the same cache
|
||||
stamp -- and more entries in the ABI gate, whose expected-count assertion
|
||||
(`bin/patch.sh`, the `S6_ELF` check) has to move with them.
|
||||
|
||||
**The binary set was MEASURED, and it is bigger than reading the oneshot
|
||||
runner alone suggests.** The estimate above -- four new s6 names and two
|
||||
execline binaries -- was made from `write_oneshot_runner`. `s6-rc-compile` also
|
||||
emits an `s6rc-fdholder` servicedir, and that one execs `pipeline`, `if`,
|
||||
`forstdin`, `exit`, `redirfd`, `s6-ipcclient`, `s6-fdholder-daemon`, `s6-svc`
|
||||
and `libexec/s6-rc-fdholder-filler`. Read off the generated `servicedirs/*/run`
|
||||
of a real compile, the additions are **seven** execline binaries (`execlineb`,
|
||||
`fdmove`, `pipeline`, `if`, `forstdin`, `exit`, `redirfd`), six s6 binaries we
|
||||
build but do not ship today (`s6-ipcserver-socketbinder`, `s6-ipcserverd`,
|
||||
`s6-ipcserver-access`, `s6-sudod`, `s6-fdholder-daemon`, `s6-ipcclient`),
|
||||
`s6-rc`, `s6-rc-init` and `s6-rc-db`, and two `libexec` helpers
|
||||
(`s6-rc-oneshot-run`, `s6-rc-fdholder-filler`). Stripped, for this target:
|
||||
|
||||
today, the s6 subset + s6-ftrigrd 1434 KB
|
||||
added by s6-rc 1309 KB
|
||||
|
||||
So s6-rc roughly DOUBLES what supervision costs on the data partition, and it
|
||||
does so to buy a boot order that is three real edges wide (see the section
|
||||
above) and was previously spelled as filenames. That is the honest trade and it should be read next to the
|
||||
free-space unknown phase 6 records: nobody has ever run `df` on a real printer.
|
||||
`s6-rc-compile` (131KB) is host-side only and is not shipped.
|
||||
|
||||
**Both new packages must be configured `--prefix=/usr/data/anvil`, for a reason
|
||||
one level worse than the `s6-ftrigrd` trap.** execline's `--shebangdir`
|
||||
defaults to `$bindir` and is baked into `EXECLINE_SHEBANGPREFIX`
|
||||
(`execline-2.9.9.2/configure` lines 152, 587); `s6-rc-compile` pastes that
|
||||
string into the `#!` line of the oneshot runner it generates. So the prefix
|
||||
that ends up INSIDE the shipped database is a property of the execline that the
|
||||
*compiler* was linked against, not of the execline we ship. Get it wrong and
|
||||
every oneshot fails at exec with ENOENT while every longrun keeps working --
|
||||
late and partial, exactly as `tools/supervisor/README.md` describes for the
|
||||
waiting verbs.
|
||||
|
||||
### Where the database lives, and when it is compiled
|
||||
|
||||
**Compiled on the build host, in `bin/patch.sh`, and shipped.** The database
|
||||
files are architecture-neutral by construction: `s6-rc-compile` writes every
|
||||
integer with `uint32_pack_big` (`s6-rc-compile.c` lines 1037-1042, 1199, 1214,
|
||||
1482) and the runtime reads them back with `uint32_unpack_big`
|
||||
(`src/libs6rc/s6rc_db_read_uint32.c` line 12), so nothing native-word-sized or
|
||||
host-endian reaches the file; `resolve.cdb` is djb's fixed cdb format. What is
|
||||
NOT neutral is the `servicedirs/` subdirectory, because of the shebang above --
|
||||
which is a configure problem, not a portability problem, and is solved by
|
||||
building s6-rc twice from one tarball: once for the host to get a
|
||||
`s6-rc-compile` configured with the printer's prefixes, once for mipsel to get
|
||||
`s6-rc`, `s6-rc-init`, `s6-rc-db` and `libexec/s6-rc-oneshot-run`. That is the
|
||||
shape section 5c already uses for CPython, where a throwaway x86-64 3.13.7 is
|
||||
built from the same tarball as the shipped mipsel one.
|
||||
|
||||
Compiling on the printer at install time was the alternative and is rejected:
|
||||
it means shipping `s6-rc-compile` plus the execline lexer plus the whole source
|
||||
tree, and it adds a way for `run-append.sh` to fail that ends in a printer with
|
||||
no services and a log nobody will read. The database is a build artefact. Build
|
||||
artefacts are built where the build is.
|
||||
|
||||
**Paths.** The compiled database ships at
|
||||
`$MODDIR/etc/s6-rc/compiled/<version>` with
|
||||
`$MODDIR/etc/s6-rc/compiled/current` a symlink to it -- skarnet's own advice on
|
||||
the `s6-rc-init` page, so the boot command never has to change when the
|
||||
database does. It is under `$MODDIR/etc/`, not `/etc/s6-rc/`, which is
|
||||
`s6-rc-init`'s default and is unreachable: `/etc` is inside the read-only
|
||||
squashfs (`docs/printer-replica.md`, the mount table). Configure with
|
||||
`--bootdb=/usr/data/anvil/etc/s6-rc/compiled/current` so a hand-run
|
||||
`s6-rc-init` over ssh is right too, and pass `-c` explicitly anyway.
|
||||
|
||||
**The live directory is `/run/s6-rc`**, which is s6-rc's own default.
|
||||
`s6-rc-init` needs it writable and skarnet recommends a RAM filesystem; `/run`
|
||||
is tmpfs on this machine (`docs/printer-replica.md`, mount table, copied from
|
||||
`/etc/fstab` and `/etc/init.d/S21mount_mmc_ext4`). That gives the reboot answer
|
||||
for free: tmpfs comes up empty, so `s6-rc-init`'s requirement that the live
|
||||
directory "should not exist prior" holds by construction on every boot, and
|
||||
nothing has to clean up after a crash. Across an UPGRADE the live directory
|
||||
survives, still pointing at a compiled database the manifest pass is about to
|
||||
delete -- see below.
|
||||
|
||||
### What starts the tree, and what happens when it dies
|
||||
|
||||
**This is where the plan changed, and it changed for the better.** Phase 3 said
|
||||
nothing supervises the supervisor, that a respawn loop in `ash` would hide a
|
||||
real fault, and that the honest fix would be inittab-style respawn. There is
|
||||
one, and it was already on the printer.
|
||||
|
||||
`firmwareExe` starts `s6-svscan` AS ITS OWN CHILD, runs `s6-rc-init`, issues
|
||||
one `s6-rc -u change` for the boot set, and then blocks in `wait
|
||||
"$SCANNER_PID"`. That `wait` is what holds the foreground for the stock
|
||||
`app_startup.sh` watchdog, which greps `ps` for the name `firmwareExe` five
|
||||
seconds after launch -- the job the old `while true; do sleep 3600; done` tail
|
||||
did, now done by waiting on something real.
|
||||
|
||||
When the scanner dies, `wait` returns, `firmwareExe` exits, and
|
||||
`app_startup.sh` -- FlashForge's own stock init, which the mod never patches --
|
||||
re-execs it. The whole tree comes back. That is not a hand-rolled supervisor:
|
||||
there is no loop, nothing is retried in place, and nothing is hidden.
|
||||
|
||||
Three details are load-bearing:
|
||||
|
||||
* **The scanner must be `firmwareExe`'s own child.** `wait` only works on a
|
||||
process the calling shell forked. Backgrounding it inside `S40s6 start`, as
|
||||
phase 3 did, puts it in a subshell of another program. `S40s6` keeps its own
|
||||
copy of the sequence for the ssh path, and every step of it is a no-op when
|
||||
the step has already happened.
|
||||
* **Not `exec s6-svscan`.** The watchdog greps for the name; `exec` would make
|
||||
`$0` `s6-svscan`, and busybox `ash` has no `exec -a`. The shell also has to
|
||||
run `s6-rc-init` *after* the scanner is up, which an `exec` forecloses.
|
||||
* **`wait "$SCANNER_PID"`, not bare `wait`.** Services and `ff-startup` leave
|
||||
background children around; a bare `wait` would return on the wrong one.
|
||||
|
||||
What it does NOT buy: it is one restart of EVERYTHING, not a targeted restart
|
||||
of the scanner. klippy goes down with it, mid-print. A printer whose scanner
|
||||
dies twice has a real fault -- realistically the `-D_FILE_OFFSET_BITS=64` build
|
||||
trap -- and the second time round this looks like a boot loop, which is what a
|
||||
boot loop is for.
|
||||
|
||||
### `init.d/` is deleted
|
||||
|
||||
Phase 4 promised the `S*` scripts would survive as thin wrappers. They did not,
|
||||
and the reason is worth stating because it is the same argument the `run`
|
||||
scripts already make: a service that only works when a shell script happened to
|
||||
run first is not supervised, it is lucky. Every prerequisite a wrapper did --
|
||||
the `mkdir`, the leftover sweep, the binary check -- has to be 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>`
|
||||
numbering encoded an ordering that no longer existed.
|
||||
|
||||
So `s6-rc` is the interface. `s6-rc -u change nginx`, `s6-rc -d change nginx`,
|
||||
`s6-svc -r $SCANDIR/nginx`, `s6-svstat $SCANDIR/nginx`. `S60nginx restart` is
|
||||
gone from people's hands and from `docs/`.
|
||||
|
||||
* **`payload/anvil-service.sh` is deleted too.** With the wrappers gone its
|
||||
only caller was `firmwareExe`, and a shared library for one consumer is the
|
||||
redundancy the wrappers were. What `firmwareExe` uses is inlined there: the
|
||||
paths, the `PATH` prepend, the scanner fork, `s6-rc-init`, the boot set.
|
||||
* **The tree-starter exists once**, in `firmwareExe`. There is no `S40s6` and
|
||||
no replacement helper. The ssh recovery path is plain `s6-rc` commands.
|
||||
* **`bin/ff-startup.py` is two services**, `mcu-bringup` and `ff-startup`. A
|
||||
compiled database cannot read `anvil.conf`, so `MOD_STARTUP`, `MOD_IMPORT`,
|
||||
`MOD_STARTUP_TIMEOUT` and `MOD_KLIPPER_TRIES` become argv inside the two
|
||||
`up` files, which source `anvil.conf` at runtime.
|
||||
* **wifi is a oneshot**, not a "do the work then sleep forever" longrun -- s6
|
||||
would respawn a longrun that had finished. Its `up` returns when the
|
||||
supplicant is launched, not when there is an address, which is safe only
|
||||
because NOTHING DEPENDS ON WIFI. A oneshot that returns early lies to its
|
||||
dependents.
|
||||
* **klipper loses its retry loop and `force-start`** -- `ff-startup` owns the
|
||||
retry, with moonraker to ask instead of `printer.log` to tail, and nothing
|
||||
stands aside from anything any more. It restarts with `s6-svc -wr -t` rather
|
||||
than two s6-rc transitions, because `ff-startup` calls it from inside one and
|
||||
`s6-rc` takes an exclusive lock on the live directory.
|
||||
* **`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 every one of them would fork a second,
|
||||
unsupervised klippy beside the supervised one. `stop`, `restart` and `status`
|
||||
redirect at the service rather than reimplementing pidfile semantics nothing
|
||||
writes any more.
|
||||
* **The UI is a supervised longrun.** The plan said it could not be: the UI had
|
||||
to hold `firmwareExe`'s foreground for the watchdog. `firmwareExe` holds its
|
||||
own foreground on `wait` now, so the foreground is not available to lend and
|
||||
`$MODDIR/.ui-choice` is deleted rather than left unread.
|
||||
|
||||
**BLOCKING UNKNOWN: does `helix-screen` run under `s6-supervise`?** Nothing has
|
||||
proved it. A supervised process gets the descriptors `s6-supervise` hands it,
|
||||
no controlling terminal, and a session it did not create; until now the UI
|
||||
inherited `firmwareExe`'s. The environment half is easy and is in
|
||||
`etc/s6-rc/source/ui/run` -- `HELIX_DATA_DIR`, `HELIX_LOG_DEST`,
|
||||
`HELIX_LOG_FILE`, `HELIX_DISABLE_AUTO_UPDATES`, exactly what `firmwareExe`
|
||||
exported. A tty is not something a `run` script can conjure. This has to be
|
||||
settled on the replica before the change ships; if it turns out a terminal is
|
||||
needed, the answer is s6's own tty handling or a small wrapper, NOT a return to
|
||||
the foreground, which no longer exists.
|
||||
|
||||
`firmwareExe`'s loop over `$MODDIR/init.d/S*` is gone and nothing replaces it:
|
||||
the body is the scanner, `s6-rc-init`, one `s6-rc -u change`, and `wait`.
|
||||
|
||||
### The `MOD_WEB` / `MOD_CAM` gate survives, and gets better
|
||||
|
||||
**The three `down` files are deleted, and the reason is not the one the manual
|
||||
gives.** MEASURED: `s6-rc-compile` does not refuse a `down` file in a definition
|
||||
directory -- it ACCEPTS it and SILENTLY DROPS it, producing a byte-identical
|
||||
servicedir. So "not started by default" in s6-rc is not a file at all; it is
|
||||
membership of the `ok-all` bundle. The mechanism whose long argument was
|
||||
written inside `payload/etc/s6/nginx/down` stops existing either way.
|
||||
|
||||
What replaces it is strictly better, and it is worth being explicit because the
|
||||
`down` file's own text spends thirty lines on this exact point: the gate has to
|
||||
be a RUNTIME read of `anvil.conf`, because the payload is built once and
|
||||
`anvil.conf` is edited over ssh. Under s6-rc the gate is the ARGUMENT LIST of
|
||||
`s6-rc -u change` -- `svc_rc_enabled` in `anvil-service.sh` reads the switches
|
||||
at runtime, `svc_rc_boot_set` expands the `ok-all` bundle with `s6-rc-db
|
||||
contents` and drops what is switched off, and both `firmwareExe` and each
|
||||
`S*` script's `start()` ask the same function, so they cannot disagree. `MOD_CAM=0` means camera is never named, so it never starts;
|
||||
`MOD_WEB=0` means neither nginx nor moonraker is named. A printer upgrading
|
||||
with `MOD_CAM=0` already set needs no action at all and behaves identically.
|
||||
That is the whole upgrade path for the flags, and it is why they are kept
|
||||
rather than dropped.
|
||||
|
||||
The one thing genuinely lost is the corner case the `down` file's own "THE
|
||||
COST" paragraph describes -- and it is lost by being FIXED. There, a killed
|
||||
`s6-supervise` was replaced by a fresh one that re-read `down` and left the
|
||||
service stopped; under s6-rc the live servicedir's `down` file is managed
|
||||
against s6-rc's current state, so the replacement supervisor reads what the
|
||||
machine is supposed to be doing. The reasoning in the deleted file does not
|
||||
disappear: it moves to `svc_rc_enabled` and `svc_rc_boot_set` in
|
||||
`anvil-service.sh`, which is where the decision now lives.
|
||||
|
||||
### Upgrade
|
||||
|
||||
Phase 1 is already shipped, which is what makes this section short. The
|
||||
compiled database, the source definitions and everything else are staged under
|
||||
`$MOD_PAYLOAD` and therefore named in `.install-manifest` (`bin/patch.sh`
|
||||
section 10b), so `run-append.sh`'s manifest pass deletes the previous
|
||||
database and extracts the new one with no new code. **`s6-rc-update` is
|
||||
deliberately not used**: it exists to swap a compiled database under a live
|
||||
machine, and a flash ends in a reboot. Reaching for it would mean the installer
|
||||
mutating a running supervision tree on a machine that is about to restart
|
||||
anyway.
|
||||
|
||||
Two things do need writing.
|
||||
|
||||
* **The scandir MUST be swept, and the manifest cannot do it.** MEASURED:
|
||||
`s6-rc-init` creates one symlink per service in the scandir and fails
|
||||
outright -- `unable to supervise service directories ...: File exists` -- if
|
||||
a name is taken. A printer upgrading from the pre-s6-rc payload has `nginx`,
|
||||
`moonraker` and `camera` sitting there as real directories. The manifest pass
|
||||
deletes every FILE it listed and then `rmdir`s the directories, but
|
||||
`s6-supervise` created `supervise/` and `event/` inside each of them at
|
||||
RUNTIME, and no manifest can list a file the previous install did not write
|
||||
-- so the `rmdir` correctly refuses and leaves exactly the name `s6-rc-init`
|
||||
collides with. `run-append.sh` therefore does `rm -rf $MODDIR/etc/s6` and
|
||||
recreates it, after extraction, plus `rm -rf /run/s6-rc` for the person who
|
||||
runs `sh run.sh` over ssh instead of rebooting.
|
||||
* **The legacy sweeps stay.** `S62moonraker`'s `/run/moonraker.pid` cleanup
|
||||
(lines 369-388) and `S65camera`'s `LEGACY_PIDFILE` block (lines 225-238) are
|
||||
the same class of problem one release further on, and this phase adds a third
|
||||
instance of it: a printer that boots the new payload while an old
|
||||
`s6-supervise` from the pre-s6-rc scandir is still alive has two supervisors
|
||||
fighting for `:80`. The reboot settles it; the ssh path has to sweep it.
|
||||
|
||||
### SETTLED: both of this phase's blocking unknowns were measured
|
||||
|
||||
Run on 2026-08-28, in the replica, against the real cross-built stack --
|
||||
skalibs 2.15.1.0, execline 2.9.9.2, s6 2.15.1.0 and s6-rc 0.7.0.0, all four
|
||||
built twice from ONE set of tarballs (the skalibs and s6 sha256s are the ones
|
||||
already pinned in `versions.env`): natively, to get an `s6-rc-compile` that
|
||||
runs on the build host, and for `mipsel-buildroot-linux-musl` with the Bootlin
|
||||
toolchain `versions.env` pins, both configured `--prefix=/usr/data/anvil`.
|
||||
|
||||
**A host-compiled database IS accepted, byte for byte.** The same two-service
|
||||
source tree -- one oneshot, one longrun, the shapes `S50wifi` and `S60nginx`
|
||||
become -- was compiled by the host `s6-rc-compile` and by the target one under
|
||||
qemu. `db`, `n` and `resolve.cdb` are identical (`db` md5
|
||||
`80f5261437191fa591744ada6087c6f7` both ways) and `diff -r` reports the
|
||||
generated `servicedirs/` trees identical too. The endianness argument holds.
|
||||
Then the HOST-compiled database was run: `s6-rc-init` returned 0, `s6-rc -u
|
||||
change ok-all` returned 0 with no warnings, the longrun came up with a
|
||||
`supervise/` directory, and the oneshot's `up` actually executed -- its stamp
|
||||
file was written -- which is the half that goes through execline. `s6-svscan`'s
|
||||
log stayed empty. **Nothing about the database needs to be compiled on the
|
||||
printer, and `s6-rc-update` is not needed to make one usable.**
|
||||
|
||||
**`/run` is a tmpfs and is writable.** `rw,seclabel,relatime,inode64`, no
|
||||
`noexec`, and a write to it succeeded. Use `/run/s6-rc` as the live directory
|
||||
as designed; the `/tmp/s6-rc` fallback is not needed. Note this was measured in
|
||||
the replica, which models the machine but is not one -- the replica does mount
|
||||
the chroot root READ-ONLY (`overlay ... (ro)`), which is the property that
|
||||
matters here, and only `/usr/data`, `/tmp`, `/run` and `/dev` are writable.
|
||||
|
||||
**Two traps found by running it, both of which would have cost a day on a
|
||||
printer.** Neither is a reason not to do the phase; both are things the gate
|
||||
must assert.
|
||||
|
||||
* **`s6-svscan` resolves `s6-supervise` through `PATH`.** Started from a shell
|
||||
without `$MODDIR/bin` on `PATH`, the scanner answers `s6-svscanctl -a`
|
||||
perfectly and then fails every spawn with `unable to spawn s6-supervise for
|
||||
X: No such file or directory`, forever, in a log nobody is reading.
|
||||
`s6-rc-init` hangs rather than fails while this is true. This is the same
|
||||
class as the `s6-ftrigrd` trap and `payload/anvil-env.sh` lines 160-174
|
||||
already documents it for `s6-svlisten` -- but it applies to the scanner
|
||||
itself, and `S40s6` must not assume its caller's environment.
|
||||
* **`s6-rc-init` creates one symlink per service in the scandir it is given,
|
||||
and collides with what is already there.** Pointed at a live
|
||||
`$MODDIR/etc/s6` that already holds `nginx`, `moonraker` and `camera` from
|
||||
phases 4 and 5, it fails with `unable to supervise service directories ...:
|
||||
File exists`. So the cutover is not additive: the phase-4/5 servicedirs must
|
||||
be REMOVED from the scandir in the same change that introduces the database,
|
||||
and an upgrade that leaves them behind produces a printer whose supervision
|
||||
tree never initialises. This is the sharpest edge in the phase.
|
||||
|
||||
*Files:* `versions.env` and `bin/fetch-assets.sh` (execline, s6-rc),
|
||||
`bin/patch.sh` (5b grows a host and a target s6-rc, `S6_BINS` grows by the
|
||||
fifteen names the measurement above lists, the ABI gate count moves, a new step
|
||||
compiles the database),
|
||||
`payload/etc/s6-rc/source/` (new: the definition directories),
|
||||
`payload/etc/s6/*/down` (deleted), `payload/anvil-service.sh` (`svc_rc_*`),
|
||||
`payload/init.d/S40s6`, `S50wifi`, `S60nginx`, `S62moonraker`, `S65camera`,
|
||||
`payload/firmwareExe` (the loop goes).
|
||||
|
||||
*Gate:* `qa/replica/test_s6rc.py`, in `qa/` rather than as a new
|
||||
`case-*.sh` -- this phase's failures are per-assertion and
|
||||
`docs/qa-migration.md` is explicit about where an assertion belongs. Against a
|
||||
machine the real installer produced, it must show: the database the BUILD
|
||||
produced is the one running (`s6-rc-db -c .../current list all` from inside the
|
||||
chroot); `s6-rc -a list` reports the four services after the boot; with
|
||||
`MOD_CAM=0` it reports three and there is no `mjpg_streamer`, which is the
|
||||
negative control; `S60nginx stop` leaves moonraker answering, which is the
|
||||
property the `S60web` split bought and the property a wrong dependency edge
|
||||
would silently destroy; `kill -9` on nginx is respawned; the wifi oneshot
|
||||
reports up and the init sequence still returned promptly; and `S40s6 start`
|
||||
twice in a row does not fail. Two more, one per trap the experiment found:
|
||||
`S40s6` brings the tree up when invoked from an environment with an empty
|
||||
`PATH` (the scanner-spawn trap, which otherwise hangs `s6-rc-init` rather than
|
||||
failing it), and an upgrade FROM a phase-4/5 payload -- whose scandir still
|
||||
holds `nginx`, `moonraker` and `camera` -- initialises rather than colliding on
|
||||
`File exists`. Then one negative control on the phase itself:
|
||||
move the compiled database aside and assert the boot says so with `!!` and that
|
||||
Klipper still comes up, because Klipper is not in the database and must not
|
||||
care.
|
||||
|
||||
## How this gets implemented
|
||||
|
||||
Phases 1, 2 and 3 are sequential and all touch `run-append.sh`, `patch.sh` and
|
||||
@@ -313,6 +767,13 @@ script, one service directory and one gate, each in its own worktree.
|
||||
Phase 5 is one worker again: it touches user data and deserves undivided
|
||||
attention.
|
||||
|
||||
Phase 8 is one worker as well, and in two sittings with a review between them:
|
||||
the portability probe first (host-compiled database, run in the replica,
|
||||
nothing else touched), then everything else. It rewrites the boot path and
|
||||
touches `patch.sh`, `anvil-service.sh` and five init scripts at once, so
|
||||
fanning it out produces merge conflicts in exactly the files a bad merge is
|
||||
hardest to notice in.
|
||||
|
||||
Every agent follows the rule this project already has -- install the payload
|
||||
and run the real tools in the replica, assert on behaviour, always with a
|
||||
negative control. Never grep a shipped script to decide whether it works.
|
||||
+2
-2
@@ -32,7 +32,7 @@ documented in the file headers.
|
||||
| [`payload/klipper/config/ff-tool-offset.cfg`](../payload/klipper/config/ff-tool-offset.cfg) | `/usr/data/config/` | `[ff_tool_offset]` — probe geometry and guards for the calibration commands |
|
||||
| [`payload/klipper/config/ff-legacy.cfg`](../payload/klipper/config/ff-legacy.cfg) | `/usr/data/config/` | `[ff_legacy]` — stays included permanently; declares the section and, optionally, `firmware_config_dir` |
|
||||
| [`payload/bin/ffscreen.py`](../payload/bin/ffscreen.py) | `/usr/data/anvil/bin/` | A few lines of text and a progress bar drawn straight onto `/dev/fb0`, geometry read from sysfs. The framebuffer is **portrait 480×800@32** and the panel is that buffer turned 90° clockwise (landscape 800×480) — established from FlashForge's own `/usr/prog/start.img`, 1536000 bytes, which only decodes into a picture read that way. Drawing is done in landscape coordinates and each rectangle is rotated on the way into the buffer, so it costs arithmetic per rectangle and nothing per pixel. All of the first boot happens before HelixScreen starts, so without it the panel is black for the longest wait of the install — which reads as a brick and invites a power cut mid-`SAVE_CONFIG`. `make boot-screen` renders every frame to PNG on the host; `make boot-screen-sim` renders the same frames inside the replica using FlashForge's own python3 on MIPS (they come out byte-identical); `make test-boot-screen` is the gate |
|
||||
| [`payload/bin/ff-startup.py`](../payload/bin/ff-startup.py) | `/usr/data/anvil/bin/` | Everything before HelixScreen. **Every boot** it hands the toolhead boards over from their bootloaders (calling `ff_mcu_bringup.py` directly — it owns when klippy opens the ports, so it owns doing this first), starts klipper, and waits for klipper + moonraker to be ready, naming the board or service holding things up and re-handing the boards over on each retry. `init.d/S70klipper` stands aside for it and takes the job back if it cannot run — starting the UI before that is what produces a screen reporting a disconnected printer with no clue which board is missing. **First boot only**, once that has happened, it sends `FF_IMPORT_FIRMWARE_CONFIG` and `SAVE_CONFIG` over the moonraker API and stamps `/usr/data/anvil/.firmware-config-imported`. Only a verified save stamps, so a slow boot retries. `MOD_STARTUP=0` skips the lot; `MOD_IMPORT=0` keeps the wait and skips the migration |
|
||||
| [`payload/bin/ff-startup.py`](../payload/bin/ff-startup.py) | `/usr/data/anvil/bin/` | Everything before HelixScreen. **Every boot** it hands the toolhead boards over from their bootloaders (calling `ff_mcu_bringup.py` directly — it owns when klippy opens the ports, so it owns doing this first), starts klipper, and waits for klipper + moonraker to be ready, naming the board or service holding things up and re-handing the boards over on each retry. It runs as two s6-rc oneshots (`mcu-bringup`, then `ff-startup`), and the UI depends on the second — starting the UI before that is what produces a screen reporting a disconnected printer with no clue which board is missing. **First boot only**, once that has happened, it sends `FF_IMPORT_FIRMWARE_CONFIG` and `SAVE_CONFIG` over the moonraker API and stamps `/usr/data/anvil/.firmware-config-imported`. Only a verified save stamps, so a slow boot retries. `MOD_STARTUP=0` skips the lot; `MOD_IMPORT=0` keeps the wait and skips the migration |
|
||||
| [`payload/klipper/config/ff-print-macros.cfg`](../payload/klipper/config/ff-print-macros.cfg) | `/usr/data/config/` | `START_PRINT` / `END_PRINT` / `PAUSE` / `RESUME` / `CANCEL_PRINT`, reconstructed from the app's sequences, plus the `_FF_PREFLIGHT` calibration and tool-presence gate; declares `[ff_print]` and the `FF_BEFORE_PRINT_START` / `FF_AFTER_PRINT_END` entry points it calls |
|
||||
| [`payload/klipper/config/ff-filament.cfg`](../payload/klipper/config/ff-filament.cfg) | `/usr/data/config/` | `LOAD_FILAMENT` / `UNLOAD_FILAMENT` / `PURGE` — the touchscreen's filament-load sequence (grab tool, purge chute, feed) recovered from the binary; unload is a designed retract (the stock app has none) |
|
||||
| [`payload/klipper/config/ff-runout.cfg`](../payload/klipper/config/ff-runout.cfg) | `/usr/data/config/` | Runout / clog handling: gives the stock `fd_ex*` / `fm_ex*` sensors a `runout_gcode` that pauses a Mainsail print when the **mounted** tool runs out or clogs (the app's E0162 / E0163, reported here in plain words); `ff_toolchange` arms only the mounted tool's sensors |
|
||||
@@ -174,7 +174,7 @@ scp payload/klipper/config/ff-*.cfg \
|
||||
```
|
||||
|
||||
Only a save it can verify writes the stamp. A boot where the heater board
|
||||
needed several klippy restarts (`S70klipper`) simply times out and tries
|
||||
needed several klippy restarts simply times out and tries
|
||||
again next boot; `/usr/data/logs/anvil-boot.log` says which service it was
|
||||
waiting on.
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
# is how this list got to ten in the first place.
|
||||
#
|
||||
# Sourcing twice is safe: each directory is added only if it is not already
|
||||
# there, so firmwareExe sourcing this and then running init.d/S62moonraker --
|
||||
# there, so firmwareExe sourcing this and then s6 starting moonraker --
|
||||
# which sources it again with the first copy already inherited -- does not grow
|
||||
# the variable on every boot.
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
# NOT /usr/prog/mjpg-streamer: that one is a plugin directory rather than a
|
||||
# library package, it carries its own libjpeg.so.9, and putting it in front of
|
||||
# every python process invites a version conflict for the sake of one service.
|
||||
# init.d/S65camera prepends it for itself.
|
||||
# the camera's own s6-rc run script prepends it for itself.
|
||||
#
|
||||
# libsodium is NOT here any more. It existed only for 3.8's libnacl, in
|
||||
# FlashForge's site-packages, which asks the loader for libsodium.so.18 --
|
||||
@@ -115,8 +115,8 @@ export LD_LIBRARY_PATH
|
||||
# callers need -- tornado, lmdb, cffi, greenlet, libnacl -- is cross-built
|
||||
# beside it in $MODDIR/lib/python3.13/site-packages; and Moonraker has been
|
||||
# measured SERVING on that interpreter THROUGH THE REAL BOOT PATH on the
|
||||
# replica (test/integration/printer/case-moonraker313-s6.sh): S40s6's scandir,
|
||||
# S62moonraker, s6-svwait -U gating on :7125 actually listening rather than on
|
||||
# replica (test/integration/printer/case-moonraker313-s6.sh): the scandir, the
|
||||
# moonraker service, s6-svwait -U gating on :7125 actually listening rather than on
|
||||
# the process forking, a kill -9 respawning onto 3.13 again, stop staying
|
||||
# stopped, and a churn loop when site-packages is moved away -- not just an
|
||||
# interpreter that imports cleanly.
|
||||
@@ -132,7 +132,7 @@ export LD_LIBRARY_PATH
|
||||
# WHO THIS DOES NOT MOVE. Klipper is not on this list. It is started by
|
||||
# FlashForge's own /usr/prog/klipper/start.sh, hardcoded to
|
||||
# /usr/prog/Python-3.8.2/bin/python3, independently of FF_PYTHON -- see
|
||||
# init.d/S70klipper's own header. klippy's numpy gap
|
||||
# the klipper service's own run script. klippy's numpy gap
|
||||
# (extras/stepper_resonance_tester.py) is therefore not this switch's problem;
|
||||
# it stays open as a separate, smaller item.
|
||||
#
|
||||
@@ -168,10 +168,11 @@ esac
|
||||
# s6-svc: fatal: unable to exec s6-svlisten: No such file or directory
|
||||
#
|
||||
# It is here rather than in one init script because both halves need it and
|
||||
# they inherit it from different places: S40s6 sources this file before
|
||||
# they inherit it from different places: firmwareExe sources this file before
|
||||
# starting the SCANNER, which is the process that has to find s6-supervise,
|
||||
# and every service script sources it before running s6-svc, which is the
|
||||
# process that has to find s6-svlisten. Prepended, not appended: these are
|
||||
# and a shell running s6-svc by hand is the process that has to find
|
||||
# s6-svlisten. firmwareExe repeats the prepend itself, because this file is
|
||||
# allowed to be missing. Prepended, not appended: these are
|
||||
# ours and nothing on the base rootfs answers to those names, but a printer
|
||||
# that ever grows a second s6 should get the one we shipped.
|
||||
#
|
||||
|
||||
@@ -1,373 +0,0 @@
|
||||
# The shape every mod service script has. Sourced, never executed.
|
||||
#
|
||||
# MODDIR=/usr/data/anvil
|
||||
# [ -f $MODDIR/anvil.conf ] && . $MODDIR/anvil.conf
|
||||
# . $MODDIR/anvil-service.sh
|
||||
# SVC_NAME=moonraker
|
||||
#
|
||||
# WHY THIS FILE EXISTS. The five scripts in init.d/ were written one at a time,
|
||||
# months apart, each solving the same four problems as it hit them: how to say
|
||||
# something in the boot log, how to tell whether the daemon is still there, how
|
||||
# to stop it without leaving a stale pidfile, and how to wait on hardware
|
||||
# without holding up the boot. They arrived at four different answers to the
|
||||
# liveness question alone -- a pidfile and `kill -0`, two spellings of
|
||||
# `ps | grep`, `pgrep -f` with a ps fallback, and a socket test -- and at two
|
||||
# different answers to `restart`. None of those differences were decisions;
|
||||
# they were the order the scripts got written in. This file is the shared
|
||||
# answer, so that fixing one of these problems fixes it everywhere instead of
|
||||
# in whichever script the bug was noticed in.
|
||||
#
|
||||
# It is deliberately small. Nothing here does anything a service script could
|
||||
# not do in three lines; what it buys is that all of them now do it the same
|
||||
# way, and that the busybox corrections below (see svc_stop_daemon) live in one
|
||||
# place where the comment explaining them cannot drift away from the code.
|
||||
#
|
||||
# THE API, in full:
|
||||
#
|
||||
# SVC_NAME the log prefix, set once per script. Required.
|
||||
# SVC_EXTRA_VERBS optional; extra verbs for the usage line, e.g.
|
||||
# SVC_EXTRA_VERBS="|force-start"
|
||||
#
|
||||
# svc_say MSG -> "name: MSG"
|
||||
# svc_warn MSG -> "name: !! MSG"
|
||||
# svc_pid_alive PIDFILE is the pid in this file still a process?
|
||||
# svc_proc_alive PATTERN is a process matching this pattern running?
|
||||
# svc_start_daemon PIDFILE EXEC [ARGS...]
|
||||
# start-stop-daemon -S, backgrounded, with a
|
||||
# pidfile; warns and returns non-zero if
|
||||
# nothing was started
|
||||
# svc_stop_daemon PIDFILE stop it, WAIT for it to actually be gone,
|
||||
# remove the pidfile
|
||||
# svc_detach FUNC [ARGS...] run a slow thing in the background and
|
||||
# return at once; SVC_BG_PID is its pid
|
||||
# svc_dispatch "$@" the start|stop|restart|status block; calls
|
||||
# the start(), stop() and status() the script
|
||||
# defines, and exits
|
||||
#
|
||||
# -- talking to s6, for the services that have been moved to it --
|
||||
# SVC_S6_BIN where the s6 binaries are; $MODDIR/bin
|
||||
# SVC_S6_SCANDIR the scandir S40s6 scans; $MODDIR/etc/s6
|
||||
#
|
||||
# svc_s6_running is the SCANNER alive and listening?
|
||||
# svc_s6_svc NAME OPT... s6-svc OPT... $SVC_S6_SCANDIR/NAME
|
||||
# svc_s6_up NAME [SECONDS] bring NAME up and WAIT until it is up
|
||||
# svc_s6_down NAME [SECONDS] bring NAME down and WAIT until it is down
|
||||
# svc_s6_stat NAME one line of s6-svstat, on stdout
|
||||
# svc_s6_ready NAME [SECONDS] block until NAME declares itself READY
|
||||
#
|
||||
# The script defines start(), stop() and status(); this file defines
|
||||
# everything else. A script with an extra verb intercepts it before handing the
|
||||
# rest over -- see svc_dispatch.
|
||||
|
||||
# ---- saying things ---------------------------------------------------------
|
||||
#
|
||||
# Every line any of these scripts has ever printed is "name: something", because
|
||||
# firmwareExe runs all five into the same boot log and a bare "started" tells
|
||||
# nobody which of them started. Repeating the name in every echo is how one of
|
||||
# them ended up printing "web: stopped" from a script that by then also owned
|
||||
# the camera. Name it once, at the top, in SVC_NAME.
|
||||
#
|
||||
# The "!!" on svc_warn is not decoration: it is what somebody greps for when a
|
||||
# printer boots wrong, and it already means "this is why your printer is not
|
||||
# working" everywhere else in the mod (firmwareExe and start.sh both use it).
|
||||
svc_say() {
|
||||
echo "${SVC_NAME:-service}: $*"
|
||||
}
|
||||
|
||||
svc_warn() {
|
||||
echo "${SVC_NAME:-service}: !! $*"
|
||||
}
|
||||
|
||||
# ---- is it alive? ----------------------------------------------------------
|
||||
#
|
||||
# Two answers, because there are genuinely two situations.
|
||||
#
|
||||
# svc_pid_alive is for a daemon WE started through start-stop-daemon, so we
|
||||
# know its pid exactly. It is the better of the two and should be preferred:
|
||||
# it cannot be confused by a second copy, by a grep matching its own pipeline,
|
||||
# or by an unrelated process with a similar name.
|
||||
#
|
||||
# The pidfile can still lie -- a pid gets recycled -- which is the whole reason
|
||||
# svc_stop_daemon below removes it. A stale pidfile plus this check is how
|
||||
# `status` came to report a dead moonraker as running.
|
||||
svc_pid_alive() {
|
||||
[ -f "$1" ] || return 1
|
||||
kill -0 "`cat "$1" 2>/dev/null`" 2>/dev/null
|
||||
}
|
||||
|
||||
# svc_proc_alive is for a process we did NOT start with a pidfile -- klippy,
|
||||
# launched by FlashForge's start.sh; wpa_supplicant, which backgrounds itself
|
||||
# with -B; mjpg_streamer under its own respawn loop. All we have is the name.
|
||||
#
|
||||
# pgrep is tried first and is not assumed: this busybox is built with it on the
|
||||
# printers we have looked at, but that is a build option and a rootfs we do not
|
||||
# control ships it. The fallback is plain `ps` (busybox ps takes no options
|
||||
# worth relying on, so this is deliberately bare) piped through grep, with the
|
||||
# `grep -v grep` that stops the pipeline from matching itself.
|
||||
#
|
||||
# Pass the pattern in the form pgrep wants; `klippy\.py` matches both, because
|
||||
# grep reads the backslash-escaped dot the same way.
|
||||
svc_proc_alive() {
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
pgrep -f "$1" >/dev/null 2>&1
|
||||
else
|
||||
ps 2>/dev/null | grep "$1" | grep -v grep >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
# ---- starting and stopping -------------------------------------------------
|
||||
#
|
||||
# start-stop-daemon -S with -b (background it), -m (write the pidfile) and -p.
|
||||
# The pidfile is the point: without one there is no way to stop the daemon
|
||||
# except by name, and by-name is how you kill the wrong python.
|
||||
#
|
||||
# The caller announces success itself, because "started" is worth saying with
|
||||
# the path or the port that makes it useful. Failure is announced here, with
|
||||
# the exit code, because there is exactly one thing to say about it and it went
|
||||
# unsaid for a long time: busybox start-stop-daemon exits non-zero when a
|
||||
# process matching the pidfile is ALREADY running, which after a `restart`
|
||||
# means the old one outlived the wait in stop(). Either way nothing new was
|
||||
# started, and a script that prints nothing at that moment looks like it worked.
|
||||
#
|
||||
# SVC_NICE, when the caller sets it to a non-empty non-zero number, becomes
|
||||
# start-stop-daemon's -N: the daemon starts at that nice value. It exists
|
||||
# because klippy shares two cores with everything else the mod added, and
|
||||
# Klipper reports a host that misses its step deadlines as an MCU "Timer too
|
||||
# close" shutdown -- so the services around it should yield to it rather than
|
||||
# compete on equal terms. Only CPU: busybox 1.31.1 on this printer has no
|
||||
# ionice applet (checked in the replica, not assumed), so eMMC contention has
|
||||
# no equivalent knob and is not addressed here.
|
||||
#
|
||||
# Why -N and not a `nice -n` wrapper: --exec is what start-stop-daemon matches
|
||||
# and records, and wrapping would make that the nice binary rather than the
|
||||
# program itself. -N keeps the exec path real. Both were verified to produce
|
||||
# the wanted nice value on the printer's own busybox; -N is the one that keeps
|
||||
# the pidfile honest.
|
||||
svc_start_daemon() {
|
||||
_svc_pidfile=$1
|
||||
shift
|
||||
_svc_exec=$1
|
||||
shift
|
||||
# An empty or 0 SVC_NICE means "as before": no -N, normal priority.
|
||||
if [ -n "${SVC_NICE:-}" ] && [ "${SVC_NICE:-0}" != 0 ]; then
|
||||
start-stop-daemon -S -b -m -N "$SVC_NICE" \
|
||||
-p "$_svc_pidfile" --exec "$_svc_exec" -- "$@"
|
||||
else
|
||||
start-stop-daemon -S -b -m -p "$_svc_pidfile" --exec "$_svc_exec" -- "$@"
|
||||
fi
|
||||
_svc_rc=$?
|
||||
[ $_svc_rc = 0 ] || svc_warn "FAILED to start -- start-stop-daemon returned $_svc_rc"
|
||||
unset _svc_pidfile _svc_exec
|
||||
return $_svc_rc
|
||||
}
|
||||
|
||||
# The correction busybox needs. Its start-stop-daemon -K sends the signal and
|
||||
# returns immediately, and it does not remove the pidfile. Both of those are
|
||||
# wrong for us, in ways that took a while to read as bugs:
|
||||
#
|
||||
# * Not waiting makes `restart` race its own start. stop() returns, start()
|
||||
# runs start-stop-daemon -S, the old process is still holding the pidfile,
|
||||
# -S refuses, and the service is now stopped rather than restarted.
|
||||
# * Not removing the pidfile leaves svc_pid_alive testing a pid that the
|
||||
# kernel is free to hand to something else, so `status` eventually reports
|
||||
# an unrelated process as a running service.
|
||||
#
|
||||
# So: signal, then wait for it to actually be gone, then remove the file. The
|
||||
# bound is 15 seconds because that is long enough for moonraker to finish
|
||||
# writing its database and short enough that a stuck process does not hold a
|
||||
# boot open; a process still there at the end gets said out loud rather than
|
||||
# silently abandoned, because the next start is about to fail and this is the
|
||||
# line that explains why.
|
||||
svc_stop_daemon() {
|
||||
[ -f "$1" ] || return 0
|
||||
start-stop-daemon -K -p "$1" 2>/dev/null
|
||||
_svc_waited=0
|
||||
while svc_pid_alive "$1" && [ $_svc_waited -lt 15 ]; do
|
||||
sleep 1
|
||||
_svc_waited=$((_svc_waited + 1))
|
||||
done
|
||||
svc_pid_alive "$1" && svc_warn "still running after ${_svc_waited}s"
|
||||
unset _svc_waited
|
||||
rm -f "$1"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---- not making the boot wait ----------------------------------------------
|
||||
#
|
||||
# firmwareExe runs $MODDIR/init.d/S* one at a time, in the FOREGROUND, in
|
||||
# filename order. So every second a start() spends waiting is a second the
|
||||
# services after it do not exist -- and the waits in these scripts are all
|
||||
# hardware: up to 15s for wlan0 to register after the 8821cu insmod and 20s
|
||||
# more to associate, 30s for a USB camera to enumerate, 90s for klippy to give
|
||||
# up on an MCU. Inline, the longest delay lands on exactly the machine with no
|
||||
# network, no camera or no heater board, which is the machine somebody needs to
|
||||
# ssh into to find out why.
|
||||
#
|
||||
# The rule firmwareExe already states for a service that FAILS, applied to one
|
||||
# that is merely slow: the waiting happens detached and start() returns at
|
||||
# once. The cost is that the detached lines arrive in the boot log after the
|
||||
# later services' lines, out of order. That is the trade, and it is the right
|
||||
# way round.
|
||||
#
|
||||
# It returns 0 unconditionally: nothing is going to read the exit status of a
|
||||
# background job, and letting `func &` be the last statement of start() would
|
||||
# hand the caller the shell's opinion of a fork rather than a verdict on the
|
||||
# service. SVC_BG_PID is there for a supervisor that has to be killable --
|
||||
# S65camera writes it to a pidfile, because killing only the streamer it
|
||||
# supervises gets the streamer restarted three seconds later.
|
||||
svc_detach() {
|
||||
"$@" &
|
||||
SVC_BG_PID=$!
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---- talking to s6 ---------------------------------------------------------
|
||||
#
|
||||
# Everything above this line is the hand-rolled supervisor: a liveness test we
|
||||
# wrote, a stop-and-wait we wrote, and -- in S65camera -- a respawn loop we
|
||||
# wrote. Everything below is how a service asks the real one instead. Both
|
||||
# sets exist at once and will for a while: S40s6 starts the scanner, and the
|
||||
# six services move into it ONE AT A TIME, each keeping its S* script as the
|
||||
# interface people already type. A service that has not moved uses the
|
||||
# functions above and does not know these exist.
|
||||
#
|
||||
# WHERE THE PATHS COME FROM, and why they are here rather than in each script.
|
||||
# s6 bakes its own prefix in at COMPILE time -- its waiting verbs exec
|
||||
# s6-svlisten, which spawns s6-ftrigrd out of the libexecdir chosen by
|
||||
# ./configure, so the binaries only work from the prefix they were built for
|
||||
# (/usr/data/anvil; see tools/supervisor/README.md, where shipping them
|
||||
# elsewhere made every waiting verb fail while status and respawn kept
|
||||
# working, i.e. it failed late and partially). The scandir is a second thing
|
||||
# every one of these scripts has to agree about with S40s6, and the way to
|
||||
# make several scripts agree is to say it once, in the file they all source.
|
||||
# Both are overridable so a test can point at a scandir of its own without
|
||||
# reinstalling the payload.
|
||||
SVC_S6_BIN=${SVC_S6_BIN:-${MODDIR:-/usr/data/anvil}/bin}
|
||||
# Not written as one ${MODDIR:-/usr/data/anvil} expression ending in etc/s6:
|
||||
# test_paths.py's absolute-path regex reads the closing brace immediately
|
||||
# followed by that suffix as a STOCK rootfs path in its own right (its
|
||||
# lookbehind excludes a preceding word character or dot, but not a brace),
|
||||
# and this directory is not something any stock FlashForge rootfs ships -- it
|
||||
# lives under $MODDIR, on the data partition. Splitting the fallback onto its
|
||||
# own line puts a word character (the variable name) right before the suffix
|
||||
# instead, which the same lookbehind does exclude.
|
||||
_s6_default_scandir=${MODDIR:-/usr/data/anvil}
|
||||
_s6_default_scandir=$_s6_default_scandir/etc/s6
|
||||
SVC_S6_SCANDIR=${SVC_S6_SCANDIR:-$_s6_default_scandir}
|
||||
unset _s6_default_scandir
|
||||
|
||||
# Is the SCANNER there? Not "is there a process called s6-svscan" -- this is
|
||||
# asked BY BEHAVIOUR, which is the rule the rest of this file was rewritten to
|
||||
# follow. `s6-svscanctl -a` is the request to rescan the scandir: harmless,
|
||||
# idempotent, and it travels down $SCANDIR/.s6-svscan/control, a FIFO opened
|
||||
# non-blocking. If nothing is reading that FIFO the open fails with ENXIO and
|
||||
# s6-svscanctl exits 100 saying "supervisor not listening" -- measured on the
|
||||
# replica, both ways round. A dead scanner leaves the FIFO on disk, so this is
|
||||
# a distinction `ps | grep` genuinely cannot make, and it is the same class of
|
||||
# lie as the stale pidfile and the stale klippy socket that this file already
|
||||
# has two comments about.
|
||||
#
|
||||
# It answers false rather than exploding when s6 is not installed at all,
|
||||
# because that is a real state on a printer half-way through an update.
|
||||
svc_s6_running() {
|
||||
[ -x "$SVC_S6_BIN/s6-svscanctl" ] || return 1
|
||||
"$SVC_S6_BIN/s6-svscanctl" -a "$SVC_S6_SCANDIR" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# The raw verb, for anything the four wrappers below do not cover. NAME first
|
||||
# and the options after, which is the opposite order to s6-svc's own -- s6
|
||||
# wants `s6-svc OPTIONS servicedir` -- because in ash it is the first argument
|
||||
# that can be shifted off cleanly, and because every caller here is naming a
|
||||
# service, not a directory. The directory is this file's business.
|
||||
svc_s6_svc() {
|
||||
_svc_s6_name=$1
|
||||
shift
|
||||
"$SVC_S6_BIN/s6-svc" "$@" "$SVC_S6_SCANDIR/$_svc_s6_name"
|
||||
_svc_s6_rc=$?
|
||||
unset _svc_s6_name
|
||||
return $_svc_s6_rc
|
||||
}
|
||||
|
||||
# Up and down, each WAITING for the thing it asked for -- which is the entire
|
||||
# reason for preferring a supervisor to start-stop-daemon. -wu/-wD mean "do
|
||||
# not return until the service is really up / really down", and -T is a bound
|
||||
# in MILLISECONDS so that a service which never comes down cannot hold a boot
|
||||
# or an ssh session open for ever. The default is 15 seconds, the same bound
|
||||
# and the same reasoning as svc_stop_daemon: long enough for moonraker to
|
||||
# finish writing its database, short enough not to hang a boot.
|
||||
#
|
||||
# This pair is what deletes the busybox correction at the top of this file.
|
||||
# start-stop-daemon -K returns before the process is dead, which is how
|
||||
# `restart` came to race its own start; s6-svc -wD returns when it is dead,
|
||||
# and it is C rather than a `while ... sleep 1` we maintain.
|
||||
svc_s6_up() {
|
||||
svc_s6_svc "$1" -wu -T "$(( ${2:-15} * 1000 ))" -u
|
||||
}
|
||||
|
||||
svc_s6_down() {
|
||||
svc_s6_svc "$1" -wD -T "$(( ${2:-15} * 1000 ))" -d
|
||||
}
|
||||
|
||||
# One line about a service, on stdout, for a status() to print. s6-svstat's
|
||||
# own output is already the right shape -- "up (pid 1234) 71 seconds" or
|
||||
# "down 5 seconds, normally up" -- so it is passed through rather than
|
||||
# paraphrased, exactly as S50wifi passes ifconfig through: it is the answer
|
||||
# you came for, in the shape you would have typed the command to get.
|
||||
# 2>&1 because "unable to open supervise/status" is also an answer.
|
||||
svc_s6_stat() {
|
||||
"$SVC_S6_BIN/s6-svstat" "$SVC_S6_SCANDIR/$1" 2>&1
|
||||
}
|
||||
|
||||
# READINESS -- the reason this is s6 and not runit, and the one thing here
|
||||
# that no amount of shell could have provided. A service that writes to its
|
||||
# notification-fd is telling the supervisor it is USABLE, not merely forked,
|
||||
# and s6-svwait -U blocks until it does. runit's `sv start` returns when the
|
||||
# process has been forked, which says nothing at all.
|
||||
#
|
||||
# It is unused today and that is expected: it is the seam phase 4 is aimed at.
|
||||
# S65camera currently polls /dev/video0 for up to 30 seconds and S70klipper
|
||||
# retries an MCU handshake for up to 90, both inside their own scripts, and
|
||||
# both of those become "declare ready when the device is there" plus a caller
|
||||
# that waits here. Timeout in seconds, converted to the milliseconds s6 wants;
|
||||
# non-zero if it ran out, so a caller can carry on degraded rather than block.
|
||||
svc_s6_ready() {
|
||||
"$SVC_S6_BIN/s6-svwait" -U -t "$(( ${2:-30} * 1000 ))" "$SVC_S6_SCANDIR/$1"
|
||||
}
|
||||
|
||||
# ---- the verb block --------------------------------------------------------
|
||||
#
|
||||
# Every one of these scripts ends in the same case statement, and they had
|
||||
# drifted: two spellings of the usage line, and `restart` implemented twice --
|
||||
# `stop; start` in one, `stop; sleep 2; start` in the others.
|
||||
#
|
||||
# THE SLEEP IS GONE, on purpose. It was standing in for a wait that nobody was
|
||||
# doing: busybox start-stop-daemon -K returns before the process is dead, so
|
||||
# something had to pass before the start could succeed, and two seconds was a
|
||||
# guess that happened to be enough most of the time. svc_stop_daemon now waits
|
||||
# for the process to actually be gone, which is the thing the sleep was
|
||||
# approximating -- and does it correctly, for as long as it genuinely takes. A
|
||||
# stop() that waits makes the sleep redundant; a stop() that does not is a bug
|
||||
# to fix in stop(), because the sleep only ever hid it for two seconds.
|
||||
#
|
||||
# EXTRA VERBS. S70klipper has `force-start`, which overrides its hand-off to
|
||||
# ff-startup.py. A script with a verb of its own handles it first and hands the
|
||||
# rest here, and names it in SVC_EXTRA_VERBS so the usage line stays true:
|
||||
#
|
||||
# SVC_EXTRA_VERBS="|force-start"
|
||||
# case "$1" in
|
||||
# force-start) start force ;;
|
||||
# *) svc_dispatch "$@" ;;
|
||||
# esac
|
||||
#
|
||||
# This exits rather than returns, so it is the last statement of the script.
|
||||
# The exit code is the one start(), stop() or status() produced -- an unknown
|
||||
# verb is the only thing this file decides, and it is 1.
|
||||
svc_dispatch() {
|
||||
case "$1" in
|
||||
start|'') start; exit $? ;;
|
||||
stop) stop; exit $? ;;
|
||||
restart) stop; start; exit $? ;;
|
||||
status) status; exit $? ;;
|
||||
*) echo "usage: $0 {start|stop|restart|status${SVC_EXTRA_VERBS:-}}"; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
+30
-13
@@ -1,6 +1,19 @@
|
||||
# /usr/data/anvil/anvil.conf -- runtime switches. Edit on the printer, reboot.
|
||||
# PRESERVED across mod updates.
|
||||
|
||||
# HOW THESE SWITCHES REACH THE PRINTER. The boot is one `s6-rc -u change` over
|
||||
# a compiled service database, and a database built on a build host cannot read
|
||||
# a file you edit here. So the switches are applied to the ARGUMENT LIST:
|
||||
# firmwareExe expands the ok-all bundle and drops whatever is switched off.
|
||||
# That loop is the ONLY place these gates exist. A service with no switch
|
||||
# (klipper, the MCU bring-up, ff-startup) is one there is no sensible printer
|
||||
# without.
|
||||
#
|
||||
# A printer whose s6 does not run has no services at all -- ssh, and nothing
|
||||
# else. That is the deliberate trade: one implementation of each service,
|
||||
# exercised on every printer, instead of a second one exercised only on
|
||||
# printers that are already broken.
|
||||
|
||||
# Web stack: nginx (Mainsail on :80) + moonraker (:7125).
|
||||
# The only one there is -- FlashForge's HTTP API went with firmwareExe.
|
||||
# 0 = no web UI at all, ssh only.
|
||||
@@ -8,7 +21,10 @@ MOD_WEB=1
|
||||
|
||||
# Camera: mjpg-streamer on :8080, proxied by nginx at /webcam/. The binary is
|
||||
# FlashForge's own, already on the firmware partition; firmwareExe used to be
|
||||
# what started it. Optional tunables, defaults shown:
|
||||
# what started it. It is the one service the boot genuinely waits on for
|
||||
# nothing -- it declares itself ready only once it has a camera, and a printer
|
||||
# without one spends 40s of every boot finding that out. MOD_CAM=0 on a printer
|
||||
# that has no camera is worth setting. Optional tunables, defaults shown:
|
||||
#MOD_CAM_RES=1280x720
|
||||
#MOD_CAM_FPS=30
|
||||
#MOD_CAM_PORT=8080
|
||||
@@ -28,10 +44,13 @@ MOD_SPLASH=1
|
||||
# boards over again on each retry, which is what gives one that missed its
|
||||
# window another chance. MOD_KLIPPER_TRIES (below) bounds the retries.
|
||||
#
|
||||
# 0 = hand klipper's start back to init.d/S70klipper, which does it the old
|
||||
# way (background, printer.log tailing, no screen) and starts the UI at once.
|
||||
# That is also what happens automatically if this script or the interpreter
|
||||
# is missing -- klipper starting must never depend on python.
|
||||
# 0 = do not wait for the printer: klipper still starts (it is a service in
|
||||
# its own right and has never depended on this program), the MCU bring-up still
|
||||
# happens (it is klipper's dependency), and the screen comes up at once in
|
||||
# front of a machine that may still be connecting. What is skipped is the wait,
|
||||
# the retries when a board did not answer, the panel narration and the
|
||||
# first-boot import. That is also what happens automatically if the script or
|
||||
# the interpreter is missing -- klipper starting must never depend on python.
|
||||
#MOD_STARTUP_TIMEOUT=240
|
||||
MOD_STARTUP=1
|
||||
|
||||
@@ -42,8 +61,13 @@ MOD_STARTUP=1
|
||||
# wait above still happens.
|
||||
MOD_IMPORT=1
|
||||
|
||||
# Touchscreen UI: HelixScreen, started by the firmwareExe wrapper.
|
||||
# Touchscreen UI: HelixScreen, a supervised service that starts once the
|
||||
# printer is actually ready -- it depends on the startup program above, so the
|
||||
# screen no longer appears in front of a machine that is still connecting, and
|
||||
# s6 restarts it if it dies.
|
||||
# 0 = boot headless (blank screen); ssh and Mainsail are unaffected.
|
||||
# To drive any of these by hand: s6-rc -u change <name>, s6-rc -d change <name>,
|
||||
# s6-svstat /usr/data/anvil/etc/s6/<name>.
|
||||
MOD_UI=1
|
||||
|
||||
# SSH: dropbear on :22. NOT read on the printer -- ssh comes from the stock
|
||||
@@ -56,13 +80,6 @@ MOD_SSH=1
|
||||
# do it and no longer runs.
|
||||
MOD_WIFI=1
|
||||
|
||||
# Process supervision: s6-svscan, started first by init.d/S40s6 and watching
|
||||
# $MODDIR/etc/s6 for service directories: nginx, the camera and moonraker are
|
||||
# on it now, so turning it off leaves them running unsupervised -- see the
|
||||
# init.d fallback for each. It is here so that a printer where the supervisor
|
||||
# itself is suspect has a switch. 0 = no scanner.
|
||||
MOD_S6=1
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Priorities. Nice values (0..19) for the services that share this SoC's two
|
||||
# cores with klippy. Higher = yields more.
|
||||
|
||||
+90
-76
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
# Everything that has to happen before HelixScreen, and the panel that says so.
|
||||
#
|
||||
# The firmwareExe wrapper runs this after the init.d services and before the
|
||||
# UI. It does two jobs, and only the second is once-per-install:
|
||||
# Run as two s6-rc oneshots, between klipper and the UI. It does two jobs, and
|
||||
# only the second is once-per-install:
|
||||
#
|
||||
# EVERY BOOT wait until the printer is genuinely up -- the toolhead boards
|
||||
# handed over from their bootloaders, then klipper and
|
||||
@@ -34,28 +34,43 @@
|
||||
# 1. Hand the toolhead boards over from their bootloaders, by calling
|
||||
# ff_mcu_bringup.py directly -- it is a module here, not a subprocess --
|
||||
# naming on the panel whichever board is still being waited for.
|
||||
# 2. Launch klipper (start.sh, told not to redo the bring-up) and wait for
|
||||
# klippy and moonraker to be ready. If a board never answered, klippy
|
||||
# says so and names it, and so do we; then stop klippy, redo the
|
||||
# bring-up and try again, which is what reopening the port achieves.
|
||||
# 2. Wait for klippy and moonraker to be ready. If a board never answered,
|
||||
# klippy says so and names it, and so do we; then hand the boards over
|
||||
# again and restart klippy, which is what reopening the port achieves.
|
||||
# 3. If the install is already stamped, stop here -- the rest is the
|
||||
# migration, and it has been done.
|
||||
# 4. Otherwise run FF_IMPORT_FIRMWARE_CONFIG, then SAVE_CONFIG (which
|
||||
# restarts klippy), wait for it to come back, and only then stamp.
|
||||
#
|
||||
# WHY THIS OWNS KLIPPER'S START. The bring-up has to happen before klippy
|
||||
# opens the serial ports, and the retry that actually fixes a board which
|
||||
# missed its window is "close the port, hand it over again, reopen" -- so
|
||||
# whatever does the bring-up has to be the same thing that starts and
|
||||
# restarts klippy. Splitting them across two processes meant this program
|
||||
# could only watch, through a file, work it was not allowed to do. init.d/
|
||||
# S70klipper stands aside when this program is going to run and takes the job
|
||||
# back when it cannot: klipper starting is the single most important thing
|
||||
# the mod does, and it must not depend on a python interpreter.
|
||||
# HOW IT IS RUN NOW: as TWO s6-rc oneshots, both of them this same program.
|
||||
# `mcu-bringup` is step 1 alone (--only-bringup) and klipper depends on it, so
|
||||
# the boards are handed over before klippy opens the ports and nothing has to
|
||||
# remember to do it -- including s6 restarting klippy after a crash, which is a
|
||||
# path no script of ours is on. `ff-startup` is steps 2-4 (--no-bringup
|
||||
# --no-klipper) and depends on klipper and moonraker; the UI depends on it. See
|
||||
# the `up` scripts in payload/etc/s6-rc/source/, which assemble both command
|
||||
# lines out of anvil.conf.
|
||||
#
|
||||
# WHAT THAT DEPENDENCY DOES NOT GIVE US, and why the waiting below stays. s6-rc
|
||||
# counts a longrun up as soon as it is forked unless its servicedir carries a
|
||||
# notification-fd, and klipper has none: klippy's "ready" is a state on
|
||||
# moonraker's API, which is exactly what this program polls for. So depending
|
||||
# on klipper means "after klippy was LAUNCHED", not "after klippy is usable".
|
||||
# moonraker and the camera do have a notification-fd, so their readiness is
|
||||
# real -- which is why depending on moonraker genuinely does mean the API is
|
||||
# answering.
|
||||
#
|
||||
# WHY IT STILL RESTARTS KLIPPER. The retry that actually fixes a board which
|
||||
# missed its window is "close the port, hand it over again, reopen", so
|
||||
# whatever does the bring-up has to be the same thing that restarts klippy.
|
||||
# That has not changed; only the way it asks has -- `s6-svc -wr -t`, which
|
||||
# terminates klippy and waits for the supervisor to bring it back. NOT `s6-rc`:
|
||||
# this program runs INSIDE an s6-rc transition, which holds an exclusive lock on
|
||||
# the live directory, so an s6-rc underneath it would deadlock.
|
||||
#
|
||||
# Anything short of a verified success leaves NO stamp: the next boot tries
|
||||
# again. That is deliberate. The heater board on this machine routinely needs
|
||||
# several klippy restarts before it answers (see init.d/S70klipper), so "the
|
||||
# several klippy restarts before it answers, so "the
|
||||
# stack was not ready in time" is a normal event to retry, not a failure to
|
||||
# record forever.
|
||||
#
|
||||
@@ -75,7 +90,8 @@
|
||||
#
|
||||
# usage: ff-startup.py [--stamp F] [--dir D] [--moonraker URL] [--timeout S]
|
||||
# [--no-import] [--no-klipper] [--klipper-tries N]
|
||||
# [--start-sh P] [--daemon P] [--mcu-timeout S]
|
||||
# [--only-bringup] [--no-bringup] [--klipper-svcdir D]
|
||||
# [--mcu-timeout S]
|
||||
# [--fb DEV] [--fb-geometry G] [--no-screen] [--dry-run]
|
||||
#
|
||||
# Exit 0 = the printer came up, and the migration either was not needed or
|
||||
@@ -107,8 +123,13 @@ except ImportError:
|
||||
STAMP = '/usr/data/anvil/.firmware-config-imported'
|
||||
JSON_DIR = '/usr/data/firmwareRes/config'
|
||||
MOONRAKER = 'http://127.0.0.1:7125'
|
||||
START_SH = '/usr/prog/klipper/start.sh'
|
||||
DAEMON = '/usr/prog/klipper/klipperDaemon'
|
||||
# How klipper is restarted: `s6-svc -wr -t` on its live servicedir. NOT s6-rc --
|
||||
# this program runs as an s6-rc oneshot, and s6-rc holds an exclusive lock on
|
||||
# the live directory for the length of a transition, so an s6-rc underneath it
|
||||
# would wait for a lock its own parent holds. `-t` also leaves the WANTED state
|
||||
# alone, so s6-rc goes on believing klipper is up, which stays true.
|
||||
S6_SVC = '/usr/data/anvil/bin/s6-svc'
|
||||
KLIPPER_SVCDIR = '/usr/data/anvil/etc/s6/klipper'
|
||||
UDS = '/tmp/uds'
|
||||
TIMEOUT = 240.0
|
||||
MCU_TIMEOUT = 30.0
|
||||
@@ -339,56 +360,31 @@ def klippy_running():
|
||||
return False
|
||||
|
||||
|
||||
def start_klipper(args):
|
||||
"""Run start.sh, telling it the bring-up is already done.
|
||||
def restart_klipper(args):
|
||||
"""Terminate klippy and wait for its supervisor to bring it back.
|
||||
|
||||
It is not left to run in the background: everything it does after the
|
||||
bring-up we skipped is quick, and knowing it finished is what makes the
|
||||
wait that follows meaningful."""
|
||||
if not os.path.exists(args.start_sh):
|
||||
log('no %s -- cannot start klipper' % args.start_sh)
|
||||
Reopening the port is what toggles DTR/RTS and gives a board that missed
|
||||
its window another chance, so the restart is the fix rather than another
|
||||
go at the same thing -- and -wr does not return until klippy is actually
|
||||
running again, which is what makes the next attempt worth making."""
|
||||
if not os.path.exists(args.s6_svc):
|
||||
log('no %s -- cannot restart klipper' % args.s6_svc)
|
||||
return False
|
||||
env = dict(os.environ)
|
||||
env['FF_SKIP_MCU_BRINGUP'] = '1'
|
||||
log('starting klipper (%s)' % args.start_sh)
|
||||
log('restarting klipper (s6-svc -t %s)' % args.klipper_svcdir)
|
||||
try:
|
||||
completed = subprocess.run(['/bin/sh', args.start_sh], env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, timeout=120)
|
||||
completed = subprocess.run(
|
||||
[args.s6_svc, '-wr', '-T', '60000', '-t', args.klipper_svcdir],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120)
|
||||
except subprocess.TimeoutExpired:
|
||||
log('start.sh did not return within 120s')
|
||||
log('s6-svc did not return within 120s')
|
||||
return False
|
||||
except Exception as exc:
|
||||
log('start.sh could not be run (%s)' % exc)
|
||||
log('s6-svc could not be run (%s)' % exc)
|
||||
return False
|
||||
for line in (completed.stdout
|
||||
or b'').decode('utf-8', 'replace').splitlines():
|
||||
log(' start.sh: %s' % line)
|
||||
return True
|
||||
|
||||
|
||||
def stop_klipper(args):
|
||||
"""Stop klippy and wait for it to actually go.
|
||||
|
||||
Reopening the port is what toggles DTR/RTS and gives a board that missed
|
||||
its window another chance, so the stop has to be real before the next
|
||||
attempt is worth making."""
|
||||
try:
|
||||
subprocess.run([args.daemon, 'stop'], stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, timeout=30)
|
||||
except Exception as exc:
|
||||
log('klipperDaemon stop failed (%s)' % exc)
|
||||
for _ in range(15):
|
||||
if not klippy_running():
|
||||
break
|
||||
time.sleep(1.0)
|
||||
# start.sh refuses to run while the socket is there, so clear a stale one
|
||||
# -- but only once klippy is definitely gone.
|
||||
if not klippy_running():
|
||||
try:
|
||||
os.remove(UDS)
|
||||
except OSError:
|
||||
pass
|
||||
log(' klipper: %s' % line)
|
||||
return completed.returncode == 0
|
||||
|
||||
|
||||
def klippy_fault(moonraker):
|
||||
@@ -494,8 +490,19 @@ def stamp(path, note):
|
||||
def run(args):
|
||||
started = time.time()
|
||||
deadline = started + args.timeout
|
||||
moonraker = Moonraker(args.moonraker)
|
||||
panel = Panel(args.fb, args.fb_geometry, enabled=not args.no_screen)
|
||||
if args.only_bringup:
|
||||
# The `mcu-bringup` oneshot. Step 1 and nothing else: klipper depends
|
||||
# on this service, so the boards are out of their bootloaders before
|
||||
# klippy opens the ports, every time klippy starts.
|
||||
#
|
||||
# The panel is NOT handed back clean here. This runs seconds before
|
||||
# klipper and then ff-startup, and blanking the screen between the two
|
||||
# would be a flicker for no reason -- ff-startup's own panel.done()
|
||||
# ends the sequence.
|
||||
hand_over_boards(panel, args.mcu_timeout)
|
||||
return 0
|
||||
moonraker = Moonraker(args.moonraker)
|
||||
panel.say('STARTING SERVICES', 0.05)
|
||||
try:
|
||||
return startup(args, moonraker, panel, started, deadline)
|
||||
@@ -507,32 +514,34 @@ def run(args):
|
||||
def bring_up_printer(args, moonraker, panel, started, deadline):
|
||||
"""Boards out of their bootloaders, klippy up, and retry until it is.
|
||||
|
||||
This is what init.d/S70klipper's supervise() used to do by tailing
|
||||
printer.log for markers, which was the only signal a shell script had.
|
||||
Here the signal is moonraker's, and the retry can do the thing that
|
||||
actually helps: hand the boards over again before reopening the ports.
|
||||
The signal is moonraker's rather than a marker tailed out of
|
||||
printer.log, and the retry does the thing that actually helps: hand the
|
||||
boards over again before reopening the ports.
|
||||
"""
|
||||
for attempt in range(1, args.klipper_tries + 1):
|
||||
if not klippy_running():
|
||||
hand_over_boards(panel, min(args.mcu_timeout,
|
||||
max(1.0, deadline - time.time())))
|
||||
if not args.no_klipper and not start_klipper(args):
|
||||
# ATTEMPT 1 NORMALLY DOES NOTHING HERE. klipper is this service's
|
||||
# dependency, so s6-rc has already started it and the boards have
|
||||
# already been handed over by the mcu-bringup oneshot; there is
|
||||
# nothing to do but wait. The branch is for the two cases where that
|
||||
# is not true: a retry, and a klippy that is not there at all because
|
||||
# its run script is failing.
|
||||
if attempt > 1 or not klippy_running():
|
||||
if attempt > 1 or not args.no_bringup:
|
||||
hand_over_boards(panel, min(args.mcu_timeout,
|
||||
max(1.0, deadline - time.time())))
|
||||
if not args.no_klipper and not restart_klipper(args):
|
||||
panel.failed('KLIPPER COULD NOT BE STARTED')
|
||||
return False
|
||||
if wait_for_stack(moonraker, panel, deadline, started, quiet=True):
|
||||
return True
|
||||
if time.time() >= deadline or attempt == args.klipper_tries:
|
||||
break
|
||||
# Reopening the port toggles DTR/RTS, which is what gives a board
|
||||
# that missed its window another chance -- so the restart is the
|
||||
# fix, not merely another go at the same thing.
|
||||
board = klippy_fault(moonraker)
|
||||
log('attempt %d/%d failed (%s) -- restarting klipper'
|
||||
% (attempt, args.klipper_tries, board or 'no board named'))
|
||||
panel.say('RETRYING %s' % (board or 'THE PRINTER'), 0.15,
|
||||
detail='ATTEMPT %d OF %d' % (attempt + 1,
|
||||
args.klipper_tries))
|
||||
stop_klipper(args)
|
||||
report_stack_failure(moonraker, panel)
|
||||
return False
|
||||
|
||||
@@ -667,10 +676,15 @@ def main(argv):
|
||||
help="how long one bring-up pass may take")
|
||||
parser.add_argument('--klipper-tries', type=int, default=KLIPPER_TRIES,
|
||||
help="how many times to hand the boards over and retry")
|
||||
parser.add_argument('--start-sh', default=START_SH)
|
||||
parser.add_argument('--daemon', default=DAEMON)
|
||||
parser.add_argument('--s6-svc', default=S6_SVC)
|
||||
parser.add_argument('--klipper-svcdir', default=KLIPPER_SVCDIR,
|
||||
help="klipper's live servicedir, for the restart")
|
||||
parser.add_argument('--only-bringup', action='store_true',
|
||||
help="hand the toolhead boards over, then exit")
|
||||
parser.add_argument('--no-bringup', action='store_true',
|
||||
help="skip the FIRST bring-up; a retry still redoes it")
|
||||
parser.add_argument('--no-klipper', action='store_true',
|
||||
help="do not start klipper; only wait for it")
|
||||
help="never touch klipper; only wait for it")
|
||||
parser.add_argument('--no-import', action='store_true',
|
||||
help="only bring the printer up; never migrate")
|
||||
parser.add_argument('--fb', default=None,
|
||||
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
# /usr/prog/klipper/klipperDaemon -- REPLACED by the mod.
|
||||
#
|
||||
# Stock, this forks klippy with start-stop-daemon -b. klippy is an s6-rc longrun
|
||||
# now, so anything that still calls this -- FlashForge's own code, the UI's
|
||||
# restart-firmware path, a habit over ssh -- would put a second, unsupervised
|
||||
# klippy next to the supervised one, fighting for /dev/ttyS4 and /tmp/uds. So
|
||||
# `start` does nothing, and exits 0 so a caller that treats failure as fatal is
|
||||
# not broken by it.
|
||||
#
|
||||
# KLIPPER_NICENESS is FlashForge's tuning and this file is still where it lives:
|
||||
# etc/s6-rc/source/klipper/run reads the line below, and payload/run-append.sh
|
||||
# carries the value forward off the file it replaces, so a printer that has
|
||||
# never seen the stock number does not get a guessed one.
|
||||
KLIPPER_NICENESS=0
|
||||
|
||||
MODDIR=/usr/data/anvil
|
||||
S6RC=$MODDIR/bin/s6-rc
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
echo "klipperDaemon: klipper is supervised -- use '$S6RC -u change klipper'"
|
||||
exit 0
|
||||
;;
|
||||
stop)
|
||||
# Redirected rather than reimplemented. Stock semantics would signal a
|
||||
# pidfile nothing writes any more, so `stop` would silently do nothing and
|
||||
# s6 would keep klippy up regardless. This also stops what depends on
|
||||
# klipper: ff-startup, and so the UI.
|
||||
[ -x "$S6RC" ] || { echo "klipperDaemon: no $S6RC"; exit 0; }
|
||||
exec "$S6RC" -d change klipper
|
||||
;;
|
||||
restart)
|
||||
# `s6-svc -t` and not two s6-rc transitions: it takes no lock on the live
|
||||
# directory, so this is safe to call from inside one, and it leaves the
|
||||
# WANTED state alone -- s6-rc goes on believing klipper is up, which stays
|
||||
# true. -wr returns when the supervisor has started it again.
|
||||
[ -x "$MODDIR/bin/s6-svc" ] || { echo "klipperDaemon: no $MODDIR/bin/s6-svc"; exit 0; }
|
||||
exec "$MODDIR/bin/s6-svc" -wr -T 60000 -t "$MODDIR/etc/s6/klipper"
|
||||
;;
|
||||
status)
|
||||
[ -x "$MODDIR/bin/s6-svstat" ] || { echo "klipperDaemon: no $MODDIR/bin/s6-svstat"; exit 0; }
|
||||
exec "$MODDIR/bin/s6-svstat" "$MODDIR/etc/s6/klipper"
|
||||
;;
|
||||
*)
|
||||
echo "usage: $0 {start|stop|restart|status}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
File renamed without changes.
Executable
+144
@@ -0,0 +1,144 @@
|
||||
#!/bin/sh
|
||||
# The camera, as s6 supervises it: mjpg_streamer on :8080, in the FOREGROUND.
|
||||
#
|
||||
# THE ONE RULE: never fork the streamer, never background it, never return
|
||||
# while the camera is meant to be running. `exec` at the bottom makes the
|
||||
# process s6 supervises BE mjpg_streamer, so s6-svstat reports its pid and a
|
||||
# stop signals the streamer rather than a shell that has long since gone.
|
||||
#
|
||||
# LD_LIBRARY_PATH matters: libjpeg.so.9 lives in the mjpg-streamer directory
|
||||
# itself. That one directory is prepended here rather than in anvil-env.sh --
|
||||
# it is a plugin directory with its own libjpeg, and putting it in front of
|
||||
# every python process on the printer would risk a version conflict to benefit
|
||||
# one service.
|
||||
#
|
||||
# THE BINARY, THE PLUGINS AND -w ARE ABSOLUTE PATHS below even though
|
||||
# `cd "$CAMDIR"` still happens: relative plugin names worked in the replica
|
||||
# (glibc's dlopen falls back to LD_LIBRARY_PATH for a bare name) but not on
|
||||
# real hardware.
|
||||
MODDIR=/usr/data/anvil
|
||||
# anvil.conf is read HERE, not inherited. s6-supervise starts this script from
|
||||
# the SCANNER's environment, which is whatever firmwareExe had at boot -- and
|
||||
# on a `s6-svc -u` typed over ssh it is whatever that ssh session had, which is
|
||||
# not the same thing at all. Reading the file in the run script is what makes
|
||||
# MOD_CAM_RES/FPS/PORT mean the same on both paths, and it is why editing
|
||||
# anvil.conf and restarting the service is enough to change the resolution.
|
||||
[ -f $MODDIR/anvil.conf ] && . $MODDIR/anvil.conf
|
||||
[ -f $MODDIR/anvil-env.sh ] && . $MODDIR/anvil-env.sh
|
||||
|
||||
CAMDIR=/usr/prog/mjpg-streamer
|
||||
VIDEO=/dev/video0
|
||||
LOG=/usr/data/logs/mjpg.log
|
||||
RES="${MOD_CAM_RES:-1280x720}"
|
||||
FPS="${MOD_CAM_FPS:-30}"
|
||||
PORT="${MOD_CAM_PORT:-8080}"
|
||||
|
||||
# The same log file the hand-rolled loop wrote to, for the same reason: it is
|
||||
# the file the troubleshooting notes name, and mjpg_streamer is chatty enough
|
||||
# (a banner per start, a line per plugin option) that leaving it on the
|
||||
# inherited descriptor would put it in the boot log of a boot that finished
|
||||
# hours ago -- s6-svscan's descriptors outlive firmwareExe's redirection
|
||||
# exactly as the scanner itself does. Appending, not truncating, so a respawn
|
||||
# cannot erase the reason for the respawn.
|
||||
mkdir -p /usr/data/logs
|
||||
exec >>"$LOG" 2>&1
|
||||
|
||||
# The prerequisite. A run script cannot decline to run -- s6 respawns it -- so a
|
||||
# stock firmware that moved or dropped mjpg-streamer is reported here, with a
|
||||
# sleep, instead of showing up as a failed exec once a second.
|
||||
if [ ! -x "$CAMDIR/mjpg_streamer" ]; then
|
||||
echo "camera: !! no $CAMDIR/mjpg_streamer"
|
||||
sleep 5
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# THE UPGRADE PATH. A printer on the pre-s6 payload has our old
|
||||
# `while true; ... sleep 3` respawn loop alive with its pid in this file; left
|
||||
# alone it keeps putting mjpg_streamer back on :8080 and fights this service for
|
||||
# the port, with nothing in any log saying why. The loop dies first -- killing
|
||||
# only the streamer gets it restarted three seconds later. Delete once no
|
||||
# pre-s6 install can be upgraded in the field.
|
||||
if [ -f "$MODDIR/mjpg.pid" ]; then
|
||||
echo "camera: !! a pre-s6 respawn loop is still running -- stopping it"
|
||||
kill "`cat "$MODDIR/mjpg.pid" 2>/dev/null`" 2>/dev/null
|
||||
rm -f "$MODDIR/mjpg.pid"
|
||||
killall mjpg_streamer 2>/dev/null
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# ---- the 30-second wait for /dev/video0 ------------------------------------
|
||||
#
|
||||
# The wait is HERE rather than in a caller because only here does giving up
|
||||
# turn into retrying: the script exits non-zero, s6 runs it again, and a camera
|
||||
# plugged in ten minutes after boot comes up on its own. Thirty seconds is how
|
||||
# long a USB camera can take to enumerate on this box.
|
||||
#
|
||||
# THE BOOT PAYS FOR IT. s6-rc waits for this service's readiness notification,
|
||||
# bounded by `timeout-up` (40s) next to this file, so a printer with no camera
|
||||
# spends that on every boot. Nothing depends on the camera, so nothing else is
|
||||
# held back; MOD_CAM=0 is the answer for a printer that has none. The
|
||||
# alternative was a service that lies about being ready.
|
||||
waited=0
|
||||
while [ ! -e "$VIDEO" ] && [ $waited -lt 30 ]; do
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
if [ ! -e "$VIDEO" ]; then
|
||||
# No readiness notification on this path, deliberately: a service that
|
||||
# declared itself ready here would be telling nginx's /webcam/ proxy there
|
||||
# is a stream to proxy.
|
||||
echo "camera: !! $VIDEO never appeared after ${waited}s -- no camera plugged in?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export LD_LIBRARY_PATH=$CAMDIR:$LD_LIBRARY_PATH
|
||||
cd "$CAMDIR" || exit 1
|
||||
|
||||
# ---- readiness ------------------------------------------------------------
|
||||
#
|
||||
# `notification-fd` holds "3": the service is READY once a line is written to
|
||||
# fd 3, and s6-rc waits for that before counting it up.
|
||||
#
|
||||
# READY IS "THE HTTP PORT ANSWERS", not "the process was forked" (the gap
|
||||
# between fork and usable is where a camera that enumerated but cannot do
|
||||
# 1280x720 dies) and not "/dev/video0 exists" (a precondition -- the node
|
||||
# appears the instant the kernel enumerates the device). The only consumer is
|
||||
# nginx's /webcam/ proxy, and what a proxy needs is a socket that accepts.
|
||||
# output_http.so is started last, so a bound port also means the device opened
|
||||
# and the format was accepted.
|
||||
#
|
||||
# /proc/net/tcp because this busybox has no nc applet (measured): field 2 is
|
||||
# LOCAL_ADDRESS:PORT with the port in uppercase hex, field 4 is the state, 0A
|
||||
# is TCP_LISTEN. The address half is not matched, so a future `-l 127.0.0.1`
|
||||
# still counts as listening.
|
||||
#
|
||||
# A subshell, because the last thing this script does is exec and an exec'd
|
||||
# streamer cannot report on itself. It inherits fd 3; the exec below closes
|
||||
# fd 3 in the STREAMER (3>&-) so the only process holding the notification pipe
|
||||
# is the one with something to say. It gives up after 60s rather than looping
|
||||
# for ever -- a prober that never exits is one stray process per respawn.
|
||||
PORTHEX=`printf '%04X' "$PORT"`
|
||||
(
|
||||
n=0
|
||||
while [ $n -lt 60 ]; do
|
||||
if awk -v p=":$PORTHEX" '$2 ~ p"$" && $4 == "0A" { found = 1 }
|
||||
END { exit !found }' /proc/net/tcp 2>/dev/null
|
||||
then
|
||||
echo ready >&3
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
n=$((n + 1))
|
||||
done
|
||||
) &
|
||||
|
||||
# The exact invocation the hand-rolled loop used, argument for argument. Do not
|
||||
# "tidy" the quoting: mjpg_streamer takes a plugin and all of its options as
|
||||
# ONE argv entry and splits it itself, so those quotes are load-bearing.
|
||||
#
|
||||
# `nice` execs the target in its own place, so s6 still holds the streamer's
|
||||
# own pid. NICE_CAM (anvil.conf): the streamer is pure background work and the
|
||||
# most deserving of the services that yield to klippy. 0 or unset = normal.
|
||||
exec nice -n "${NICE_CAM:-0}" "$CAMDIR/mjpg_streamer" \
|
||||
-i "$CAMDIR/input_uvc.so -d $VIDEO -r $RES -f $FPS" \
|
||||
-o "$CAMDIR/output_http.so -p $PORT -w $CAMDIR/www" 3>&-
|
||||
@@ -0,0 +1 @@
|
||||
40000
|
||||
@@ -0,0 +1 @@
|
||||
longrun
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1 @@
|
||||
300000
|
||||
@@ -0,0 +1 @@
|
||||
oneshot
|
||||
@@ -0,0 +1,40 @@
|
||||
/bin/sh -c "
|
||||
# Wait until the printer is genuinely up -- klippy answering and moonraker
|
||||
# serving -- retrying klipper when a board did not come back, and on a first
|
||||
# boot import this unit's factory calibration. The UI depends on this, so
|
||||
# HelixScreen starts in front of a working printer rather than a connecting one.
|
||||
#
|
||||
# anvil.conf is sourced HERE because it is a runtime read: the compiled database
|
||||
# is built once, on a build host, and the user edits this file over ssh.
|
||||
#
|
||||
# IT NEVER EXITS NON-ZERO -- a timeout would leave the printer with no screen,
|
||||
# which is worse than the thing it would be reporting.
|
||||
MODDIR=/usr/data/anvil
|
||||
[ -f \$MODDIR/anvil.conf ] && . \$MODDIR/anvil.conf
|
||||
[ -f \$MODDIR/anvil-env.sh ] && . \$MODDIR/anvil-env.sh
|
||||
|
||||
# MOD_STARTUP=0 cannot mean 'do not run this service': the UI depends on it, so
|
||||
# s6-rc pulls it in regardless. It means what it always described -- do not wait
|
||||
# for the printer, start the screen at once. klipper still starts; the retries,
|
||||
# the panel narration and the first-boot import are what is skipped.
|
||||
if [ \"\${MOD_STARTUP:-1}\" != 1 ]; then
|
||||
echo 'ff-startup: MOD_STARTUP=0 -- not waiting for the printer; the UI starts at once'
|
||||
exit 0
|
||||
fi
|
||||
if [ ! -f \$MODDIR/bin/ff-startup.py ] || [ ! -x \"\$FF_PYTHON\" ]; then
|
||||
echo 'ff-startup: !! no ff-startup.py or no interpreter -- not waiting for the printer'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --no-bringup because mcu-bringup already did it; retries inside the program
|
||||
# still redo the handover, which is the half that matters. --no-klipper because
|
||||
# klipper is a dependency now, not this program's to launch -- it still restarts
|
||||
# it through s6-svc.
|
||||
ARGS=\"--no-bringup --no-klipper\"
|
||||
ARGS=\"\$ARGS --timeout \${MOD_STARTUP_TIMEOUT:-240}\"
|
||||
ARGS=\"\$ARGS --klipper-tries \${MOD_KLIPPER_TRIES:-6}\"
|
||||
[ \"\${MOD_IMPORT:-1}\" = 1 ] || ARGS=\"\$ARGS --no-import\"
|
||||
\"\$FF_PYTHON\" \$MODDIR/bin/ff-startup.py \$ARGS \
|
||||
|| echo \"ff-startup: !! returned \$? -- the next boot will try again\"
|
||||
exit 0
|
||||
"
|
||||
Whitespace-only changes.
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/sh
|
||||
# Klippy, as s6 supervises it. Exec'd by s6-supervise, never by hand.
|
||||
#
|
||||
# This is FlashForge's klipperDaemon command line without the -b: without it
|
||||
# busybox start-stop-daemon execs the program in place, so the process
|
||||
# s6-supervise holds IS klippy rather than a shell that started one. The
|
||||
# pidfile goes with it -- s6-supervise knows the pid.
|
||||
#
|
||||
# THE MCU BRING-UP DOES NOT HAPPEN HERE. It is the `mcu-bringup` oneshot, which
|
||||
# klipper depends on, so s6-rc runs it before this service starts. Doing it
|
||||
# here as well would hand the same serial ports to two processes at once.
|
||||
MODDIR=/usr/data/anvil
|
||||
|
||||
# klippy is FlashForge's Python 3.8.2 on the firmware partition, linked against
|
||||
# libraries in four directories nothing on the base rootfs knows about.
|
||||
# s6-supervise hands this script the scanner's environment, which is already
|
||||
# correct on the boot path -- but a service must not work only because the
|
||||
# shell that started the tree happened to be set up right.
|
||||
if [ -f $MODDIR/anvil-env.sh ]; then
|
||||
. $MODDIR/anvil-env.sh
|
||||
else
|
||||
echo "klipper/run: !! no $MODDIR/anvil-env.sh -- klippy will not find its libraries"
|
||||
fi
|
||||
|
||||
KLIPPY=/usr/prog/klipper/klippy/klippy.py
|
||||
PRINTER_CFG=/usr/data/config/printer.cfg
|
||||
PRINTER_LOG=/usr/data/logs/printer.log
|
||||
UDS=/tmp/uds
|
||||
DAEMON=/usr/prog/klipper/klipperDaemon
|
||||
|
||||
# klippy does not unlink its socket on exit, and everything on this printer
|
||||
# reads that socket as "klipper is running". Swept here rather than in a stop()
|
||||
# because s6 restarts this service without going near one.
|
||||
rm -f "$UDS"
|
||||
|
||||
# A klippy killed with SIGKILL -- an OOM -- leaves a process still holding
|
||||
# /dev/ttyS4 while s6 starts a second one that cannot open the port and reports
|
||||
# an MCU error for ever. s6-supervise runs one copy of this service at a time,
|
||||
# so anything running klippy.py now is a leftover.
|
||||
#
|
||||
# /proc rather than `ps | grep`: busybox ps truncates to the terminal width, so
|
||||
# the command line arrives cut off and the grep matches nothing. Our own pid is
|
||||
# skipped.
|
||||
for _kl_proc in /proc/[0-9]*; do
|
||||
[ -r "$_kl_proc/cmdline" ] || continue
|
||||
[ "${_kl_proc#/proc/}" = "$$" ] && continue
|
||||
case "`tr '\0' ' ' < "$_kl_proc/cmdline" 2>/dev/null`" in
|
||||
*klippy.py*) kill -9 "${_kl_proc#/proc/}" 2>/dev/null ;;
|
||||
esac
|
||||
done
|
||||
unset _kl_proc
|
||||
|
||||
# klipper_pri.sh puts klippy's main thread on SCHED_FIFO 50. It cannot run
|
||||
# before the exec below -- there would be no klippy to find -- and this script
|
||||
# has no "after", so it waits for the socket that says klippy is serving.
|
||||
# Bounded, and nothing reads the result: a missing chrt is a slower printer.
|
||||
if [ -x /usr/prog/klipper/klipper_pri.sh ]; then
|
||||
( _kp=0
|
||||
while [ $_kp -lt 60 ] && [ ! -S "$UDS" ]; do
|
||||
sleep 1
|
||||
_kp=$((_kp + 1))
|
||||
done
|
||||
[ -S "$UDS" ] && sh /usr/prog/klipper/klipper_pri.sh
|
||||
) >/dev/null 2>&1 &
|
||||
fi
|
||||
|
||||
# FlashForge's nice value, read out of the klipperDaemon the mod ships in its
|
||||
# place -- run-append.sh carries the number forward off the file it replaces,
|
||||
# so it is never guessed. A grep rather than a source, because sourcing stock's
|
||||
# klipperDaemon would run the daemon.
|
||||
KLIPPER_NICENESS=`sed -n 's/^ *KLIPPER_NICENESS= *\([-0-9][0-9]*\).*/\1/p' \
|
||||
"$DAEMON" 2>/dev/null | head -n 1`
|
||||
if [ -z "$KLIPPER_NICENESS" ] || [ "$KLIPPER_NICENESS" = 0 ]; then
|
||||
exec start-stop-daemon -S --exec "$FF_PYTHON" -- \
|
||||
"$KLIPPY" "$PRINTER_CFG" -l "$PRINTER_LOG" -a "$UDS"
|
||||
fi
|
||||
exec start-stop-daemon -S -N "$KLIPPER_NICENESS" --exec "$FF_PYTHON" -- \
|
||||
"$KLIPPY" "$PRINTER_CFG" -l "$PRINTER_LOG" -a "$UDS"
|
||||
@@ -0,0 +1 @@
|
||||
longrun
|
||||
@@ -0,0 +1 @@
|
||||
60000
|
||||
@@ -0,0 +1 @@
|
||||
oneshot
|
||||
@@ -0,0 +1,37 @@
|
||||
/bin/sh -c "
|
||||
# The toolhead boards, out of their bootloaders, before klippy opens the ports.
|
||||
# klipper DEPENDS ON THIS, so nothing that starts klipper can skip it --
|
||||
# including s6 restarting klippy after a crash, which no script of ours is on.
|
||||
#
|
||||
# The heater board on /dev/ttyS4 is frequently not answering when klippy first
|
||||
# opens the port; klippy then gives up after 90s with 'MCU error during connect'
|
||||
# and neither exits nor retries.
|
||||
#
|
||||
# IT NEVER EXITS NON-ZERO. s6-rc does not start what depends on a failed
|
||||
# service, so a bring-up that reported a problem would leave the printer with no
|
||||
# klipper at all -- worse than the board it is reporting on.
|
||||
MODDIR=/usr/data/anvil
|
||||
[ -f \$MODDIR/anvil-env.sh ] && . \$MODDIR/anvil-env.sh
|
||||
|
||||
# cmd_mcu is on the base rootfs, version-matched to the firmware; the mod does
|
||||
# not ship it. This is the main board (/dev/ttyS2); ff-startup.py does the other
|
||||
# three.
|
||||
if [ -x /usr/bin/cmd_mcu ]; then
|
||||
cmd_mcu write_firmware /usr/prog/libmcu-bare.bin
|
||||
cmd_mcu bootup
|
||||
# It takes a moment to leave its bootloader, and everything after this talks
|
||||
# to serial ports it owns.
|
||||
sleep 2
|
||||
else
|
||||
echo 'mcu-bringup: !! no /usr/bin/cmd_mcu -- the main board is not brought up'
|
||||
fi
|
||||
|
||||
if [ ! -f \$MODDIR/bin/ff-startup.py ] || [ ! -x \"\$FF_PYTHON\" ]; then
|
||||
echo 'mcu-bringup: !! no ff-startup.py or no interpreter -- the toolhead boards'
|
||||
echo ' are left to klippy, which names the one that did not answer'
|
||||
exit 0
|
||||
fi
|
||||
\"\$FF_PYTHON\" \$MODDIR/bin/ff-startup.py --only-bringup \
|
||||
|| echo \"mcu-bringup: !! returned \$? -- going on to klipper anyway\"
|
||||
exit 0
|
||||
"
|
||||
File renamed without changes.
Executable
+149
@@ -0,0 +1,149 @@
|
||||
#!/bin/sh
|
||||
# The moonraker service, as s6 runs it. Exec'd by s6-supervise, never by hand.
|
||||
#
|
||||
# NO start-stop-daemon AND NO `-b`: the process this script turns into IS the
|
||||
# service, so a fork-and-return would have s6 supervise the process that
|
||||
# exited, decide the service died a millisecond after starting, and restart it
|
||||
# for ever while the real moonraker served :7125 supervised by nobody.
|
||||
#
|
||||
# AND `exec`. Without it s6 supervises this shell, and every signal it sends --
|
||||
# the SIGTERM behind `s6-rc -d change moonraker` above all -- goes to a shell that
|
||||
# ignores it while moonraker carries on.
|
||||
#
|
||||
# THIS FILE DOES NOT LOOK AT MOD_WEB: it runs whenever s6 decides the service
|
||||
# should be up, and a `run` that exits because it disagrees is a respawn loop
|
||||
# with extra steps. There is no `down` file next to it either -- MEASURED:
|
||||
# s6-rc-compile accepts one in a definition directory and SILENTLY DROPS it,
|
||||
# producing a byte-identical servicedir. The gate is the argument list;
|
||||
# svc_rc_enabled reads MOD_WEB at runtime.
|
||||
#
|
||||
# Moving /usr/data/config is phase 7 of docs/notes/80-s6-migration.md; the `-d`
|
||||
# below is the one line here that changes when it does.
|
||||
MODDIR=/usr/data/anvil
|
||||
|
||||
# Read HERE, not inherited: s6-supervise starts this script from the SCANNER's
|
||||
# environment, which is firmwareExe's at boot but whatever an ssh session had
|
||||
# when the service is restarted by hand. moonraker is the process that dies on
|
||||
# "libpython3.8.so.1.0: cannot open shared object file" when that is wrong.
|
||||
[ -f $MODDIR/anvil.conf ] && . $MODDIR/anvil.conf
|
||||
[ -f $MODDIR/anvil-env.sh ] && . $MODDIR/anvil-env.sh
|
||||
|
||||
# One moonraker, on the DATA partition -- /usr/prog is what a stock flash
|
||||
# overwrites.
|
||||
MOONRAKER_MAIN=$MODDIR/moonraker/moonraker.py
|
||||
MOONRAKER_DATA=/usr/data
|
||||
MOONRAKER_TMP=/usr/data/tmp
|
||||
MOONRAKER_CONF=$MOONRAKER_DATA/config/moonraker.conf
|
||||
|
||||
# TMPDIR: /tmp is a ramdisk here, so a large upload without this fills memory
|
||||
# instead of the data partition. Set in the run script because s6 restarts this
|
||||
# service without going near any shell of ours.
|
||||
#
|
||||
# The wipe needs no "is it running" guard: s6-supervise runs one copy of this
|
||||
# service at a time and execs this only after the previous one was reaped.
|
||||
rm -rf $MOONRAKER_TMP
|
||||
mkdir -p $MOONRAKER_TMP $MOONRAKER_DATA/logs
|
||||
export TMPDIR=$MOONRAKER_TMP
|
||||
|
||||
# The prerequisites. A run script cannot decline to run -- s6 respawns it -- so
|
||||
# a missing one is reported and the sleep keeps it to one line every 5 seconds.
|
||||
if [ ! -x "$FF_PYTHON" ]; then
|
||||
echo "moonraker: !! no interpreter at ${FF_PYTHON:-/usr/prog/Python-3.8.2/bin/python3}"
|
||||
sleep 5
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$MOONRAKER_MAIN" ]; then
|
||||
echo "moonraker: !! nothing at $MOONRAKER_MAIN -- the install did not complete"
|
||||
sleep 5
|
||||
exit 1
|
||||
fi
|
||||
# THE UPGRADE PATH: a printer on the pre-s6 payload has a start-stop-daemon'd
|
||||
# moonraker whose pid is in this file. The sweep below kills the process; this
|
||||
# removes the file so nothing reads a pid the kernel has since reused. Delete
|
||||
# both once no pre-s6 install can be upgraded in the field.
|
||||
rm -f /run/moonraker.pid
|
||||
|
||||
# A moonraker already holding :7125 -- started by hand over ssh, or left by the
|
||||
# pre-s6 payload on a printer upgraded without a reboot -- makes this service
|
||||
# fail to bind, exit, get respawned, and loop for as long as the printer is on,
|
||||
# with s6-svstat saying "up" while an unsupervised leftover answers Mainsail.
|
||||
#
|
||||
# /proc rather than `ps | grep`: busybox ps truncates to the terminal width, so
|
||||
# a long python command line arrives cut in half. The match is the absolute
|
||||
# path of the entry point, which appears in a moonraker's command line and in
|
||||
# no other process here -- `s6-supervise moonraker` and this script have the
|
||||
# word but not the path. Our own pid is skipped.
|
||||
for _mr_proc in /proc/[0-9]*; do
|
||||
[ -r "$_mr_proc/cmdline" ] || continue
|
||||
[ "${_mr_proc#/proc/}" = "$$" ] && continue
|
||||
_mr_args=`tr '\0' ' ' < "$_mr_proc/cmdline" 2>/dev/null`
|
||||
case "$_mr_args" in
|
||||
*"$MOONRAKER_MAIN"*) kill -9 "${_mr_proc#/proc/}" 2>/dev/null ;;
|
||||
esac
|
||||
done
|
||||
unset _mr_proc _mr_args
|
||||
|
||||
# ---- readiness ------------------------------------------------------------
|
||||
#
|
||||
# `notification-fd` holds "3": the service is READY once a line is written to
|
||||
# fd 3. s6-rc waits for that before counting this service up, and ff-startup
|
||||
# depends on it -- so "ready" has to mean something a caller can use.
|
||||
#
|
||||
# READY IS "THE HTTP API IS LISTENING", not "the process was forked" (component
|
||||
# imports fail seconds later) and NOT "klippy_state is ready" -- moonraker
|
||||
# reporting klippy_state=error IS moonraker doing its job, so that definition
|
||||
# would never fire on precisely the broken printer whose UI somebody needs.
|
||||
#
|
||||
# /proc/net/tcp because this busybox has no nc applet (measured): field 2 is
|
||||
# LOCAL_ADDRESS:PORT with the port in uppercase hex, field 4 is the state, 0A
|
||||
# is TCP_LISTEN. The address half is not matched -- a user who sets
|
||||
# host: 127.0.0.1 should still count as listening.
|
||||
#
|
||||
# The port is read from moonraker's own config because it is user-editable and
|
||||
# a probe watching the wrong port would never fire: s6-rc would burn the whole
|
||||
# `timeout-up` on a working printer and skip everything that depends on it. The
|
||||
# awk is section-aware so a `port:` in another component cannot be picked up.
|
||||
MOONRAKER_PORT=`awk '
|
||||
/^[[:space:]]*\[/ { section = $0 }
|
||||
section ~ /\[[[:space:]]*server[[:space:]]*\]/ && /^[[:space:]]*port[[:space:]]*:/ {
|
||||
sub(/^[^:]*:[[:space:]]*/, "");
|
||||
gsub(/[^0-9]/, "");
|
||||
if (length($0)) { print; exit }
|
||||
}' "$MOONRAKER_CONF" 2>/dev/null`
|
||||
[ -n "$MOONRAKER_PORT" ] || MOONRAKER_PORT=7125
|
||||
|
||||
# A subshell, because the last thing this script does is exec and an exec'd
|
||||
# moonraker cannot report on itself. It inherits fd 3; the exec below closes
|
||||
# fd 3 in MOONRAKER (3>&-) so the only process holding the notification pipe is
|
||||
# the one with something to say -- otherwise s6 never sees the pipe close when
|
||||
# the prober gives up.
|
||||
#
|
||||
# 180s: moonraker imports its whole component tree on a 1GHz mipsel box off
|
||||
# eMMC, and slower again on the qemu replica. Giving up sends no notification,
|
||||
# and s6-rc then gives up at `timeout-up` (120s, in the file next to this).
|
||||
(
|
||||
_mr_n=0
|
||||
while [ $_mr_n -lt 180 ]; do
|
||||
if awk -v p=":`printf '%04X' "$MOONRAKER_PORT"`" \
|
||||
'$2 ~ p"$" && $4 == "0A" { found = 1 }
|
||||
END { exit !found }' /proc/net/tcp 2>/dev/null
|
||||
then
|
||||
echo ready >&3
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
_mr_n=$((_mr_n + 1))
|
||||
done
|
||||
) &
|
||||
|
||||
# `-d /usr/data` is moonraker's data path -- config/moonraker.conf,
|
||||
# logs/moonraker.log and its database. Not optional: moonraker defaults to
|
||||
# ~/printer_data, a home directory that does not exist here.
|
||||
#
|
||||
# stderr goes wherever the scanner's does ($SVC_S6_LOG), which is where a
|
||||
# moonraker that dies before its own logging is configured explains itself.
|
||||
#
|
||||
# `nice` sets its own priority and then execs in place (verified: the pid s6
|
||||
# holds is the interpreter's, not nice's), so this is still one process.
|
||||
# NICE_MOONRAKER protects klippy from this process; 0 or unset = normal.
|
||||
exec nice -n "${NICE_MOONRAKER:-0}" "$FF_PYTHON" "$MOONRAKER_MAIN" -d "$MOONRAKER_DATA" 3>&-
|
||||
@@ -0,0 +1 @@
|
||||
120000
|
||||
@@ -0,0 +1 @@
|
||||
longrun
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/bin/sh
|
||||
# The nginx service, as s6 runs it. Exec'd by s6-supervise, never by hand.
|
||||
#
|
||||
# THEREFORE `daemon off;`. nginx's default is to fork a master into the
|
||||
# background and return, which under a supervisor means s6 watches the process
|
||||
# that forked and exited, decides the service died a millisecond after starting
|
||||
# it, and restarts it for ever while the real nginx serves on, supervised by
|
||||
# nobody. `-g` puts the directive on the command line so the config file stays
|
||||
# shared with a hand-run nginx. Verified on the replica against this printer's
|
||||
# own nginx (1.5.0, FlashForge's build): the pid we launched is still the
|
||||
# master five seconds later.
|
||||
#
|
||||
# AND `exec`. Without it s6 supervises this shell, and every signal it sends --
|
||||
# the SIGTERM behind `s6-rc -d change nginx` above all -- goes to a shell that
|
||||
# ignores it while nginx carries on.
|
||||
#
|
||||
# THIS FILE DOES NOT LOOK AT MOD_WEB: it runs whenever s6 decides the service
|
||||
# should be up, and a `run` that exits because it disagrees is a respawn loop
|
||||
# with extra steps. There is no `down` file next to it either -- MEASURED:
|
||||
# s6-rc-compile accepts one in a definition directory and SILENTLY DROPS it,
|
||||
# producing a byte-identical servicedir. The gate is the argument list;
|
||||
# svc_rc_enabled reads MOD_WEB at runtime.
|
||||
MODDIR=/usr/data/anvil
|
||||
NGINX=/usr/prog/nginx/sbin/nginx
|
||||
|
||||
# -p sets the prefix: logs, the pidfile and the temp dirs land under $MODDIR on
|
||||
# the data partition, never on /usr/prog. nginx will not start if it cannot
|
||||
# create them and does not create them itself.
|
||||
mkdir -p $MODDIR/nginx/logs $MODDIR/nginx/tmp
|
||||
|
||||
# The prerequisites. A run script cannot decline to run -- s6 respawns it -- so
|
||||
# a missing one is reported and the sleep keeps it to one line every 5 seconds
|
||||
# instead of one a second in a log nobody is reading.
|
||||
if [ ! -x "$NGINX" ]; then
|
||||
echo "nginx: !! no $NGINX -- this printer's firmware did not ship one"
|
||||
sleep 5
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f $MODDIR/nginx/nginx.conf ]; then
|
||||
echo "nginx: !! no $MODDIR/nginx/nginx.conf -- the install did not complete"
|
||||
sleep 5
|
||||
exit 1
|
||||
fi
|
||||
# nginx writes this even under `daemon off;` (measured), and nothing reads it.
|
||||
# Removed so nothing on this printer ever starts to.
|
||||
rm -f $MODDIR/nginx/logs/nginx.pid
|
||||
|
||||
# THE ORPHANED WORKER. nginx is a master that binds :80 and a worker that
|
||||
# inherits the listening socket. TERM the master -- what `s6-rc -d change`
|
||||
# does -- and the worker goes with it. SIGKILL it, which is what an OOM does,
|
||||
# and the worker is orphaned, KEEPS THE SOCKET, and goes on serving: s6 then
|
||||
# starts a new nginx every second that cannot bind, while s6-svstat says "up"
|
||||
# and an unsupervised leftover answers the browser. All measured on the replica.
|
||||
#
|
||||
# s6-supervise runs one copy of this service at a time, so anything speaking
|
||||
# for nginx now is a leftover.
|
||||
#
|
||||
# /proc rather than `ps | grep`: busybox ps truncates to the terminal width, so
|
||||
# on the replica the command line arrived cut off at "/usr/prog/nginx/" and the
|
||||
# grep matched nothing; and busybox kill will not take a negative pid, so
|
||||
# signalling the process group is not available either. Both were tried.
|
||||
#
|
||||
# The match is POSITIVE and narrow, because a wrong match here kills the wrong
|
||||
# thing: "s6-supervise nginx" and this very script both have
|
||||
# the word nginx in their command lines, and s6-supervise is the process that
|
||||
# would otherwise restart us. Only the two shapes nginx itself produces are
|
||||
# swept -- "nginx: master process ..." / "nginx: worker process" (it rewrites
|
||||
# its own argv) and anything exec'd from the binary before it does.
|
||||
for _ng_proc in /proc/[0-9]*; do
|
||||
[ -r "$_ng_proc/cmdline" ] || continue
|
||||
[ "${_ng_proc#/proc/}" = "$$" ] && continue
|
||||
_ng_args=`tr '\0' ' ' < "$_ng_proc/cmdline" 2>/dev/null`
|
||||
case "$_ng_args" in
|
||||
"nginx: "*|*"$NGINX"*) kill -9 "${_ng_proc#/proc/}" 2>/dev/null ;;
|
||||
esac
|
||||
done
|
||||
unset _ng_proc _ng_args
|
||||
|
||||
# stderr is where nginx puts the reason it would not start, and it lands in the
|
||||
# scanner's log -- a file that outlives the boot.
|
||||
exec $NGINX -p $MODDIR/nginx -c $MODDIR/nginx/nginx.conf -g "daemon off;"
|
||||
@@ -0,0 +1 @@
|
||||
longrun
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1 @@
|
||||
bundle
|
||||
Whitespace-only changes.
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/bin/sh
|
||||
# HelixScreen, as s6 supervises it. Exec'd by s6-supervise, never by hand.
|
||||
#
|
||||
# `exec` at the bottom is what makes the process s6 supervises BE the UI, so
|
||||
# s6-svstat reports its pid and `s6-rc -d change ui` signals it rather than a
|
||||
# shell that has long since gone.
|
||||
#
|
||||
# BLOCKING UNKNOWN: nothing has proved helix-screen runs under a supervisor. It
|
||||
# gets the descriptors s6-supervise hands it, no controlling terminal, and a
|
||||
# session it did not create. The environment below is the easy half; a tty is
|
||||
# not something a run script can conjure. If one turns out to be needed, the
|
||||
# answer is s6's own tty handling or a small wrapper -- the foreground this
|
||||
# used to run in no longer exists. See docs/notes/80-s6-migration.md.
|
||||
MODDIR=/usr/data/anvil
|
||||
|
||||
if [ -f $MODDIR/anvil-env.sh ]; then
|
||||
. $MODDIR/anvil-env.sh
|
||||
else
|
||||
echo "ui/run: !! no $MODDIR/anvil-env.sh -- HelixScreen may not find its libraries"
|
||||
fi
|
||||
|
||||
# HELIX_DATA_DIR is the root HelixScreen resolves config/settings.json against,
|
||||
# which is why run-append.sh backs that tree up across an update.
|
||||
HELIX_DATA_DIR=$MODDIR/helixscreen
|
||||
HELIX_LOG_DEST=file
|
||||
HELIX_LOG_FILE=/usr/data/logs/helixscreen.log
|
||||
# This repo pins and patches the HelixScreen build it ships; an in-app update
|
||||
# would replace it behind the package's back. HelixScreen documents this
|
||||
# variable as the vendor opt-out.
|
||||
HELIX_DISABLE_AUTO_UPDATES=1
|
||||
export HELIX_DATA_DIR HELIX_LOG_DEST HELIX_LOG_FILE HELIX_DISABLE_AUTO_UPDATES
|
||||
|
||||
mkdir -p /usr/data/logs
|
||||
|
||||
# The prerequisite. A run script cannot decline to run -- s6 respawns it -- so
|
||||
# a missing HelixScreen is reported with a sleep instead of showing up as a
|
||||
# failed exec once a second, which is a printer that got hot and never drew
|
||||
# anything. MOD_UI=0 is a different thing and is handled in firmwareExe, which
|
||||
# simply leaves this service out of the transition.
|
||||
if [ ! -x $MODDIR/helixscreen/bin/helix-launcher.sh ] \
|
||||
&& [ ! -x $MODDIR/helixscreen/bin/helix-screen ]; then
|
||||
echo "ui: !! HelixScreen is not installed under $MODDIR/helixscreen"
|
||||
sleep 5
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# s6-supervise runs one copy of this service at a time, so anything answering
|
||||
# to these names is a leftover -- a UI started by hand over ssh -- and two
|
||||
# processes fighting over /dev/fb0 is a black screen with no explanation.
|
||||
killall helix-screen helix-watchdog helix-splash 2>/dev/null
|
||||
|
||||
# The launcher is what a HelixScreen release ships to set up its own
|
||||
# environment; skipping it silently would change behaviour on the next release
|
||||
# rather than on this one.
|
||||
if [ -x $MODDIR/helixscreen/bin/helix-launcher.sh ]; then
|
||||
exec $MODDIR/helixscreen/bin/helix-launcher.sh
|
||||
fi
|
||||
exec $MODDIR/helixscreen/bin/helix-screen
|
||||
@@ -0,0 +1 @@
|
||||
longrun
|
||||
@@ -0,0 +1,16 @@
|
||||
/bin/sh -c "
|
||||
# By name: nothing here was started with a pidfile, and inventing one for a
|
||||
# daemon that backgrounds itself would be a second thing that can be wrong.
|
||||
killall wpa_cli 2>/dev/null
|
||||
killall udhcpc 2>/dev/null
|
||||
killall wpa_supplicant 2>/dev/null
|
||||
# killall returns as soon as the signal is sent, so without this wait a
|
||||
# reconnect a moment later finds the dying supplicant in the process table and
|
||||
# does nothing.
|
||||
n=0
|
||||
while [ \$n -lt 15 ] && { pgrep -f wpa_supplicant >/dev/null 2>&1 || ps 2>/dev/null | grep wpa_supplican[t] >/dev/null; }; do
|
||||
sleep 1
|
||||
n=\$((n + 1))
|
||||
done
|
||||
echo 'wifi: stopped'
|
||||
"
|
||||
@@ -0,0 +1 @@
|
||||
oneshot
|
||||
@@ -0,0 +1,58 @@
|
||||
/bin/sh -c "
|
||||
# wlan0 + wpa_supplicant + udhcpc. Nothing in /etc/init.d brings the interface
|
||||
# up: FlashForge's firmwareExe owned the whole network stack, and app_startup.sh
|
||||
# only insmods the 8821cu driver. So the moment the mod replaces firmwareExe the
|
||||
# printer boots with no ssh, no Mainsail and no way to diagnose either.
|
||||
#
|
||||
# A ONESHOT, not a longrun: wpa_supplicant backgrounds itself with -B and so
|
||||
# does udhcpc, so there is no process for s6-supervise to hold.
|
||||
#
|
||||
# THE WAITS ARE BACKGROUNDED and this returns at once, because a oneshot's up
|
||||
# runs inside the transition firmwareExe is blocked on -- up to 15s for wlan0 to
|
||||
# register after the insmod and 20s more to associate, and that delay would land
|
||||
# on exactly the printer with no network. Returning early is lying to whatever
|
||||
# depends on this, so NOTHING DEPENDS ON WIFI, deliberately: moonraker and nginx
|
||||
# bind 0.0.0.0 and klipper talks to serial ports.
|
||||
#
|
||||
# The saved networks live in /usr/prog/wifi/wpa_supplicant.conf, which the mod
|
||||
# never ships or touches, so whatever FlashForge's UI saved is still there.
|
||||
IFACE=wlan0
|
||||
CONF=/usr/prog/wifi/wpa_supplicant.conf
|
||||
|
||||
if pgrep -f wpa_supplicant >/dev/null 2>&1 || ps 2>/dev/null | grep wpa_supplican[t] >/dev/null; then
|
||||
echo 'wifi: wpa_supplicant already running'
|
||||
exit 0
|
||||
fi
|
||||
[ -f \"\$CONF\" ] || { echo \"wifi: !! no \$CONF -- nothing to connect to\"; exit 0; }
|
||||
|
||||
(
|
||||
n=0
|
||||
while [ \$n -lt 15 ] && [ ! -d /sys/class/net/\$IFACE ]; do
|
||||
sleep 1
|
||||
n=\$((n + 1))
|
||||
done
|
||||
if [ ! -d /sys/class/net/\$IFACE ]; then
|
||||
echo \"wifi: !! \$IFACE never appeared -- is 8821cu.ko loaded?\"
|
||||
exit 1
|
||||
fi
|
||||
/sbin/ifconfig \$IFACE up 2>/dev/null
|
||||
mkdir -p /var/run/wpa_supplicant
|
||||
# nl80211 first, wext as the fallback: the 8821cu is a Realtek USB part and
|
||||
# which one it registers varies by build.
|
||||
if /usr/sbin/wpa_supplicant -B -i \$IFACE -c \"\$CONF\" -D nl80211,wext; then
|
||||
echo 'wifi: wpa_supplicant started'
|
||||
else
|
||||
echo 'wifi: !! wpa_supplicant failed to start'
|
||||
exit 1
|
||||
fi
|
||||
n=0
|
||||
while [ \$n -lt 20 ]; do
|
||||
/usr/sbin/wpa_cli -i \$IFACE status 2>/dev/null | grep -q '^wpa_state=COMPLETED' && break
|
||||
sleep 1
|
||||
n=\$((n + 1))
|
||||
done
|
||||
/sbin/udhcpc -i \$IFACE -b -t 5 >/dev/null 2>&1 &
|
||||
echo \"wifi: udhcpc requesting a lease on \$IFACE\"
|
||||
) &
|
||||
echo 'wifi: bringing up wlan0 in the background'
|
||||
"
|
||||
@@ -1,26 +0,0 @@
|
||||
# The presence of this file is the whole message: s6-supervise starts a service
|
||||
# whose directory contains a `down` file in the DOWN state. s6 never reads the
|
||||
# contents, so this text is here purely for whoever finds the file on a printer
|
||||
# and wants to know why it exists.
|
||||
#
|
||||
# WHY THE CAMERA SHIPS DOWN. MOD_CAM lives in /usr/data/anvil/anvil.conf and is
|
||||
# read at RUNTIME, on the printer, by a user who may have edited it after the
|
||||
# install; this payload is built ONCE, in CI, months earlier. So the build
|
||||
# cannot know whether the camera is wanted, and the alternative -- shipping no
|
||||
# `down` file and letting bin/patch.sh drop one in when MOD_CAM=0 -- would bake
|
||||
# a build-time answer into the image and quietly ignore the file the user
|
||||
# actually edits.
|
||||
#
|
||||
# Shipping `down` unconditionally moves the decision to the only place that can
|
||||
# make it. The moment s6-svscan notices this directory it starts an
|
||||
# s6-supervise for it, and without this file that supervise would launch the
|
||||
# streamer immediately, before any init script had read anvil.conf -- MOD_CAM=0
|
||||
# would mean "the camera runs for a second and is then killed", which is not
|
||||
# what it says. With this file the service sits down and idle, costing one
|
||||
# ~30KB supervisor and nothing else, until payload/init.d/S65camera -- which
|
||||
# HAS read anvil.conf -- brings it up with `s6-svc -u`.
|
||||
#
|
||||
# The state is not sticky across boots, and that is correct: `down` is consulted
|
||||
# by s6-supervise when the scandir is scanned, which happens fresh on every
|
||||
# boot, so a camera someone brought up by hand does not silently outlive the
|
||||
# MOD_CAM=0 they set afterwards.
|
||||
@@ -1,179 +0,0 @@
|
||||
#!/bin/sh
|
||||
# The camera, as s6 supervises it: mjpg_streamer on :8080, in the FOREGROUND.
|
||||
#
|
||||
# WHAT THIS REPLACES. Until phase 4 this was a `while true; do mjpg_streamer
|
||||
# ...; sleep 3; done` inside a background subshell of payload/init.d/S65camera,
|
||||
# started with svc_detach and remembered by a pidfile that held THE LOOP rather
|
||||
# than the streamer -- because killing only the streamer got it restarted three
|
||||
# seconds later. All of that is gone. s6-supervise runs this script, notices
|
||||
# when it exits, and runs it again; the respawn loop, the pidfile, the "which
|
||||
# of the two processes do I kill first" ordering and the 15-second
|
||||
# wait-for-it-to-die in stop() are the supervisor's problem now, in C, once,
|
||||
# instead of ours in ash, per service.
|
||||
#
|
||||
# So the ONE rule this file has to keep is: never fork the streamer, never
|
||||
# background it, never return while the camera is meant to be running. `exec`
|
||||
# at the bottom is what makes the process s6 supervises BE mjpg_streamer, so
|
||||
# that s6-svstat reports the streamer's own pid and `s6-svc -d` signals the
|
||||
# streamer rather than a shell that has long since gone.
|
||||
#
|
||||
# EVERYTHING BELOW THE WAIT IS COPIED FROM THE OLD supervise(), DELIBERATELY.
|
||||
# LD_LIBRARY_PATH matters: libjpeg.so.9 lives in the mjpg-streamer directory
|
||||
# itself, so without it the binary dies on a missing shared object. That one
|
||||
# directory is prepended here rather than in anvil-env.sh, which carries the
|
||||
# shared list: it is a plugin directory with its own libjpeg, and putting it
|
||||
# in front of every python process on the printer would risk a version
|
||||
# conflict to benefit one service.
|
||||
#
|
||||
# THE BINARY, THE PLUGINS AND -w ARE ALL ABSOLUTE PATHS BELOW, deliberately,
|
||||
# even though `cd "$CAMDIR"` still happens: relative plugin names worked in
|
||||
# the replica (glibc's dlopen falls back to LD_LIBRARY_PATH for a bare name)
|
||||
# but not on real hardware, reported and confirmed fixed by users running the
|
||||
# equivalent invocation by hand with every path spelled out. Whatever this
|
||||
# printer's dlopen actually does with a bare "input_uvc.so", it is not worth
|
||||
# relying on again -- the cwd is kept only because some future flag may still
|
||||
# want it, not because anything below reads it.
|
||||
MODDIR=/usr/data/anvil
|
||||
# anvil.conf is read HERE, not inherited. s6-supervise starts this script from
|
||||
# the SCANNER's environment, which is whatever firmwareExe had at boot -- and
|
||||
# on a `s6-svc -u` typed over ssh it is whatever that ssh session had, which is
|
||||
# not the same thing at all. Reading the file in the run script is what makes
|
||||
# MOD_CAM_RES/FPS/PORT mean the same on both paths, and it is why editing
|
||||
# anvil.conf and restarting the service is enough to change the resolution.
|
||||
[ -f $MODDIR/anvil.conf ] && . $MODDIR/anvil.conf
|
||||
[ -f $MODDIR/anvil-env.sh ] && . $MODDIR/anvil-env.sh
|
||||
|
||||
CAMDIR=/usr/prog/mjpg-streamer
|
||||
VIDEO=/dev/video0
|
||||
LOG=/usr/data/logs/mjpg.log
|
||||
RES="${MOD_CAM_RES:-1280x720}"
|
||||
FPS="${MOD_CAM_FPS:-30}"
|
||||
PORT="${MOD_CAM_PORT:-8080}"
|
||||
|
||||
# The same log file the hand-rolled loop wrote to, for the same reason: it is
|
||||
# the file the troubleshooting notes name, and mjpg_streamer is chatty enough
|
||||
# (a banner per start, a line per plugin option) that leaving it on the
|
||||
# inherited descriptor would put it in the boot log of a boot that finished
|
||||
# hours ago -- s6-svscan's descriptors outlive firmwareExe's redirection
|
||||
# exactly as the scanner itself does. Appending, not truncating, so a respawn
|
||||
# cannot erase the reason for the respawn.
|
||||
mkdir -p /usr/data/logs
|
||||
exec >>"$LOG" 2>&1
|
||||
|
||||
# ---- the 30-second wait for /dev/video0 ------------------------------------
|
||||
#
|
||||
# WHY THE WAIT LIVES HERE AND NOT IN S65camera. It used to be the first half of
|
||||
# the detached supervise(): poll for the device, give up after 30s, print
|
||||
# "no camera plugged in?" and never try again until somebody re-ran the script.
|
||||
# The other candidate home for it was a `svc_s6_ready camera 30` in the init
|
||||
# script -- wait in the caller instead of the callee. This is the callee, and
|
||||
# the reason is that only here does giving up turn into RETRYING:
|
||||
#
|
||||
# * The device is not a boot-time fact. A USB camera plugged in ten minutes
|
||||
# after boot, or unplugged and replugged mid-print, used to mean a dark
|
||||
# camera until somebody ssh'd in and typed `S65camera restart`. Here the
|
||||
# script exits non-zero, s6 runs it again, and the next thirty seconds of
|
||||
# waiting cost one sleeping shell. The retry is free because it is exactly
|
||||
# what the supervisor is already doing for a streamer that dies.
|
||||
# * Waiting in the init script would put a 30-second block on the boot path
|
||||
# of precisely the printer with no camera -- the machine somebody needs to
|
||||
# ssh into to find out why. S65camera does still wait, but DETACHED and
|
||||
# only in order to report; nothing on the boot path blocks on it.
|
||||
# * A caller-side wait happens once. This one is the service's own
|
||||
# precondition, and a precondition belongs with the thing it precedes.
|
||||
#
|
||||
# 30 seconds is the number the old poll used and there is no reason to move it:
|
||||
# it is how long a USB camera can take to enumerate on this box.
|
||||
waited=0
|
||||
while [ ! -e "$VIDEO" ] && [ $waited -lt 30 ]; do
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
if [ ! -e "$VIDEO" ]; then
|
||||
# No readiness notification is sent on this path, and that is the point:
|
||||
# `s6-svwait -U` in the caller keeps blocking, times out, and S65camera
|
||||
# prints the honest line. A service that declared itself ready here would
|
||||
# be telling nginx's /webcam/ proxy there is a stream to proxy.
|
||||
echo "camera: !! $VIDEO never appeared after ${waited}s -- no camera plugged in?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export LD_LIBRARY_PATH=$CAMDIR:$LD_LIBRARY_PATH
|
||||
cd "$CAMDIR" || exit 1
|
||||
|
||||
# ---- readiness: fd 3, and what "ready" is allowed to mean ------------------
|
||||
#
|
||||
# The `notification-fd` file next to this script holds "3", which is s6's
|
||||
# contract: this service is READY once a line is written to file descriptor 3,
|
||||
# and `s6-svwait -U` blocks until it is. This is the one thing s6 has that
|
||||
# runit has no concept of, and it is the reason the supervisor choice went the
|
||||
# way it did -- see tools/supervisor/README.md.
|
||||
#
|
||||
# WHEN IS THE CAMERA READY? Three candidate answers, and only one of them is
|
||||
# worth anything to whoever asks:
|
||||
#
|
||||
# "the process was forked" -- worthless. That is what runit's `sv start`
|
||||
# already gives you and what a pidfile already told us. mjpg_streamer
|
||||
# forks, opens /dev/video0, negotiates a pixel format and a resolution,
|
||||
# and only then binds its port; the gap between fork and usable is
|
||||
# exactly where a camera that enumerated but cannot do 1280x720 dies.
|
||||
# "/dev/video0 exists" -- better, and it is what the old poll checked,
|
||||
# but it is a PRECONDITION, not readiness. The node appears the instant
|
||||
# the kernel enumerates the device, before anything has proved it can be
|
||||
# opened, and it is equally there for a webcam whose format negotiation
|
||||
# is about to fail.
|
||||
# "the HTTP port answers" -- this one. The only consumer of this service
|
||||
# is nginx's /webcam/ proxy (moonraker.conf's [webcam] entry points at
|
||||
# the proxy rather than at :8080, so that a tunnel forwarding one port
|
||||
# still shows the stream), and what a proxy needs is a socket that
|
||||
# accepts. By the time mjpg_streamer has bound the port it has already
|
||||
# opened the device and set the format -- output_http.so is started
|
||||
# last. So this is the strongest claim available and the only one that
|
||||
# means what a caller thinks it means.
|
||||
#
|
||||
# HOW IT IS CHECKED, and why it needs no tool. /proc/net/tcp lists every TCP
|
||||
# socket on the box: field 2 is LOCAL_ADDRESS:PORT with the port in uppercase
|
||||
# hex, field 4 is the state, and 0A is TCP_LISTEN. No nc, no curl, no wget --
|
||||
# measured on the replica, whose busybox has no nc applet at all, so a
|
||||
# shell-level port test that reached for one would have failed on the printer
|
||||
# and nowhere else. The address half is deliberately not matched:
|
||||
# mjpg_streamer binds 0.0.0.0 today, and a future `-l 127.0.0.1` should still
|
||||
# count as listening.
|
||||
#
|
||||
# The prober is a subshell because the last thing this script does is exec, and
|
||||
# an exec'd mjpg_streamer cannot report on itself. It inherits fd 3 across the
|
||||
# fork; the exec below closes fd 3 in the STREAMER (3>&-) so the only process
|
||||
# holding the notification pipe is the one with something to say about it. It
|
||||
# gives up after 60s rather than looping for ever, because a prober that never
|
||||
# exits is one stray process per respawn -- and giving up simply means no
|
||||
# notification is ever sent, which is the same honest outcome as the no-device
|
||||
# path above.
|
||||
PORTHEX=`printf '%04X' "$PORT"`
|
||||
(
|
||||
n=0
|
||||
while [ $n -lt 60 ]; do
|
||||
if awk -v p=":$PORTHEX" '$2 ~ p"$" && $4 == "0A" { found = 1 }
|
||||
END { exit !found }' /proc/net/tcp 2>/dev/null
|
||||
then
|
||||
echo ready >&3
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
n=$((n + 1))
|
||||
done
|
||||
) &
|
||||
|
||||
# The exact invocation the hand-rolled loop used, argument for argument. Do not
|
||||
# "tidy" the quoting: mjpg_streamer takes a plugin and all of its options as
|
||||
# ONE argv entry and splits it itself, so those quotes are load-bearing.
|
||||
#
|
||||
# nice -n "${NICE_CAM:-0}" in front: the pre-s6 respawn loop this replaced
|
||||
# used the same applet for the same reason (NICE_CAM in anvil.conf -- the
|
||||
# streamer is pure background work and the most deserving of the two
|
||||
# services that yield to klippy), and `nice` execs the target in its own
|
||||
# place, so s6 still ends up holding the streamer's own pid, re-niced on
|
||||
# every respawn because s6-supervise re-execs this whole script each time.
|
||||
# 0 or unset = normal priority.
|
||||
exec nice -n "${NICE_CAM:-0}" "$CAMDIR/mjpg_streamer" \
|
||||
-i "$CAMDIR/input_uvc.so -d $VIDEO -r $RES -f $FPS" \
|
||||
-o "$CAMDIR/output_http.so -p $PORT -w $CAMDIR/www" 3>&-
|
||||
@@ -1,56 +0,0 @@
|
||||
This file's presence, not its contents, is the whole message: s6-supervise
|
||||
starts a service whose directory contains a file called `down` in the DOWN
|
||||
state instead of starting it. s6 never opens the file, so the reasoning can
|
||||
live in it rather than in a comment somewhere else that would drift away from
|
||||
it. (case-moonraker.sh checks that too, with this text in place, because a
|
||||
fact this file depends on is worth measuring rather than believing.)
|
||||
|
||||
WHY MOONRAKER SHIPS DOWN. The gate on the web stack is MOD_WEB in anvil.conf,
|
||||
and it is ONE switch across two services -- init.d/S60nginx and
|
||||
init.d/S62moonraker -- because "no web stack" is one intention and there is no
|
||||
useful printer with Mainsail and no moonraker behind it. That switch is read
|
||||
AT RUNTIME, on the printer, out of a file the user edits over ssh; this payload
|
||||
is built ONCE, weeks earlier, by bin/patch.sh. So the scandir cannot be where
|
||||
the decision is made: a service directory that ships without this file is a
|
||||
service the scanner starts a fraction of a second after init.d/S40s6 runs,
|
||||
before anything has read anvil.conf and regardless of what it says. MOD_WEB=0
|
||||
would mean "moonraker runs for two seconds on every boot and is then shot",
|
||||
which is not what it says on the tin and is worse than not having the switch.
|
||||
|
||||
So the shipped state is "down", meaning: THE SCANNER DOES NOT DECIDE WHEN
|
||||
MOONRAKER STARTS. init.d/S62moonraker does, at the point in the boot where
|
||||
anvil.conf has been sourced, by calling svc_s6_up -- which overrides this file
|
||||
for as long as that s6-supervise lives. MOD_WEB=1 and it comes up there;
|
||||
MOD_WEB=0 and it never comes up at all, which is exactly what the switch has
|
||||
always promised. The same is then true of every other way in: `S62moonraker
|
||||
start` over ssh re-reads anvil.conf, and a user who sets MOD_WEB=0 and reboots
|
||||
gets no API server rather than one that has to be caught and stopped.
|
||||
|
||||
IT MATTERS MORE HERE THAN ANYWHERE ELSE, and that is worth saying plainly.
|
||||
Moonraker is not a service that idles when it is started too early: it opens
|
||||
its database, binds :7125, and connects to klippy's socket at /tmp/uds. A
|
||||
scanner-started moonraker would be doing all of that before S60nginx, before
|
||||
S70klipper and before anything had decided whether klippy is even meant to
|
||||
run -- and the copy that init.d/S62moonraker started afterwards would then be
|
||||
the SECOND one, fighting the first for the port, in the exact respawn loop the
|
||||
sweep at the top of the `run` script exists to prevent. The `down` file is what
|
||||
keeps that loop hypothetical.
|
||||
|
||||
THE COST, stated honestly, because it is the one thing this arrangement gives
|
||||
up. `down` is consulted when s6-supervise STARTS, not when the service dies:
|
||||
once S62moonraker has brought moonraker up, s6 keeps restarting it on every
|
||||
crash for as long as that supervisor lives, which is the entire point of the
|
||||
migration and is unaffected by this file. What it does mean is that if
|
||||
s6-supervise ITSELF is killed -- not moonraker, the supervisor -- s6-svscan
|
||||
starts a fresh one, that fresh one reads this file, and moonraker stays down
|
||||
until something runs `S62moonraker start` again. That is a rare failure
|
||||
(s6-supervise is a small C program that blocks in poll() and is not the thing
|
||||
that crashes) with an honest signal (`S62moonraker status` says it is not
|
||||
running, and says so from s6-svstat rather than from a pidfile) and a one-line
|
||||
fix from an ssh session. The alternative -- deleting this file at runtime once
|
||||
MOD_WEB has been read -- buys back that corner case at the price of a payload
|
||||
that rewrites its own install tree, which every path in this mod is arranged
|
||||
not to do.
|
||||
|
||||
This file is not a record of anything having gone wrong: it is the state
|
||||
moonraker ships in, on every build, and nothing removes it.
|
||||
@@ -1,258 +0,0 @@
|
||||
#!/bin/sh
|
||||
# The moonraker service, as s6 runs it. Exec'd by s6-supervise, never by hand.
|
||||
#
|
||||
# WHAT A `run` SCRIPT IS. s6-svscan sees this directory in the scandir and
|
||||
# starts one s6-supervise for it; that s6-supervise forks, puts the child in a
|
||||
# process group of its own, and execs this file. The process this script turns
|
||||
# into IS the service: when it exits, s6-supervise starts the whole thing
|
||||
# again. Everything below follows from that one sentence.
|
||||
#
|
||||
# THEREFORE: NO start-stop-daemon, and no `-b`. init.d/S62moonraker used to
|
||||
# start moonraker with `start-stop-daemon -S -b -m -p /run/moonraker.pid`,
|
||||
# which forks the server into the background and returns. Under a supervisor
|
||||
# that is exactly wrong: s6 would supervise the process that forked and exited,
|
||||
# decide the service had died a millisecond after starting it, and start it
|
||||
# again for ever, while the real moonraker it left behind went on serving
|
||||
# :7125 supervised by nobody. So the invocation at the bottom is the same
|
||||
# interpreter, the same entry point and the same `-d` argument as the old
|
||||
# daemon line -- argument for argument -- with the backgrounding removed and
|
||||
# `exec` in front.
|
||||
#
|
||||
# AND `exec`. Without it this shell would stay around as moonraker's parent, s6
|
||||
# would be supervising the shell, and every signal it sent -- the SIGTERM
|
||||
# behind `S62moonraker stop` above all -- would go to a shell that ignores it
|
||||
# while moonraker carried on. exec makes the pid s6 holds the pid of the python
|
||||
# process itself, which is also what makes `s6-svstat -p` the honest answer to
|
||||
# "which moonraker is this printer running?" that the pidfile used to give.
|
||||
#
|
||||
# THIS FILE DOES NOT LOOK AT MOD_WEB. It cannot: it runs whenever s6 decides
|
||||
# the service should be up, and a `run` that exits because it disagrees is a
|
||||
# respawn loop with extra steps. The gate is the `down` file next to this one
|
||||
# plus init.d/S62moonraker, which is the thing that reads anvil.conf at the
|
||||
# point in the boot where reading it means something. See the comment inside
|
||||
# `down`; that is where the whole MOD_WEB story is written.
|
||||
#
|
||||
# NOTHING HERE MOVES. The interpreter, the entry point, the data directory and
|
||||
# the config moonraker reads out of it are all exactly where init.d/S62moonraker
|
||||
# had them. Moving /usr/data/config is phase 7 of docs/notes/80-s6-migration.md
|
||||
# and it is deliberately not this change: FlashForge's own klipperDaemon
|
||||
# hardcodes /usr/data/config/printer.cfg, so the directory cannot move until we
|
||||
# own the klippy launch as well. When it does move, the `-d` below is the one
|
||||
# line in this file that changes.
|
||||
MODDIR=/usr/data/anvil
|
||||
|
||||
# anvil.conf and anvil-env.sh are read HERE, not inherited, for the reason the
|
||||
# camera's run script gives at the same point: s6-supervise starts this script
|
||||
# from the SCANNER's environment, which is whatever firmwareExe had at boot --
|
||||
# and on an `s6-svc -u` typed over ssh it is whatever that ssh session had,
|
||||
# which is not the same thing at all. anvil-env.sh is the file that defines
|
||||
# FF_PYTHON and LD_LIBRARY_PATH, and moonraker is the process that dies with
|
||||
# "libpython3.8.so.1.0: cannot open shared object file" when that is wrong --
|
||||
# so a supervised respawn that inherited a stranger's environment would be a
|
||||
# service that works on the boot path and fails on every restart afterwards.
|
||||
[ -f $MODDIR/anvil.conf ] && . $MODDIR/anvil.conf
|
||||
[ -f $MODDIR/anvil-env.sh ] && . $MODDIR/anvil-env.sh
|
||||
|
||||
# ONE moonraker, at one path, and that path is on the DATA partition. The long
|
||||
# version is in init.d/S62moonraker and it has not changed: the byte-identical
|
||||
# second copy under /usr/prog is gone, because /usr/prog is the partition a
|
||||
# stock FlashForge flash overwrites, and running from there made "which
|
||||
# moonraker is this printer running?" depend on what was flashed last.
|
||||
MOONRAKER_MAIN=$MODDIR/moonraker/moonraker.py
|
||||
MOONRAKER_DATA=/usr/data
|
||||
MOONRAKER_TMP=/usr/data/tmp
|
||||
MOONRAKER_CONF=$MOONRAKER_DATA/config/moonraker.conf
|
||||
|
||||
# TMPDIR, which is one of the exactly two things FlashForge's moonrakerDaemon
|
||||
# did for us that were worth keeping -- the other was the pidfile, and s6 has
|
||||
# made that one redundant. Moonraker writes temporary files, uploads among
|
||||
# them, and /tmp on this box is a ramdisk, so a large upload without this fills
|
||||
# memory instead of the data partition. It is exported here rather than left to
|
||||
# the init script because s6 restarts this service without going anywhere near
|
||||
# init.d/S62moonraker: a service that only has its environment when a shell
|
||||
# script happened to run first is not supervised, it is lucky.
|
||||
#
|
||||
# The wipe is unconditional here where the init script had to guard it on
|
||||
# "moonraker is not already running". That guard existed because re-running
|
||||
# S62moonraker over ssh against a LIVE server would otherwise delete the
|
||||
# directory it was streaming an upload into. Under s6 it is satisfied by
|
||||
# construction: s6-supervise runs exactly one copy of this service at a time
|
||||
# and only execs this script after the previous one has been reaped, so at the
|
||||
# moment this line runs there is no moonraker of ours anywhere.
|
||||
rm -rf $MOONRAKER_TMP
|
||||
mkdir -p $MOONRAKER_TMP $MOONRAKER_DATA/logs
|
||||
export TMPDIR=$MOONRAKER_TMP
|
||||
|
||||
# ---- the leftover from an unsupervised start -------------------------------
|
||||
#
|
||||
# WHY THIS IS HERE. init.d/S62moonraker still has a fallback that starts
|
||||
# moonraker directly when there is no scanner -- see the long argument in that
|
||||
# file -- so a printer can reach the moment when the supervised service comes
|
||||
# up with a hand-started moonraker already holding :7125. It can also reach it
|
||||
# by way of an upgrade: a printer running the pre-phase-5 payload has a
|
||||
# start-stop-daemon'd moonraker alive with its pid in /run/moonraker.pid, and
|
||||
# installing this payload and typing `S62moonraker restart` rather than
|
||||
# rebooting would leave it exactly there.
|
||||
#
|
||||
# What follows is not a graceful loss. Moonraker exits when it cannot bind its
|
||||
# port, s6 starts it again a second later, it cannot bind again, and the loop
|
||||
# runs for as long as the printer is on -- with `s6-svstat` saying "up" most of
|
||||
# the time, because the process it is watching really is alive, it is just
|
||||
# dying -- while what is actually answering Mainsail is an unsupervised
|
||||
# leftover that nothing will ever restart. That is the same failure the nginx
|
||||
# run script sweeps for, measured there first, and it is worth the same handful
|
||||
# of lines here.
|
||||
#
|
||||
# It reads /proc rather than `ps | grep`, for the reason written out in the
|
||||
# nginx run script: busybox `ps` truncates its output to the terminal width, so
|
||||
# a long python command line arrives cut in half and a grep for the entry point
|
||||
# matches nothing at all. /proc/PID/cmdline is the whole command line, always.
|
||||
#
|
||||
# The match is POSITIVE and narrow -- the absolute path of the entry point we
|
||||
# are about to run, which appears in the command line of a moonraker and of
|
||||
# nothing else on this printer. `s6-supervise moonraker`, this script and
|
||||
# `init.d/S62moonraker start` all have the word moonraker in their command
|
||||
# lines and none of them has moonraker.py's absolute path in it; our own pid is
|
||||
# skipped as well, for the same belt-and-braces reason nginx skips it.
|
||||
for _mr_proc in /proc/[0-9]*; do
|
||||
[ -r "$_mr_proc/cmdline" ] || continue
|
||||
[ "${_mr_proc#/proc/}" = "$$" ] && continue
|
||||
_mr_args=`tr '\0' ' ' < "$_mr_proc/cmdline" 2>/dev/null`
|
||||
case "$_mr_args" in
|
||||
*"$MOONRAKER_MAIN"*) kill -9 "${_mr_proc#/proc/}" 2>/dev/null ;;
|
||||
esac
|
||||
done
|
||||
unset _mr_proc _mr_args
|
||||
|
||||
# ---- readiness: fd 3, and what "ready" is allowed to mean ------------------
|
||||
#
|
||||
# The `notification-fd` file next to this script holds "3": this service is
|
||||
# READY once a line is written to file descriptor 3, and `s6-svwait -U` blocks
|
||||
# until it is. This is the capability runit has no concept of and the reason
|
||||
# the supervisor choice went the way it did -- see tools/supervisor/README.md.
|
||||
#
|
||||
# WHY MOONRAKER GETS ONE AT ALL, when nginx does not. Because something already
|
||||
# waits for it, badly. bin/ff-startup.py -- the program S70klipper hands the
|
||||
# boot to -- polls http://127.0.0.1:7125/server/info in a retry loop to learn
|
||||
# klippy_state, and every one of those early polls is against a moonraker that
|
||||
# has been forked and is not listening yet. init.d/S62moonraker had its own
|
||||
# version of the same guess: a detached moonraker_check that slept two seconds
|
||||
# five times and called the service "up" if the pid was still there. A pid that
|
||||
# is still there is not an API that answers, and the gap between the two is
|
||||
# exactly the window in which moonraker imports its components and dies
|
||||
# quietly.
|
||||
#
|
||||
# WHEN IS MOONRAKER READY? The candidates, and why only one of them is worth
|
||||
# telling a caller:
|
||||
#
|
||||
# "the process was forked" -- worthless, and it is what the pidfile and the
|
||||
# old moonraker_check both actually measured. Component-load failures
|
||||
# happen seconds AFTER the fork, which is why that check had to sleep at
|
||||
# all, and a moonraker that is still importing components answers
|
||||
# nothing.
|
||||
# "klippy_state is ready" -- WRONG, and tempting, because that is what
|
||||
# ff-startup.py is ultimately waiting for. But it is not a fact about
|
||||
# THIS service. Moonraker is up and doing its job on a printer whose MCU
|
||||
# never answered -- reporting klippy_state=error IS the job -- so a
|
||||
# readiness that waited for klippy would never fire on precisely the
|
||||
# broken printer whose UI somebody needs, and the caller would time out
|
||||
# and print a warning about a moonraker that was working perfectly. It
|
||||
# would also invert the dependency: moonraker would be declaring itself
|
||||
# ready on behalf of a service it neither owns nor starts.
|
||||
# "the HTTP API is listening" -- this one. Every consumer of this service --
|
||||
# Mainsail through nginx's /webcam-and-everything-else proxy,
|
||||
# HelixScreen, and ff-startup.py -- is an HTTP client, and what an HTTP
|
||||
# client needs is a socket that accepts. Moonraker binds its port after
|
||||
# the server object is built and its core components have imported, so a
|
||||
# bound port is also the strongest available statement about the
|
||||
# component failures the old check was written to catch.
|
||||
#
|
||||
# HOW IT IS CHECKED, and why it needs no tool. /proc/net/tcp lists every TCP
|
||||
# socket on the box: field 2 is LOCAL_ADDRESS:PORT with the port in uppercase
|
||||
# hex, field 4 is the state, and 0A is TCP_LISTEN. No nc -- this busybox has no
|
||||
# nc applet at all, measured on the replica -- and no wget in the hot path of a
|
||||
# supervised service. The address half is deliberately not matched: moonraker
|
||||
# binds 0.0.0.0 under the shipped config, and a user who sets host: 127.0.0.1
|
||||
# should still count as listening.
|
||||
#
|
||||
# THE PORT IS READ FROM MOONRAKER'S OWN CONFIG, not written down here. 7125 is
|
||||
# the shipped default and also moonraker's, but it is a user-editable line in a
|
||||
# user-editable file, and a probe watching the wrong port would report a
|
||||
# perfectly healthy moonraker as never ready -- which is worse than having no
|
||||
# probe at all, because S62moonraker would then print a warning and a log tail
|
||||
# on every boot of a printer that is working. The awk is section-aware so that
|
||||
# a `port:` belonging to some other component cannot be picked up by mistake.
|
||||
MOONRAKER_PORT=`awk '
|
||||
/^[[:space:]]*\[/ { section = $0 }
|
||||
section ~ /\[[[:space:]]*server[[:space:]]*\]/ && /^[[:space:]]*port[[:space:]]*:/ {
|
||||
sub(/^[^:]*:[[:space:]]*/, "");
|
||||
gsub(/[^0-9]/, "");
|
||||
if (length($0)) { print; exit }
|
||||
}' "$MOONRAKER_CONF" 2>/dev/null`
|
||||
[ -n "$MOONRAKER_PORT" ] || MOONRAKER_PORT=7125
|
||||
|
||||
# The prober is a subshell because the last thing this script does is exec, and
|
||||
# an exec'd moonraker cannot report on itself. It inherits fd 3 across the
|
||||
# fork; the exec below closes fd 3 in MOONRAKER (3>&-) so that the only process
|
||||
# holding the notification pipe is the one with something to say about it --
|
||||
# were moonraker to keep it open, s6 would never see the pipe close when the
|
||||
# prober gave up.
|
||||
#
|
||||
# 180 seconds, which is much longer than the camera's 60 and is not a copied
|
||||
# number. Moonraker imports its whole component tree on a 1GHz mipsel box off
|
||||
# eMMC; on the qemu replica the same startup is slower again, and
|
||||
# case-moonraker.sh has waited up to 105 seconds for the API since long before
|
||||
# s6 was involved. A prober that gave up first would turn a slow printer into a
|
||||
# printer that reports itself broken. Giving up simply means no notification is
|
||||
# ever sent, which is the honest outcome: S62moonraker then prints its warning
|
||||
# and the tail of moonraker.log, exactly as its old detached check did.
|
||||
(
|
||||
_mr_n=0
|
||||
while [ $_mr_n -lt 180 ]; do
|
||||
if awk -v p=":`printf '%04X' "$MOONRAKER_PORT"`" \
|
||||
'$2 ~ p"$" && $4 == "0A" { found = 1 }
|
||||
END { exit !found }' /proc/net/tcp 2>/dev/null
|
||||
then
|
||||
echo ready >&3
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
_mr_n=$((_mr_n + 1))
|
||||
done
|
||||
) &
|
||||
|
||||
# THE INVOCATION, preserved argument for argument from the start-stop-daemon
|
||||
# line it replaces:
|
||||
#
|
||||
# start-stop-daemon -S -b -m -p /run/moonraker.pid --exec "$FF_PYTHON" \
|
||||
# -- "$MODDIR/moonraker/moonraker.py" -d /usr/data
|
||||
#
|
||||
# `-d /usr/data` is moonraker's data path: it is where it finds
|
||||
# config/moonraker.conf, where it writes logs/moonraker.log and where its
|
||||
# database lives. It is not a debug flag and it is not optional -- moonraker
|
||||
# defaults to ~/printer_data, which on this printer is a home directory that
|
||||
# does not exist.
|
||||
#
|
||||
# stderr is inherited from s6-supervise, which inherited it from the scanner,
|
||||
# which init.d/S40s6 points at /usr/data/logs/s6.log. That is a genuine
|
||||
# improvement rather than an accident of the port: under start-stop-daemon -b
|
||||
# these descriptors went to /dev/null, so a moonraker that died before its own
|
||||
# logging was configured -- a syntax error in a component, a missing shared
|
||||
# object -- died with its explanation thrown away, and the only symptom was a
|
||||
# service that "did not come up". Now it lands in a file that outlives the
|
||||
# boot. Moonraker's own structured log stays exactly where it has always been,
|
||||
# at $MOONRAKER_DATA/logs/moonraker.log, because moonraker opens that itself
|
||||
# from -d.
|
||||
#
|
||||
# nice -n "${NICE_MOONRAKER:-0}", not a plain exec of the interpreter: this
|
||||
# service is supervised now, which init.d/S62moonraker's start_unsupervised()
|
||||
# fallback is not, and NICE_MOONRAKER (anvil.conf) exists to protect klippy
|
||||
# from exactly the process this script starts -- reaching only the fallback
|
||||
# would leave the feature dead for every printer that has s6, which by
|
||||
# default is all of them. `nice` sets its own priority and then execs the
|
||||
# given command in its own place (verified: the pid s6 ends up holding is the
|
||||
# interpreter's, not nice's, exactly as required above), so this is still one
|
||||
# process and $FF_PYTHON is still what --exec would have matched had this
|
||||
# been start-stop-daemon. 0 or unset = normal priority, unchanged from before
|
||||
# this existed.
|
||||
exec nice -n "${NICE_MOONRAKER:-0}" "$FF_PYTHON" "$MOONRAKER_MAIN" -d "$MOONRAKER_DATA" 3>&-
|
||||
@@ -1,43 +0,0 @@
|
||||
This file's presence, not its contents, is the whole message: s6-supervise
|
||||
starts a service whose directory contains a file called `down` in the DOWN
|
||||
state instead of starting it. s6 never opens the file, so the reasoning can
|
||||
live in it rather than in a comment somewhere else that would drift away from
|
||||
it. (The gate checks that too, with this text in place, because a fact this
|
||||
file depends on is worth measuring rather than believing.)
|
||||
|
||||
WHY NGINX SHIPS DOWN. The gate on the web stack is MOD_WEB in anvil.conf. It
|
||||
is read AT RUNTIME, on the printer, from a file the user edits over ssh -- and
|
||||
this payload is built ONCE, weeks earlier, by bin/patch.sh. So the scandir
|
||||
cannot be where the decision is made: a service directory that ships without
|
||||
this file is a service the scanner starts a fraction of a second after
|
||||
init.d/S40s6 runs, before anything has read anvil.conf and regardless of what
|
||||
it says. MOD_WEB=0 would mean "nginx runs for two seconds on every boot and is
|
||||
then shot", which is not what it says on the tin and is worse than not having
|
||||
the switch.
|
||||
|
||||
So the shipped state is "down", meaning: THE SCANNER DOES NOT DECIDE WHEN
|
||||
NGINX STARTS. init.d/S60nginx does, at the point in the boot where anvil.conf
|
||||
has been sourced, by calling svc_s6_up -- which overrides this file for as
|
||||
long as that s6-supervise lives. MOD_WEB=1 and it comes up there; MOD_WEB=0
|
||||
and it never comes up at all, which is exactly what the switch has always
|
||||
promised. The same is then true of every other way in: `S60nginx start` over
|
||||
ssh re-reads anvil.conf, and a user who sets MOD_WEB=0 and reboots gets no web
|
||||
server rather than a web server that has to be caught and stopped.
|
||||
|
||||
THE COST, stated honestly, because it is the one thing this arrangement gives
|
||||
up. `down` is consulted when s6-supervise STARTS, not when the service dies:
|
||||
once S60nginx has brought nginx up, s6 keeps restarting it on every crash for
|
||||
as long as that supervisor lives, which is the entire point of the migration
|
||||
and is unaffected by this file. What it does mean is that if s6-supervise
|
||||
ITSELF is killed -- not nginx, the supervisor -- s6-svscan starts a fresh one,
|
||||
that fresh one reads this file, and nginx stays down until something runs
|
||||
`S60nginx start` again. That is a rare failure (s6-supervise is a small C
|
||||
program that blocks in poll() and is not the thing that crashes) with an
|
||||
honest signal (`S60nginx status` says it is not running, and says so from
|
||||
s6-svstat rather than from a pidfile) and a one-line fix from an ssh session.
|
||||
The alternative -- deleting this file at runtime once MOD_WEB has been read --
|
||||
buys back that corner case at the price of a payload that rewrites its own
|
||||
install tree, which every path in this mod is arranged not to do.
|
||||
|
||||
This file is not a record of anything having gone wrong: it is the state nginx
|
||||
ships in, on every build, and nothing removes it.
|
||||
@@ -1,105 +0,0 @@
|
||||
#!/bin/sh
|
||||
# The nginx service, as s6 runs it. Exec'd by s6-supervise, never by hand.
|
||||
#
|
||||
# WHAT A `run` SCRIPT IS. s6-svscan sees this directory in the scandir and
|
||||
# starts one s6-supervise for it; that s6-supervise forks, puts the child in a
|
||||
# process group of its own, and execs this file. The process this script turns
|
||||
# into IS the service: when it exits, s6-supervise runs `finish` if there is
|
||||
# one and then starts the whole thing again. That is the entire contract, and
|
||||
# every line below follows from it.
|
||||
#
|
||||
# THEREFORE: `daemon off;`. nginx's default is to fork a master into the
|
||||
# background and return, which is what init.d/S60nginx relied on for years and
|
||||
# is exactly wrong here -- s6 would supervise the process that forked and
|
||||
# exited, decide the service had died a millisecond after starting it, and
|
||||
# start it again, for ever, while the real nginx it left behind kept serving
|
||||
# quite happily and was supervised by nobody. `-g` puts a directive on the
|
||||
# command line as though it were at the top of nginx.conf, so the config file
|
||||
# stays the one thing it should be -- shared with the hand-run and fallback
|
||||
# paths -- and only the way we are launching it changes. Verified on the
|
||||
# replica against this printer's own nginx (1.5.0, FlashForge's build, from
|
||||
# /usr/prog): with `daemon off;` the pid we launched is still the master five
|
||||
# seconds later, and it still writes logs/nginx.pid, which is what lets
|
||||
# S60nginx's non-supervised fallback keep working unchanged.
|
||||
#
|
||||
# AND `exec`. Without it this shell would stay around as the parent of nginx,
|
||||
# s6 would supervise the shell, and every signal s6 sent -- the SIGTERM behind
|
||||
# `S60nginx stop` above all -- would go to a shell that ignores it while nginx
|
||||
# carried on. exec makes the pid s6 is holding the pid of nginx itself.
|
||||
#
|
||||
# THIS FILE DOES NOT LOOK AT MOD_WEB. It cannot: it runs whenever s6 decides
|
||||
# the service should be up, and a `run` that exits because it disagrees is a
|
||||
# respawn loop with extra steps. The gate is the `down` file next to this one
|
||||
# plus init.d/S60nginx, which is the thing that reads anvil.conf. See the
|
||||
# comment in `down` -- that is where the whole MOD_WEB story is written.
|
||||
MODDIR=/usr/data/anvil
|
||||
NGINX=/usr/prog/nginx/sbin/nginx
|
||||
|
||||
# -p sets the prefix: logs, the pidfile and the temp directories all land
|
||||
# under $MODDIR on the data partition, never on /usr/prog. nginx will not
|
||||
# start if it cannot create them and it does not create them itself -- the
|
||||
# same mkdir S60nginx has always done, repeated here because s6 restarts this
|
||||
# service without going anywhere near S60nginx, and a service that only works
|
||||
# when a shell script happened to run first is not supervised, it is lucky.
|
||||
mkdir -p $MODDIR/nginx/logs $MODDIR/nginx/tmp
|
||||
|
||||
# THE ORPHANED WORKER, and why there is a loop in what should be two lines.
|
||||
#
|
||||
# nginx is two processes: a master that binds :80 and a worker that inherits
|
||||
# the listening socket and answers on it. Kill the master politely -- which is
|
||||
# what `s6-svc -d` does, SIGTERM -- and it shuts the worker down with it;
|
||||
# measured on the replica, nothing is left and the port is free. Kill the
|
||||
# master with SIGKILL, which is what an OOM or a `kill -9` does, and the
|
||||
# worker is orphaned, KEEPS THE LISTENING SOCKET, and goes on serving. s6 then
|
||||
# does its job perfectly and makes everything worse: it starts a new nginx one
|
||||
# second later, that nginx cannot bind because the orphan still holds :80, it
|
||||
# retries five times over five seconds, prints "still could not bind()" and
|
||||
# exits, and s6 starts it again. For ever. `s6-svstat` says "up" the whole
|
||||
# time -- the process it is watching really is alive, it is just failing --
|
||||
# while what is actually answering the browser is an unsupervised leftover.
|
||||
# All of that was measured on the replica; none of it is a guess.
|
||||
#
|
||||
# So: before nginx starts, anything already speaking for it is a leftover, and
|
||||
# leftovers get killed. That is true by construction -- s6-supervise runs
|
||||
# exactly one copy of this service at a time, so at the moment this line runs
|
||||
# there is no nginx of ours anywhere -- and it makes the fallback in S60nginx
|
||||
# safe as well: a printer that started nginx directly because the scanner was
|
||||
# not running gets that copy swept the moment the supervised one takes over,
|
||||
# instead of two nginxes fighting over the port.
|
||||
#
|
||||
# It reads /proc rather than `ps | grep`, which is the opposite of what the
|
||||
# rest of the mod does and is deliberate. busybox `ps` truncates its output to
|
||||
# the terminal width, so on the replica the command line arrives cut off at
|
||||
# "/usr/prog/nginx/" and a grep for the binary matches nothing at all; and
|
||||
# busybox `kill` will not take a negative pid, so signalling the process group
|
||||
# -- the obvious fix -- is not available on this box either. Both were tried
|
||||
# and both failed there. /proc/PID/cmdline is the whole command line, always.
|
||||
#
|
||||
# The match is POSITIVE and narrow, because the cost of a wrong match here is
|
||||
# not a missed cleanup, it is killing the wrong thing: "s6-supervise nginx",
|
||||
# "/usr/data/anvil/init.d/S60nginx start" and this very script all have the
|
||||
# word nginx in their command lines, and s6-supervise in particular is the
|
||||
# process that would otherwise restart us. So only two shapes are swept -- the
|
||||
# ones nginx itself produces:
|
||||
#
|
||||
# nginx: master process /usr/prog/nginx/sbin/nginx -p ... (it rewrites
|
||||
# nginx: worker process its own argv)
|
||||
# ...anything exec'd from /usr/prog/nginx/sbin/nginx (before it does)
|
||||
#
|
||||
# and our own pid is skipped for good measure.
|
||||
for _ng_proc in /proc/[0-9]*; do
|
||||
[ -r "$_ng_proc/cmdline" ] || continue
|
||||
[ "${_ng_proc#/proc/}" = "$$" ] && continue
|
||||
_ng_args=`tr '\0' ' ' < "$_ng_proc/cmdline" 2>/dev/null`
|
||||
case "$_ng_args" in
|
||||
"nginx: "*|*"$NGINX"*) kill -9 "${_ng_proc#/proc/}" 2>/dev/null ;;
|
||||
esac
|
||||
done
|
||||
unset _ng_proc _ng_args
|
||||
|
||||
# stderr is where nginx puts the reason it would not start, and s6-supervise
|
||||
# hands this script the descriptors the scanner had, which is the s6 log --
|
||||
# /usr/data/logs/s6.log, per init.d/S40s6. That is the right place for it: it
|
||||
# is a file that outlives the boot, unlike the boot log S60nginx used to print
|
||||
# into, and "see the error above" finally points at something you can read.
|
||||
exec $NGINX -p $MODDIR/nginx -c $MODDIR/nginx/nginx.conf -g "daemon off;"
|
||||
+177
-123
@@ -4,23 +4,22 @@
|
||||
# The stock boot chain is:
|
||||
# busybox init -> /etc/init.d/rcS -> S99factory_test_shell
|
||||
# -> /usr/prog/app_startup.sh -> firmwareExe
|
||||
# and firmwareExe itself is what launches "/usr/prog/klipper/start.sh &".
|
||||
# Replacing this one file owns the whole userspace boot, so app_startup.sh, rcS
|
||||
# and the init chain stay 100% STOCK and are never patched.
|
||||
#
|
||||
# Because everything funnels through this one path, replacing this binary
|
||||
# with a script is the cleanest possible integration point: app_startup.sh,
|
||||
# rcS and the whole init chain stay 100% STOCK and are never patched.
|
||||
# THE WATCHDOG CONSTRAINT, which shapes the whole file: app_startup.sh greps
|
||||
# `ps` for "firmwareExe" 5s after launch and re-execs it if it is gone. So this
|
||||
# script keeps that name (it IS $0) and must stay in the FOREGROUND. The last
|
||||
# statement is `wait "$SCANNER_PID"`.
|
||||
#
|
||||
# HelixScreen is the only UI. FlashForge's binary is not kept anywhere: the
|
||||
# installer wipes the software dir before run.sh anyway, so the way back is
|
||||
# flashing the stock FlashForge package, which still ships it.
|
||||
# If this script is ever removed, app_startup.sh restores a firmwareExe from a
|
||||
# version directory -- the stock recovery path still works.
|
||||
#
|
||||
# Two further properties we get for free:
|
||||
# * app_startup.sh's own watchdog greps `ps` for "firmwareExe" 5s after
|
||||
# launch. This script keeps that name (it IS $0) and stays in the
|
||||
# FOREGROUND, so the stock watchdog supervises the mod correctly.
|
||||
# * If this script is ever removed, app_startup.sh restores a firmwareExe
|
||||
# from a version directory (the last `ls` entry, alphabetical -- and there is
|
||||
# normally only one) -- the stock recovery path still works.
|
||||
# WHAT IT DOES: start s6-svscan as its own child, initialise s6-rc, ask for one
|
||||
# machine state, and block. There is no init.d directory any more and nothing
|
||||
# runs programs in sequence; the boot order is the compiled s6-rc graph. This
|
||||
# is also the only script that starts the supervision tree -- over ssh, the
|
||||
# recovery is plain s6-rc.
|
||||
|
||||
MODDIR=/usr/data/anvil
|
||||
|
||||
@@ -29,27 +28,50 @@ mkdir -p /usr/data/logs
|
||||
exec >>/usr/data/logs/anvil-boot.log 2>&1
|
||||
echo "=== mod firmwareExe `date 2>/dev/null` ==="
|
||||
|
||||
# The library path and the interpreter, for this script and everything it
|
||||
# starts. See anvil-env.sh: app_startup.sh exports these on the boot path, so
|
||||
# inheriting them looks like it works right up until somebody re-runs a
|
||||
# service by hand.
|
||||
# The library path and the interpreter. app_startup.sh exports these on the
|
||||
# boot path, so inheriting them looks like it works right up until somebody
|
||||
# re-runs something by hand.
|
||||
if [ -f $MODDIR/anvil-env.sh ]; then
|
||||
. $MODDIR/anvil-env.sh
|
||||
else
|
||||
echo "!! no $MODDIR/anvil-env.sh -- the mod payload is not installed"
|
||||
fi
|
||||
|
||||
S6BIN=$MODDIR/bin
|
||||
# The s6 scandir: one live servicedir per service, laid down by s6-rc-init.
|
||||
SCANDIR=$MODDIR/etc/s6
|
||||
# The compiled database. `current` is a symlink bin/patch.sh points at the
|
||||
# stamped directory it built, so this path never changes when the database does.
|
||||
S6DB=$MODDIR/etc/s6-rc/compiled/current
|
||||
# The scanner outlives the redirection above, so it gets a file of its own. A
|
||||
# healthy s6-svscan says nothing; anything here is a fault.
|
||||
S6LOG=/usr/data/logs/s6.log
|
||||
|
||||
# MEASURED: s6-svscan execs s6-supervise BY NAME off PATH, and s6-svc -w execs
|
||||
# s6-svlisten the same way. Without $MODDIR/bin on PATH the scanner answers
|
||||
# `s6-svscanctl -a` perfectly and then fails every spawn with ENOENT for ever,
|
||||
# and s6-rc-init HANGS rather than failing. anvil-env.sh does this too; it is
|
||||
# repeated because that file is allowed to be missing, and "carries on" must
|
||||
# not mean "starts a tree that can never supervise anything".
|
||||
case ":$PATH:" in
|
||||
*":$S6BIN:"*) ;;
|
||||
*) PATH="$S6BIN:$PATH"; export PATH ;;
|
||||
esac
|
||||
|
||||
# Is the SCANNER alive? Asked by behaviour: `s6-svscanctl -a` is a rescan
|
||||
# request travelling down a FIFO opened non-blocking, so nothing reading the
|
||||
# other end means ENXIO and exit 100. A dead scanner leaves the FIFO on disk,
|
||||
# which is a distinction `ps | grep` cannot make.
|
||||
scanner_up() {
|
||||
[ -x "$S6BIN/s6-svscanctl" ] || return 1
|
||||
"$S6BIN/s6-svscanctl" -a "$SCANDIR" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# ---- something on the panel, immediately ---------------------------------
|
||||
# From here until HelixScreen paints, nothing owns the screen. That gap is
|
||||
# the whole boot on EVERY start, not just the first: the migration below
|
||||
# exits at its stamp and draws nothing. FlashForge's own splash cannot fill
|
||||
# it -- /usr/prog/start.img exists, but the only two lines that would display
|
||||
# it (both in app_startup.sh) are commented out, so nothing has shown it for
|
||||
# a long time.
|
||||
# FF_PYTHON and LD_LIBRARY_PATH come from anvil-env.sh, which is also what
|
||||
# every service this script goes on to run will source. Exporting them here
|
||||
# means they are inherited too, so the init.d scripts re-source a set that is
|
||||
# already correct; anvil-env.sh is written to make that a no-op.
|
||||
# Nothing owns the screen until HelixScreen paints, on every boot.
|
||||
# FlashForge's own splash cannot fill it -- /usr/prog/start.img exists, but the
|
||||
# only two lines that would display it, both in app_startup.sh, are commented
|
||||
# out. FF_PYTHON and LD_LIBRARY_PATH come from anvil-env.sh.
|
||||
splash() {
|
||||
[ "${MOD_SPLASH:-1}" = 1 ] || return 0
|
||||
[ -f $MODDIR/bin/ffscreen.py ] || return 0
|
||||
@@ -57,109 +79,141 @@ splash() {
|
||||
}
|
||||
splash --title "REFORGE" --status "STARTING THE PRINTER" --progress 0.02
|
||||
|
||||
# ---- services ------------------------------------------------------------
|
||||
# init.d-style scripts, run in filename order. One failing service must never
|
||||
# stop the rest: Klipper has to come up even if the web stack is broken.
|
||||
if [ -d $MODDIR/init.d ]; then
|
||||
for service in $MODDIR/init.d/S*; do
|
||||
[ -x "$service" ] || continue
|
||||
echo "--- `basename $service` ---"
|
||||
"$service" start || echo "`basename $service`: returned $?"
|
||||
# ---- the supervision tree ------------------------------------------------
|
||||
#
|
||||
# THE SCANNER MUST BE THIS SHELL'S OWN CHILD: `wait` only works on a process
|
||||
# the calling shell forked, and `wait "$SCANNER_PID"` is what holds the
|
||||
# foreground. That is why the tree is started here and not by some script this
|
||||
# one calls -- a child of a child cannot be waited on.
|
||||
#
|
||||
# NOT `exec s6-svscan`: the watchdog greps for the NAME, and busybox ash has no
|
||||
# `exec -a`. The shell also has to run s6-rc-init after the scanner is up.
|
||||
if scanner_up; then
|
||||
# A scanner we did not fork cannot be waited on, so it goes and comes back
|
||||
# under us -- the same "restart everything" the recovery below performs.
|
||||
echo "s6: a scanner is already running -- taking it down so this boot owns it"
|
||||
"$S6BIN/s6-svscanctl" -t "$SCANDIR" 2>/dev/null
|
||||
n=0
|
||||
while scanner_up && [ $n -lt 15 ]; do
|
||||
sleep 1
|
||||
n=$((n + 1))
|
||||
done
|
||||
fi
|
||||
if [ ! -x "$S6BIN/s6-svscan" ]; then
|
||||
echo "s6: !! no $S6BIN/s6-svscan -- this payload did not ship the supervisor"
|
||||
echo "s6: !! this printer has no services; ssh is up via the stock S50dropbear"
|
||||
# Exiting would have app_startup.sh re-exec us into the same wall in a loop.
|
||||
while true; do sleep 3600; done
|
||||
fi
|
||||
# The scandir ships empty and is named in the install manifest, so
|
||||
# run-append.sh's rmdir pass can legitimately remove it before the new tarball
|
||||
# lands. One mkdir deletes a class of "the scanner did not start, log says
|
||||
# ENOENT".
|
||||
mkdir -p "$SCANDIR"
|
||||
# exec, so SCANNER_PID is the scanner itself rather than a shell that started
|
||||
# one -- otherwise `wait` would return on a wrapper that had already gone.
|
||||
( exec "$S6BIN/s6-svscan" "$SCANDIR" >>"$S6LOG" 2>&1 ) &
|
||||
SCANNER_PID=$!
|
||||
|
||||
# ---- is the supervisor still there? --------------------------------------
|
||||
# S40s6 started s6-svscan a few lines above, backgrounded, and then returned
|
||||
# -- it has to, because the scanner never returns and these scripts run in the
|
||||
# foreground. Nothing supervises the supervisor: we are not PID 1, there is no
|
||||
# inittab we can reach (/etc is inside the read-only squashfs a stock flash
|
||||
# overwrites), and a `while true` respawn loop in this script would be one
|
||||
# more hand-rolled supervisor, which is the exact thing s6 is here to delete.
|
||||
#
|
||||
# So this is the whole answer, and it is deliberately the same shape as the
|
||||
# Klipper last-resort further down: check once, at the one moment something
|
||||
# has already had a chance to go wrong, and say so in the boot log. What it
|
||||
# actually catches is a scanner that exec'd and then died -- the
|
||||
# -D_FILE_OFFSET_BITS=64 build trap in tools/supervisor/README.md, where
|
||||
# readdir() returns EOVERFLOW and the scanner starts fine and then does
|
||||
# nothing. S40s6 already waits and warns; this catches the case where it died
|
||||
# in the seconds AFTER that, while wifi and nginx were starting.
|
||||
#
|
||||
# Deliberately NOT a loop and NOT a watchdog. If the scanner dies twice the
|
||||
# printer has a real fault and a respawn would only hide it -- and note that a
|
||||
# dead scanner leaves the s6-supervise processes it started running, still
|
||||
# respawning their own services, so it is a printer that has stopped noticing
|
||||
# changes to the scandir rather than a printer that has stopped printing.
|
||||
# Today, with an empty scandir, it is not even that. See S40s6's header.
|
||||
if [ "${MOD_S6:-1}" = 1 ] && [ -x $MODDIR/init.d/S40s6 ]; then
|
||||
if ! $MODDIR/init.d/S40s6 status | grep -q scanning; then
|
||||
echo "s6: the scanner is not answering -- one restart, then we leave it"
|
||||
$MODDIR/init.d/S40s6 start
|
||||
fi
|
||||
# The wait is for a thing that takes no time -- a mkdir, a mkfifo and two
|
||||
# opens, no hardware involved. What it buys is that a scanner which exec'd and
|
||||
# then DIED is reported here: that is the -D_FILE_OFFSET_BITS=64 trap in
|
||||
# tools/supervisor/README.md, where readdir() returns EOVERFLOW and the scanner
|
||||
# starts fine and then does nothing. The 5s is only paid on a broken printer.
|
||||
n=0
|
||||
while ! scanner_up && [ $n -lt 5 ]; do
|
||||
sleep 1
|
||||
n=$((n + 1))
|
||||
done
|
||||
if ! scanner_up; then
|
||||
echo "s6: !! the scanner is not answering after ${n}s -- see $S6LOG"
|
||||
echo "s6: !! this printer has no services; ssh is up via the stock S50dropbear"
|
||||
while true; do sleep 3600; done
|
||||
fi
|
||||
echo "s6: scanning $SCANDIR (pid $SCANNER_PID)"
|
||||
|
||||
# Once per boot: s6-rc-init creates one symlink per service in the scandir and
|
||||
# fails outright with "File exists" if a name is taken -- which is why
|
||||
# run-append.sh sweeps $MODDIR/etc/s6 on upgrade. /run is a fresh tmpfs on a
|
||||
# normal boot, so the check below is for the printer that got here some other
|
||||
# way. `s6-rc -a list` needs the live directory, so it answers the question by
|
||||
# behaviour.
|
||||
if "$S6BIN/s6-rc" -a list >/dev/null 2>&1; then
|
||||
echo "s6-rc: already initialised -- leaving the live state alone"
|
||||
elif [ ! -d "$S6DB" ]; then
|
||||
echo "s6-rc: !! no compiled database at $S6DB -- this printer has no services"
|
||||
while true; do sleep 3600; done
|
||||
elif ! "$S6BIN/s6-rc-init" -c "$S6DB" "$SCANDIR"; then
|
||||
echo "s6-rc: !! could not initialise -- this printer has no services"
|
||||
while true; do sleep 3600; done
|
||||
fi
|
||||
|
||||
# ---- wait for the printer, and migrate on the first boot -----------------
|
||||
# Two jobs, and the panel narrates both (bin/ff-startup.py):
|
||||
# ---- one transition, and it is the whole boot ----------------------------
|
||||
#
|
||||
# every boot wait for the toolhead boards to leave their bootloaders and
|
||||
# for klipper and moonraker to be ready, naming whichever one
|
||||
# is holding things up. Starting HelixScreen before that is
|
||||
# what produces a UI reporting a disconnected printer with no
|
||||
# clue as to which board is missing.
|
||||
# first boot move this unit's factory calibration into printer.cfg. That
|
||||
# runs HERE, ahead of the UI, because the save restarts klippy
|
||||
# and doing that behind a HelixScreen already showing a
|
||||
# printer is worse than making the splash wait.
|
||||
# mcu-bringup -> klipper -> ff-startup -> ui
|
||||
# ^
|
||||
# moonraker
|
||||
#
|
||||
# MOD_IMPORT=0 keeps the wait and skips the migration.
|
||||
if [ "${MOD_STARTUP:-1}" = 1 ] && [ -f $MODDIR/bin/ff-startup.py ]; then
|
||||
STARTUP_ARGS="--timeout ${MOD_STARTUP_TIMEOUT:-240}"
|
||||
STARTUP_ARGS="$STARTUP_ARGS --klipper-tries ${MOD_KLIPPER_TRIES:-6}"
|
||||
[ "${MOD_IMPORT:-1}" = 1 ] || STARTUP_ARGS="$STARTUP_ARGS --no-import"
|
||||
"$FF_PYTHON" $MODDIR/bin/ff-startup.py $STARTUP_ARGS \
|
||||
|| echo "ff-startup: returned $? -- the next boot will try again"
|
||||
# A transition carries on past a service it could not start, and only what
|
||||
# DEPENDS on that service is skipped.
|
||||
#
|
||||
# THE BOOT SET is the ok-all bundle minus what anvil.conf switched off. The
|
||||
# bundle is read out of the compiled database rather than written here, so
|
||||
# there is one statement of what a default printer runs; the switches are read
|
||||
# at runtime, because a database built on a build host cannot read a file the
|
||||
# user edits over ssh. THIS IS THE ONLY PLACE THE MOD_* SERVICE GATES EXIST.
|
||||
# `contents`, not `list`: `list` takes a class of service.
|
||||
#
|
||||
# mcu-bringup is deliberately not in the bundle -- it arrives as klipper's
|
||||
# dependency, which is the point of having one.
|
||||
BOOT_SET=""
|
||||
for name in `"$S6BIN/s6-rc-db" -c "$S6DB" contents ok-all 2>/dev/null`; do
|
||||
case "$name" in
|
||||
nginx|moonraker) [ "${MOD_WEB:-1}" = 1 ] || continue ;;
|
||||
camera) [ "${MOD_CAM:-1}" = 1 ] || continue ;;
|
||||
wifi) [ "${MOD_WIFI:-1}" = 1 ] || continue ;;
|
||||
ui) [ "${MOD_UI:-1}" = 1 ] || continue ;;
|
||||
esac
|
||||
BOOT_SET="$BOOT_SET $name"
|
||||
done
|
||||
if [ -z "$BOOT_SET" ]; then
|
||||
# No database, or a renamed bundle. Asking for it whole beats a printer
|
||||
# with no services and an honoured MOD_CAM.
|
||||
echo "s6-rc: !! cannot read the ok-all bundle from $S6DB -- asking for it whole"
|
||||
BOOT_SET=ok-all
|
||||
fi
|
||||
|
||||
# Last resort. S70klipper stood aside for ff-startup, so if that program did
|
||||
# not run at all -- a broken interpreter, a truncated file, a python
|
||||
# traceback before it got as far as start.sh -- nothing has started klipper
|
||||
# and the printer would come up with a screen and no motion or heaters. This
|
||||
# is the one case where the shell has to take the job back.
|
||||
if ! $MODDIR/init.d/S70klipper status | grep -q running; then
|
||||
echo "klipper: nothing started it -- falling back to S70klipper"
|
||||
$MODDIR/init.d/S70klipper force-start
|
||||
# THIS BLOCKS FOR THE WHOLE BOOT, on purpose: moonraker and the camera are
|
||||
# waited for until READY, and ff-startup is a oneshot that waits for the boards
|
||||
# and for klippy to answer. ff-startup.py draws on /dev/fb0 throughout.
|
||||
#
|
||||
# `-u change` is ADDITIVE, not `-p` (prune) -- it brings these and their
|
||||
# dependencies up and leaves everything else alone.
|
||||
echo "s6-rc: bringing up:$BOOT_SET"
|
||||
"$S6BIN/s6-rc" -u change $BOOT_SET \
|
||||
|| echo "s6-rc: !! at least one service did not come up (see above)"
|
||||
|
||||
# The one thing a service that is not running cannot do: say so on the panel.
|
||||
if [ "${MOD_UI:-1}" != 1 ]; then
|
||||
splash --title "REFORGE" \
|
||||
--status "NO SCREEN" \
|
||||
--detail "MAINSAIL AND SSH ARE RUNNING. SET MOD_UI=1 IN ANVIL.CONF TO TURN THE SCREEN ON."
|
||||
echo "ui: MOD_UI=0 -- headless on purpose"
|
||||
fi
|
||||
|
||||
# ---- the UI, in the foreground so the stock watchdog can see us ----------
|
||||
# S80ui already decided and wrote its verdict here: "helix" to start the UI,
|
||||
# "none" to stay headless (MOD_UI=0, or HelixScreen not installed).
|
||||
UI_CHOICE=`cat $MODDIR/.ui-choice 2>/dev/null || echo none`
|
||||
echo "ui: choice is $UI_CHOICE"
|
||||
|
||||
if [ "$UI_CHOICE" = helix ]; then
|
||||
splash --title "REFORGE" --status "STARTING THE SCREEN" --progress 0.9
|
||||
HELIX_DATA_DIR=$MODDIR/helixscreen
|
||||
HELIX_LOG_DEST=file
|
||||
HELIX_LOG_FILE=/usr/data/logs/helixscreen.log
|
||||
# This repo pins and patches the HelixScreen build it ships; an in-app
|
||||
# update would replace it behind the package's back. HelixScreen documents
|
||||
# this variable as the vendor opt-out: it suppresses the update check, the
|
||||
# notify modal and the settings-panel update UI.
|
||||
HELIX_DISABLE_AUTO_UPDATES=1
|
||||
export HELIX_DATA_DIR HELIX_LOG_DEST HELIX_LOG_FILE HELIX_DISABLE_AUTO_UPDATES
|
||||
if [ -x $MODDIR/helixscreen/bin/helix-launcher.sh ]; then
|
||||
$MODDIR/helixscreen/bin/helix-launcher.sh
|
||||
else
|
||||
$MODDIR/helixscreen/bin/helix-screen
|
||||
fi
|
||||
fi
|
||||
|
||||
# If the UI exits -- or was never started at all -- we do NOT exit: dropping
|
||||
# out of this script makes app_startup.sh think the UI died and re-exec us,
|
||||
# which would restart every service. Hold the process instead; ssh and
|
||||
# Mainsail remain available.
|
||||
splash --title "REFORGE" \
|
||||
--status "NO SCREEN" \
|
||||
--detail "MAINSAIL AND SSH ARE RUNNING. SET MOD_UI=1 IN ANVIL.CONF TO TURN THE SCREEN ON."
|
||||
echo "ui: nothing left in the foreground -- holding process so services stay up"
|
||||
while true; do sleep 3600; done
|
||||
# ---- the foreground, and the recovery path -------------------------------
|
||||
#
|
||||
# This is what satisfies app_startup.sh's watchdog: still here, under this
|
||||
# name, blocked on something real.
|
||||
#
|
||||
# When the scanner dies, `wait` returns, this script exits, and app_startup.sh
|
||||
# re-execs it -- bringing the whole tree back. It is one restart of EVERYTHING,
|
||||
# not a targeted one: klippy goes down with it, mid-print. A scanner that dies
|
||||
# twice has a real fault and this looks like a boot loop, which is what a boot
|
||||
# loop is for.
|
||||
#
|
||||
# `wait "$SCANNER_PID"`, not a bare `wait`: services leave background children
|
||||
# around and a bare `wait` would return on the wrong one.
|
||||
echo "s6: holding the foreground on the scanner (pid $SCANNER_PID)"
|
||||
wait "$SCANNER_PID"
|
||||
echo "s6: !! the scanner exited -- leaving, so app_startup.sh restarts everything"
|
||||
@@ -1,229 +0,0 @@
|
||||
#!/bin/sh
|
||||
# s6-svscan -- the process supervisor, running and supervising nothing.
|
||||
#
|
||||
# WHY THIS EXISTS. Everything the other six scripts do by hand -- "is it still
|
||||
# there", "stop it and wait until it really is gone", "start it again when it
|
||||
# dies" -- is what a supervisor does in C, and anvil-service.sh has already
|
||||
# had two real bugs in exactly that code (busybox start-stop-daemon -K
|
||||
# returning before the process is dead; a stale socket making a dead klippy
|
||||
# report itself running). s6 was measured against runit on the replica and won
|
||||
# on the one thing that matters here, readiness notification: a service can
|
||||
# declare itself ready and s6-svwait -U blocks until it does, which is what
|
||||
# S65camera's 30-second poll of /dev/video0 and S70klipper's MCU handshake
|
||||
# retry are both hand-rolling. See tools/supervisor/README.md for the
|
||||
# measurements and docs/notes/80-s6-migration.md for the plan.
|
||||
#
|
||||
# THIS SCRIPT MIGRATES NOTHING. The scandir it scans is empty, on purpose.
|
||||
# All this phase buys is that the scanner is alive on every boot before any
|
||||
# service could want it, so that moving services into it later is one small
|
||||
# change per service rather than one big one. A scanner with an empty scandir
|
||||
# costs a single ~30KB process and does nothing at all -- measured on the
|
||||
# replica: it starts clean, prints not one line, and answers its control
|
||||
# socket. "Supervising nothing" is a supported state, not a degenerate one.
|
||||
#
|
||||
# THE NUMBERING IS LOAD-BEARING, the same way it is for S60nginx. firmwareExe
|
||||
# runs $MODDIR/init.d/S* in filename order, so 40 puts the scanner before
|
||||
# S50wifi and therefore before every service that will later be supervised by
|
||||
# it -- a service coming up and asking for a scanner that does not exist yet
|
||||
# is the one ordering mistake this file exists to prevent. It is not S00 or
|
||||
# S10 because those numbers are worth keeping free for something that has to
|
||||
# run BEFORE the supervisor (a filesystem check, a migration); nothing below
|
||||
# 40 exists today and this script has no reason to claim the whole range.
|
||||
#
|
||||
# IT MUST NOT BLOCK THE BOOT, and this is the sharp edge. s6-svscan NEVER
|
||||
# RETURNS -- that is its entire job. firmwareExe runs these scripts one at a
|
||||
# time in the FOREGROUND, so running the scanner inline does not delay the
|
||||
# boot, it ENDS it: no wifi, no ssh, no Mainsail, no Klipper and no screen,
|
||||
# for ever, on every boot, with no way in to find out why. So it is
|
||||
# backgrounded through svc_detach -- exactly how S65camera backgrounds the
|
||||
# respawn loop it hand-rolls for mjpg_streamer -- and start() returns at once.
|
||||
#
|
||||
# WHAT RESTARTS THE SCANNER IF IT DIES? Nothing, and that is a decision, not
|
||||
# an oversight. We are not PID 1 and there is no supervisor above this one; a
|
||||
# watchdog for it would be one more hand-rolled respawn loop in ash, which is
|
||||
# the exact thing we are buying s6 to delete. What exists instead is
|
||||
# proportionate to how it can actually fail:
|
||||
#
|
||||
# * It is one small C program that allocates nothing after startup and
|
||||
# blocks in poll(). The realistic way it "dies" is not a crash but a bad
|
||||
# BUILD -- the -D_FILE_OFFSET_BITS=64 trap in tools/supervisor/README.md,
|
||||
# where it execs fine and then dies on readdir() with EOVERFLOW. A
|
||||
# watchdog would respawn that forever and change nothing. start() below
|
||||
# therefore waits for the scanner to answer and SAYS SO when it does not,
|
||||
# which is the only response that helps.
|
||||
# * If it does die, the s6-supervise processes it started keep running and
|
||||
# keep respawning their own services. A dead scanner is a printer that has
|
||||
# stopped noticing changes to the scandir, not a printer that has stopped
|
||||
# printing. Today, with an empty scandir, it is not even that.
|
||||
# * firmwareExe re-checks it once after the init loop and restarts it there
|
||||
# if it is gone -- the same last-resort shape it already uses for Klipper,
|
||||
# and for the same reason: the check is cheap, it runs at the one moment
|
||||
# something else has already gone wrong, and the log line it prints is the
|
||||
# thing you would otherwise have had to guess.
|
||||
#
|
||||
# So: no watchdog, one re-check on the boot path, and a loud start(). If that
|
||||
# ever proves too little, the honest fix is an inittab-style respawn, not a
|
||||
# `while true` in a shell script.
|
||||
MODDIR=/usr/data/anvil
|
||||
[ -f $MODDIR/anvil.conf ] && . $MODDIR/anvil.conf
|
||||
# The library path and the interpreter, from the one file that defines them.
|
||||
# s6 itself needs nothing from it -- it is static-pie musl and links against
|
||||
# no shared object on this box, which is exactly why it was built that way --
|
||||
# but a script that gets re-run over ssh should get the same environment on
|
||||
# every path, and sourcing it twice is a no-op.
|
||||
if [ -f $MODDIR/anvil-env.sh ]; then
|
||||
. $MODDIR/anvil-env.sh
|
||||
else
|
||||
echo "s6: !! no $MODDIR/anvil-env.sh -- the mod payload is not installed"
|
||||
fi
|
||||
# start/stop/status/liveness, shared with the other services. See
|
||||
# anvil-service.sh for the whole API -- including the svc_s6_* helpers, which
|
||||
# are only usable because this script is what puts a scanner behind them.
|
||||
if [ -f $MODDIR/anvil-service.sh ]; then
|
||||
. $MODDIR/anvil-service.sh
|
||||
else
|
||||
echo "s6: !! no $MODDIR/anvil-service.sh -- the mod payload is not installed"
|
||||
exit 1
|
||||
fi
|
||||
SVC_NAME=s6
|
||||
|
||||
# SVC_S6_BIN and SVC_S6_SCANDIR come from anvil-service.sh: every script that
|
||||
# talks to the scanner has to agree with this one about where it is, and the
|
||||
# way to make that true is to name it once, in the file they all source.
|
||||
SVSCAN=$SVC_S6_BIN/s6-svscan
|
||||
SCANDIR=$SVC_S6_SCANDIR
|
||||
# The pidfile holds the SCANNER, not a supervisor: stop() has to be able to
|
||||
# take down the one process that would otherwise put everything back. Same
|
||||
# reasoning as the pidfile in S65camera, which holds the respawn loop rather
|
||||
# than the streamer it respawns.
|
||||
PIDFILE=$MODDIR/s6-svscan.pid
|
||||
# The scanner's own output, in a file of its own. A healthy s6-svscan says
|
||||
# NOTHING -- verified on the replica, where three seconds on an empty scandir
|
||||
# produced an empty log -- so anything landing here is a fault worth finding
|
||||
# on its own. It is not left on the inherited descriptor because the scanner
|
||||
# OUTLIVES firmwareExe's redirection: it would otherwise keep writing into the
|
||||
# boot log of a boot that finished hours ago.
|
||||
LOG=/usr/data/logs/s6.log
|
||||
|
||||
scanner() {
|
||||
mkdir -p /usr/data/logs
|
||||
# exec, so the pid svc_detach captured IS the scanner's pid rather than
|
||||
# that of a shell which happens to have started one. svc_stop_daemon
|
||||
# signals whatever is in the pidfile, and signalling a wrapper that has
|
||||
# already gone would leave the scanner running with nothing pointing at it.
|
||||
exec "$SVSCAN" "$SCANDIR" >>"$LOG" 2>&1
|
||||
}
|
||||
|
||||
start() {
|
||||
[ "${MOD_S6:-1}" = "1" ] || { svc_say "disabled in anvil.conf (MOD_S6=0)"; return 0; }
|
||||
if [ ! -x "$SVSCAN" ]; then
|
||||
svc_warn "no $SVSCAN -- this payload did not ship the supervisor"
|
||||
return 1
|
||||
fi
|
||||
if svc_s6_running; then
|
||||
svc_say "already scanning $SCANDIR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# WHY THE mkdir IS HERE AS WELL AS IN THE PAYLOAD. bin/patch.sh stages
|
||||
# etc/s6/ into the payload, so the directory ships, lands on a real
|
||||
# install, and -- the reason for staging it rather than only creating it
|
||||
# here -- is NAMED IN THE INSTALL MANIFEST, which is what lets a later
|
||||
# update account for it instead of orphaning it. But it ships EMPTY, and
|
||||
# an empty directory is precisely the kind of payload path that other
|
||||
# things legitimately remove: run-append.sh's manifest pass rmdir's every
|
||||
# empty directory the PREVIOUS payload listed, and it does that before the
|
||||
# new tarball is extracted. So there is a window; and there are printers
|
||||
# somebody installed by hand. One mkdir -p costs nothing and deletes a
|
||||
# whole class of "the scanner did not start and the log says ENOENT".
|
||||
mkdir -p "$SCANDIR" || { svc_warn "cannot create $SCANDIR"; return 1; }
|
||||
|
||||
# svc_detach leaves the scanner's pid in SVC_BG_PID; it is written to the
|
||||
# pidfile because the scanner is the half of this that has to be killable.
|
||||
svc_detach scanner
|
||||
echo "$SVC_BG_PID" > "$PIDFILE"
|
||||
|
||||
# Then wait for it to answer -- but only for a thing that takes no time.
|
||||
# s6-svscan is up once it has created $SCANDIR/.s6-svscan/, taken the lock
|
||||
# and started reading the control FIFO: a mkdir, a mkfifo and two opens.
|
||||
# It waits on no hardware, no network and no other service, so the check
|
||||
# below almost always succeeds on its first pass and costs nothing. What
|
||||
# it buys is that the "scanning" line is TRUE when it is printed, and that
|
||||
# a scanner which exec'd and then died -- the EOVERFLOW build trap above
|
||||
# -- is reported here, by name, instead of silently three phases from now
|
||||
# when a service is moved into the scandir and never comes up. The 5s
|
||||
# bound is only ever paid on a printer that is already broken.
|
||||
_s6_waited=0
|
||||
while ! svc_s6_running && [ $_s6_waited -lt 5 ]; do
|
||||
sleep 1
|
||||
_s6_waited=$((_s6_waited + 1))
|
||||
done
|
||||
if svc_s6_running; then
|
||||
svc_say "scanning $SCANDIR (pid $SVC_BG_PID)"
|
||||
unset _s6_waited
|
||||
return 0
|
||||
fi
|
||||
svc_warn "started but is not answering on $SCANDIR after ${_s6_waited}s -- see $LOG"
|
||||
unset _s6_waited
|
||||
return 1
|
||||
}
|
||||
|
||||
stop() {
|
||||
# -t is the documented way down, and the only one that keeps the promise
|
||||
# "stop leaves no s6 processes behind": it tells the scanner to send TERM
|
||||
# to every s6-supervise it started, reap them, and then exit itself.
|
||||
# Killing the scanner on its own would leave those supervisors orphaned
|
||||
# and still respawning their services -- the same mistake as killing
|
||||
# mjpg_streamer without its respawn loop, which S65camera's stop() is
|
||||
# written the way it is to avoid. It costs nothing today, with nothing to
|
||||
# supervise, and it is the line that has to already be right on the day
|
||||
# phase 4 puts the first service in there.
|
||||
if [ -x "$SVC_S6_BIN/s6-svscanctl" ] && svc_s6_running; then
|
||||
"$SVC_S6_BIN/s6-svscanctl" -t "$SCANDIR" 2>/dev/null
|
||||
_s6_waited=0
|
||||
while svc_s6_running && [ $_s6_waited -lt 15 ]; do
|
||||
sleep 1
|
||||
_s6_waited=$((_s6_waited + 1))
|
||||
done
|
||||
svc_s6_running && svc_warn "still answering after ${_s6_waited}s"
|
||||
unset _s6_waited
|
||||
fi
|
||||
# Then the pidfile, which does two things -t does not. It REMOVES the file
|
||||
# -- svc_pid_alive on a recycled pid is how `status` once came to report an
|
||||
# unrelated process as a running service -- and it is the way out when the
|
||||
# scanner is wedged badly enough not to read its own control socket: a
|
||||
# plain TERM to the pid exits it too, verified on the replica.
|
||||
svc_stop_daemon "$PIDFILE"
|
||||
svc_say "stopped"
|
||||
}
|
||||
|
||||
status() {
|
||||
# Asked of the SCANNER, not of the process table. `s6-svscanctl -a` is a
|
||||
# request to rescan the scandir -- harmless, idempotent, and it travels
|
||||
# down the control FIFO, so it can only succeed if something on the other
|
||||
# end is reading. A dead scanner leaves that FIFO on disk and the ping
|
||||
# then fails with "supervisor not listening" (exit 100), which is exactly
|
||||
# the distinction a `ps | grep` cannot make and the distinction that made
|
||||
# a stale pidfile lie about moonraker.
|
||||
if svc_s6_running; then
|
||||
svc_say "scanning $SCANDIR"
|
||||
else
|
||||
svc_say "not running"
|
||||
fi
|
||||
# And what it is supervising, if anything. Zero is correct today and stays
|
||||
# correct until phase 4 puts the first service directory in there; saying
|
||||
# the number rather than nothing is what makes that change visible the day
|
||||
# it happens. The glob skips .s6-svscan/ -- the scanner's own control
|
||||
# directory is hidden, which is why it is not a service here.
|
||||
_s6_n=0
|
||||
for _s6_d in "$SCANDIR"/*; do
|
||||
[ -d "$_s6_d" ] || continue
|
||||
_s6_n=$((_s6_n + 1))
|
||||
_s6_name=`basename "$_s6_d"`
|
||||
svc_say " $_s6_name: `svc_s6_stat "$_s6_name" 2>&1`"
|
||||
done
|
||||
svc_say "$_s6_n service directories in $SCANDIR"
|
||||
unset _s6_n _s6_d _s6_name
|
||||
}
|
||||
|
||||
svc_dispatch "$@"
|
||||
@@ -1,147 +0,0 @@
|
||||
#!/bin/sh
|
||||
# WiFi.
|
||||
#
|
||||
# WHY THIS EXISTS -- the same trap as Klipper, and for the same reason:
|
||||
# nothing in /etc/init.d brings up wlan0. FlashForge's firmwareExe owns the
|
||||
# whole network stack. `strings firmwareExe` shows it running "ifconfig wlan0
|
||||
# up", driving wpa_supplicant/wpa_cli against /usr/prog/wifi/wpa_supplicant.conf
|
||||
# and then "udhcpc -i wlan0". app_startup.sh only insmods the 8821cu driver;
|
||||
# it never configures the interface.
|
||||
#
|
||||
# So the moment HelixScreen replaces firmwareExe the printer boots with NO
|
||||
# network at all: no ssh, no Mainsail, no moonraker. Which also means no way
|
||||
# to diagnose it. Hence this runs before S60nginx/S62moonraker and S70klipper.
|
||||
#
|
||||
# BUT IT MUST NOT MAKE THEM WAIT. firmwareExe runs the S* scripts one at a
|
||||
# time in the foreground. Everything interesting here is a wait on hardware:
|
||||
# up to 15s for wlan0 to register after the 8821cu insmod, then up to 20s more
|
||||
# for the AP to associate. The two waits are sequential, so inline that is up
|
||||
# to ~35s with no Klipper, no Mainsail and
|
||||
# no UI whenever the AP is out of range or the password is stale -- i.e. the
|
||||
# longest delay lands on exactly the machines with no network, where you most
|
||||
# need ssh to diagnose and where waiting buys nothing. So the waiting happens
|
||||
# in a background subshell and start() returns at once: the rule firmwareExe
|
||||
# already states for a service that fails, applied to one that is merely slow.
|
||||
# The trade is that "wifi:" lines land in the boot log after the later
|
||||
# services' lines, out of order. That rule is svc_detach now -- see
|
||||
# anvil-service.sh, where it is stated once for all five scripts.
|
||||
#
|
||||
# The saved networks live in /usr/prog/wifi/wpa_supplicant.conf on the
|
||||
# firmware partition -- we never ship or touch that file, so whatever
|
||||
# FlashForge's UI saved there before the mod is still there. It sets ctrl_interface=DIR=/var/run/wpa_supplicant,
|
||||
# which is also one of the paths HelixScreen probes for a control socket, so
|
||||
# its WiFi panel drives this very daemon (and update_config=1 lets it save new
|
||||
# networks back).
|
||||
MODDIR=/usr/data/anvil
|
||||
[ -f $MODDIR/anvil.conf ] && . $MODDIR/anvil.conf
|
||||
# start/stop/status/liveness, shared with the other services. See
|
||||
# anvil-service.sh for the whole API.
|
||||
if [ -f $MODDIR/anvil-service.sh ]; then
|
||||
. $MODDIR/anvil-service.sh
|
||||
else
|
||||
echo "wifi: !! no $MODDIR/anvil-service.sh -- the mod payload is not installed"
|
||||
exit 1
|
||||
fi
|
||||
SVC_NAME=wifi
|
||||
|
||||
IFACE=wlan0
|
||||
WIFI_CONF=/usr/prog/wifi/wpa_supplicant.conf
|
||||
CTRL_DIR=/var/run/wpa_supplicant
|
||||
|
||||
# wpa_supplicant backgrounds itself with -B, so there is no pid we own and no
|
||||
# pidfile to keep it in: by name is the only answer available. svc_proc_alive
|
||||
# is the same `ps | grep | grep -v grep` this script always did, with pgrep
|
||||
# preferred where the rootfs ships it.
|
||||
supplicant_running() {
|
||||
svc_proc_alive wpa_supplicant
|
||||
}
|
||||
|
||||
# The part that waits. Detached from the boot sequence, so nothing reads its
|
||||
# exit code -- it reports through the boot log only.
|
||||
connect() {
|
||||
# app_startup.sh insmods 8821cu just before us, but the interface takes a
|
||||
# moment to register.
|
||||
waited=0
|
||||
while [ $waited -lt 15 ]; do
|
||||
[ -d /sys/class/net/$IFACE ] && break
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
if [ ! -d /sys/class/net/$IFACE ]; then
|
||||
svc_warn "$IFACE never appeared -- is 8821cu.ko loaded?"
|
||||
return 1
|
||||
fi
|
||||
|
||||
/sbin/ifconfig $IFACE up 2>/dev/null
|
||||
mkdir -p $CTRL_DIR
|
||||
|
||||
# -B backgrounds it. Try nl80211 first and fall back to wext: the 8821cu
|
||||
# is a Realtek USB part and which one it registers varies by build.
|
||||
if /usr/sbin/wpa_supplicant -B -i $IFACE -c "$WIFI_CONF" -D nl80211,wext; then
|
||||
svc_say "wpa_supplicant started"
|
||||
else
|
||||
svc_warn "wpa_supplicant failed to start"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Give association a chance before asking for a lease.
|
||||
waited=0
|
||||
while [ $waited -lt 20 ]; do
|
||||
/usr/sbin/wpa_cli -i $IFACE status 2>/dev/null | grep -q '^wpa_state=COMPLETED' && break
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
/sbin/udhcpc -i $IFACE -b -t 5 >/dev/null 2>&1 &
|
||||
svc_say "udhcpc requesting a lease on $IFACE"
|
||||
}
|
||||
|
||||
start() {
|
||||
if [ "${MOD_WIFI:-1}" != "1" ]; then
|
||||
svc_say "disabled by MOD_WIFI"
|
||||
return 0
|
||||
fi
|
||||
if supplicant_running; then
|
||||
svc_say "wpa_supplicant already running"
|
||||
return 0
|
||||
fi
|
||||
[ -f "$WIFI_CONF" ] || { svc_warn "no $WIFI_CONF -- nothing to connect to"; return 1; }
|
||||
|
||||
# Everything above was instant. Everything below waits on hardware, so it
|
||||
# goes to the background and the boot carries on to S60nginx, S62moonraker
|
||||
# and S70klipper.
|
||||
svc_detach connect
|
||||
svc_say "bringing up $IFACE in the background"
|
||||
return 0
|
||||
}
|
||||
|
||||
stop() {
|
||||
# By name, not by pidfile: nothing here was started with one, and inventing
|
||||
# one for a daemon that backgrounds itself would only be a second thing
|
||||
# that can be wrong. wpa_cli first, then the lease, then the daemon that
|
||||
# owns the association.
|
||||
killall wpa_cli 2>/dev/null
|
||||
killall udhcpc 2>/dev/null
|
||||
killall wpa_supplicant 2>/dev/null
|
||||
# Then WAIT for the supplicant to actually be gone. This is the thing the
|
||||
# `sleep 2` in front of the old `restart` was standing in for: killall
|
||||
# returns as soon as the signal is sent, so a start() running a moment
|
||||
# later finds the dying supplicant still in the process table, says
|
||||
# "already running", and does nothing -- leaving the printer stopped
|
||||
# rather than restarted. Same bound and same reasoning as svc_stop_daemon.
|
||||
waited=0
|
||||
while supplicant_running && [ $waited -lt 15 ]; do
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
supplicant_running && svc_warn "wpa_supplicant still running after ${waited}s"
|
||||
svc_say "stopped"
|
||||
}
|
||||
|
||||
status() {
|
||||
supplicant_running && svc_say "wpa_supplicant running" || svc_say "not running"
|
||||
# Raw ifconfig output, deliberately unprefixed: it is the address you came
|
||||
# here to read, in the shape you would have typed the command to get.
|
||||
/sbin/ifconfig $IFACE 2>/dev/null | grep -E 'inet |UP'
|
||||
}
|
||||
|
||||
svc_dispatch "$@"
|
||||
@@ -1,298 +0,0 @@
|
||||
#!/bin/sh
|
||||
# nginx -- the Mainsail web UI on :80.
|
||||
#
|
||||
# The binary already ships on stock firmware -- FlashForge simply comments it
|
||||
# out of /usr/prog/klipper/start.sh and serves its own HTTP API from firmwareExe
|
||||
# instead. The mod replaces firmwareExe, so that API is gone and this is the
|
||||
# only web server on the printer. Nothing is built or shipped for it beyond the
|
||||
# config and the Mainsail files under $MODDIR: we start the binary FlashForge
|
||||
# already put there, pointed at our own prefix so nothing writes to /usr/prog.
|
||||
#
|
||||
# THIS AND S62moonraker WERE ONE SCRIPT, S60web. They are separated because
|
||||
# they fail separately and are debugged separately: nginx not starting is a
|
||||
# missing config or a port already bound, and is instant; moonraker not
|
||||
# starting is a component that failed to import, and takes a background check
|
||||
# and a log tail to even notice. One script had to say "web: stopped" for both,
|
||||
# and `status` reported on two unrelated things at once. Splitting them means
|
||||
# `S62moonraker restart` over ssh -- which is the one you actually want, over
|
||||
# and over, while chasing a moonraker config -- no longer takes the UI down
|
||||
# with it.
|
||||
#
|
||||
# THE NUMBERING IS LOAD-BEARING. firmwareExe runs $MODDIR/init.d/S* in filename
|
||||
# order, so this runs before S62moonraker, which runs before S65camera. nginx
|
||||
# first is the useful order: it proxies both of the others (moonraker on 7125
|
||||
# and the camera's :8080 stream through /webcam/), and when it is up first the
|
||||
# browser gets a page that says the backend is not answering yet, rather than a
|
||||
# refused connection that says nothing at all. It also runs AFTER S40s6, which
|
||||
# is what starts the supervisor this script now hands the work to, and that
|
||||
# ordering is not a coincidence either -- see below.
|
||||
#
|
||||
# MOD_WEB=0 gates BOTH scripts, and does NOT fall back to anything: it leaves
|
||||
# the printer with ssh and the touchscreen only. It is a debugging switch, not
|
||||
# a way back to stock -- that is a stock FlashForge package. It stays one
|
||||
# switch across the two scripts because "no web stack" is one intention; there
|
||||
# is no useful printer with Mainsail and no moonraker behind it.
|
||||
#
|
||||
# WHAT CHANGED WHEN s6 ARRIVED, and what deliberately did not. nginx is now a
|
||||
# supervised service: $MODDIR/etc/s6/nginx/ holds a `run` script, s6-supervise
|
||||
# runs it, and when nginx dies s6 starts it again. That is the whole point --
|
||||
# before this, a master process that fell over stayed fallen until somebody
|
||||
# noticed the UI was gone and ssh'd in, and this script had no way to do
|
||||
# anything about it because a shell that has returned cannot restart anything.
|
||||
#
|
||||
# THE INTERFACE IS UNCHANGED, on purpose. `S60nginx restart` over ssh is in the
|
||||
# docs and in people's hands, and changing the supervisor is not a reason to
|
||||
# change what people type. So start/stop/restart/status all still exist, still
|
||||
# say "nginx: ...", and still mean what they meant; underneath, each of them is
|
||||
# now a couple of svc_s6_* calls instead of a start-stop-daemon invocation and
|
||||
# a pidfile. What that buys is the same thing the shared library was written
|
||||
# for: the waiting is done by C that s6 maintains rather than by a `while
|
||||
# kill -0 ...; sleep 1` we maintain, and `restart` cannot race its own start.
|
||||
#
|
||||
# WHERE THE MOD_WEB GATE LIVES NOW. In this file, exactly as before, and NOT in
|
||||
# the scandir -- $MODDIR/etc/s6/nginx/down ships in the payload precisely so
|
||||
# that the scanner never makes that decision. anvil.conf is read at runtime and
|
||||
# the payload is built once; the long version of that argument is written in
|
||||
# the `down` file itself, next to the thing it explains.
|
||||
MODDIR=/usr/data/anvil
|
||||
[ -f $MODDIR/anvil.conf ] && . $MODDIR/anvil.conf
|
||||
# The library path and the interpreter, from the one file that defines them.
|
||||
# See anvil-env.sh for why naming them here instead was a bug. nginx itself
|
||||
# needs nothing from it -- it links against the base rootfs -- but a script
|
||||
# that is re-run over ssh should get the same environment on every path, and
|
||||
# sourcing it twice is a no-op.
|
||||
if [ -f $MODDIR/anvil-env.sh ]; then
|
||||
. $MODDIR/anvil-env.sh
|
||||
else
|
||||
echo "nginx: !! no $MODDIR/anvil-env.sh -- the mod payload is not installed"
|
||||
fi
|
||||
# start/stop/status/liveness, shared with the other services. See
|
||||
# anvil-service.sh for the whole API -- including the svc_s6_* helpers, which
|
||||
# are what the three verbs below are built out of.
|
||||
if [ -f $MODDIR/anvil-service.sh ]; then
|
||||
. $MODDIR/anvil-service.sh
|
||||
else
|
||||
echo "nginx: !! no $MODDIR/anvil-service.sh -- the mod payload is not installed"
|
||||
exit 1
|
||||
fi
|
||||
SVC_NAME=nginx
|
||||
|
||||
NGINX=/usr/prog/nginx/sbin/nginx
|
||||
NGINX_PID=$MODDIR/nginx/logs/nginx.pid
|
||||
# The service directory the scanner watches, spelled out of the two variables
|
||||
# anvil-service.sh defines so that this script and S40s6 cannot disagree about
|
||||
# where the scandir is. The name is the service name, which is also SVC_NAME,
|
||||
# which is also what every line this script prints starts with; that is one
|
||||
# fewer thing to keep in step by hand.
|
||||
SVCDIR=$SVC_S6_SCANDIR/$SVC_NAME
|
||||
|
||||
# Is there a live s6-supervise for our service directory RIGHT NOW?
|
||||
#
|
||||
# Two separate questions hide in "is s6 there", and both have to be yes before
|
||||
# any of the verbs below can use it. svc_s6_running answers the first -- is the
|
||||
# SCANNER alive and listening -- and does it by behaviour, by pinging the
|
||||
# control FIFO. This answers the second: has that scanner actually picked our
|
||||
# directory up and started a supervisor for it? A scandir the scanner has not
|
||||
# read yet (the payload was extracted after S40s6 ran; somebody dropped the
|
||||
# directory in by hand) looks exactly like a scandir with no nginx in it, and
|
||||
# `s6-svc` against it fails with "supervisor not running" -- which is a true
|
||||
# statement about a state that fixes itself the moment the scanner rescans.
|
||||
#
|
||||
# s6-svok is that question in one syscall: it opens the supervise/control FIFO
|
||||
# and exits 0 only if a supervisor has it open at the other end. Same technique
|
||||
# as svc_s6_running, one level down, and the same reason for preferring it to a
|
||||
# test for the directory: a servicedir on disk with no supervisor behind it is
|
||||
# the stale-pidfile lie all over again.
|
||||
supervised() {
|
||||
[ -x "$SVC_S6_BIN/s6-svok" ] || return 1
|
||||
"$SVC_S6_BIN/s6-svok" "$SVCDIR"
|
||||
}
|
||||
|
||||
# Ask the scanner to pick up the service directory, then wait a moment for the
|
||||
# supervisor to appear. svc_s6_running IS the rescan request -- `s6-svscanctl
|
||||
# -a` is what it pings with -- so this is one call doing two jobs, which is
|
||||
# worth saying out loud because it does not look like it does. The bound is 5
|
||||
# seconds and is only ever paid on a printer where something is already wrong:
|
||||
# on a normal boot the payload was extracted long before S40s6 started the
|
||||
# scanner, so the supervisor is already there and the first test succeeds.
|
||||
# Anything that spends time here is spending firmwareExe's boot, one script at
|
||||
# a time in the foreground, which is why it is 5 and not 30.
|
||||
await_supervisor() {
|
||||
supervised && return 0
|
||||
svc_s6_running || return 1
|
||||
_ng_waited=0
|
||||
while ! supervised && [ $_ng_waited -lt 5 ]; do
|
||||
sleep 1
|
||||
_ng_waited=$((_ng_waited + 1))
|
||||
done
|
||||
unset _ng_waited
|
||||
supervised
|
||||
}
|
||||
|
||||
# ---- the fallback, and why it exists ---------------------------------------
|
||||
#
|
||||
# WHAT HAPPENS WHEN THERE IS NO SUPERVISOR. MOD_S6=0 is a switch S40s6 honours,
|
||||
# so "no scanner" is a state a user can choose; and a payload whose s6 failed
|
||||
# to build, or an install that got half way, is a state nobody chose but that
|
||||
# happens anyway. The choice is between refusing to start and starting nginx
|
||||
# the old way, and this script starts it the old way. The reasoning:
|
||||
#
|
||||
# * The failure this would otherwise produce is a printer with no web UI and
|
||||
# no way to be told why, because the way you would be told is a web UI.
|
||||
# ssh is the remaining door and it is the one MOD_WEB=0 leaves you; a
|
||||
# printer that loses Mainsail because a SUPERVISOR did not build has lost
|
||||
# it for a reason that has nothing to do with nginx.
|
||||
# * The old path is not hypothetical or unmaintained -- it is the code that
|
||||
# shipped for the whole life of this script, it is nine lines, and
|
||||
# case-moonraker.sh still exercises it, because that case starts nginx
|
||||
# without a scanner anywhere in sight.
|
||||
# * The classic reason NOT to fall back is that you end up with two of them:
|
||||
# a directly-started nginx, then a scanner arriving later and starting a
|
||||
# second one on a port that is already bound. That cannot happen here, and
|
||||
# it is the `down` file that makes it so -- the scanner never starts nginx
|
||||
# on its own, only this script does. And when this script does, the `run`
|
||||
# script sweeps any nginx that is already running before exec'ing its own,
|
||||
# so the hand-over from the fallback to the supervised copy is clean rather
|
||||
# than a fight over :80.
|
||||
#
|
||||
# What it is NOT allowed to be is silent. A printer running the fallback has
|
||||
# lost automatic restart-on-crash without losing the web UI, which is exactly
|
||||
# the kind of half-degraded state that gets discovered six months later, so it
|
||||
# says so every time, with "!!", which is what people grep a boot log for.
|
||||
start_unsupervised() {
|
||||
svc_warn "no supervisor -- starting nginx directly, it will NOT be restarted if it dies"
|
||||
# -p sets the prefix: logs, the pidfile and the temp directories all land
|
||||
# under $MODDIR on the data partition, never on /usr/prog. nginx will not
|
||||
# start if it cannot create them, and it does not create them itself.
|
||||
if $NGINX -p $MODDIR/nginx -c $MODDIR/nginx/nginx.conf; then
|
||||
svc_say "started (unsupervised)"
|
||||
return 0
|
||||
fi
|
||||
# nginx has already printed its own reason on stderr, which is in the
|
||||
# boot log right above this line. Do not paraphrase it.
|
||||
svc_warn "FAILED to start -- see the error above"
|
||||
return 1
|
||||
}
|
||||
|
||||
start() {
|
||||
[ "${MOD_WEB:-1}" = "1" ] || { svc_say "disabled in anvil.conf (MOD_WEB=0)"; return 0; }
|
||||
# The two prerequisites this script has always checked, in the same order
|
||||
# and with the same words, because they are the two things that are
|
||||
# actually missing when nginx does not start and neither of them is any
|
||||
# less true under a supervisor. A missing prerequisite is REPORTED and
|
||||
# returns non-zero; it is not a crash and it is not a stack trace out of
|
||||
# s6, which is what handing a supervisor a service it cannot run produces.
|
||||
if [ ! -x "$NGINX" ]; then
|
||||
svc_warn "no $NGINX -- this printer's firmware did not ship one"
|
||||
return 1
|
||||
fi
|
||||
if [ ! -f $MODDIR/nginx/nginx.conf ]; then
|
||||
svc_warn "no $MODDIR/nginx/nginx.conf -- the install did not complete"
|
||||
return 1
|
||||
fi
|
||||
mkdir -p $MODDIR/nginx/logs $MODDIR/nginx/tmp
|
||||
|
||||
# The third prerequisite, which is new: the service directory itself. It is
|
||||
# staged into the payload by bin/patch.sh and named in the install
|
||||
# manifest, so its absence means a payload that was built without it or an
|
||||
# install that did not finish -- and it is deliberately NOT created here.
|
||||
# Writing it at runtime would mean the scandir contained a directory no
|
||||
# manifest lists, which is the exact thing the manifest exists to stop, and
|
||||
# it would put the `run` script's text inside this file where nobody would
|
||||
# ever look at it again.
|
||||
if [ ! -x "$SVCDIR/run" ]; then
|
||||
svc_warn "no $SVCDIR/run -- this payload did not ship the s6 service"
|
||||
start_unsupervised
|
||||
return $?
|
||||
fi
|
||||
|
||||
if ! await_supervisor; then
|
||||
start_unsupervised
|
||||
return $?
|
||||
fi
|
||||
|
||||
# And up it goes. svc_s6_up is `s6-svc -wu -u` with a bound: it asks for the
|
||||
# service to be brought up -- overriding the `down` file, which is how the
|
||||
# MOD_WEB gate above turns into a running server -- and does not return
|
||||
# until it IS up. 10 rather than the default 15 seconds because nginx
|
||||
# coming up is a bind() and a fork() and nothing else: it is either up in
|
||||
# well under a second or it is not coming up at all, and every second spent
|
||||
# here is a second firmwareExe is not spending on moonraker, the camera and
|
||||
# Klipper.
|
||||
if svc_s6_up "$SVC_NAME" 10; then
|
||||
svc_say "started -- `svc_s6_stat "$SVC_NAME"`"
|
||||
return 0
|
||||
fi
|
||||
# Up it did not come. s6 has kept the reason where a reason belongs: the
|
||||
# `run` script's stderr is the scanner's stderr, which S40s6 points at
|
||||
# /usr/data/logs/s6.log, and nginx says precisely why it would not start
|
||||
# (an unreadable config, a port already bound) on stderr. Say where, rather
|
||||
# than paraphrasing something we have not read.
|
||||
svc_warn "FAILED to come up -- `svc_s6_stat "$SVC_NAME"` -- see /usr/data/logs/s6.log"
|
||||
return 1
|
||||
}
|
||||
|
||||
stop() {
|
||||
# A stop that the supervisor undoes is the classic mistake of this
|
||||
# migration, and `s6-svc -d` is what makes it not happen: it does not just
|
||||
# signal the process, it tells s6 the service is WANTED DOWN, and s6 then
|
||||
# does not start it again until something says otherwise. Killing the pid
|
||||
# instead -- which is all this script could do before, and all
|
||||
# svc_stop_daemon still does below -- would get nginx restarted a second
|
||||
# later by a supervisor that was doing exactly what it was asked to do.
|
||||
#
|
||||
# -wD (inside svc_s6_down) is the other half: it returns when the service
|
||||
# is really down, not when the signal has been sent. That is the busybox
|
||||
# start-stop-daemon correction from anvil-service.sh, except in C.
|
||||
if supervised; then
|
||||
svc_s6_down "$SVC_NAME" || svc_warn "still not down -- `svc_s6_stat "$SVC_NAME"`"
|
||||
# nginx writes logs/nginx.pid even with `daemon off;` -- measured on
|
||||
# the replica -- so a supervised run leaves one behind, and it is a pid
|
||||
# that the kernel is now free to hand to something else. The fallback
|
||||
# path below is the only thing that reads this file, and it is exactly
|
||||
# the stale-pidfile lie anvil-service.sh has two comments about, so it
|
||||
# goes here rather than being left for it to trip over.
|
||||
rm -f "$NGINX_PID"
|
||||
svc_say "stopped"
|
||||
return 0
|
||||
fi
|
||||
# No supervisor: whatever is running was started by start_unsupervised (or
|
||||
# by a hand-run nginx), so it is the pidfile that has to be signalled. The
|
||||
# wait matters here too: the master process takes a moment to reap its
|
||||
# workers, and returning before it has is how `restart` came back to a port
|
||||
# that was still bound.
|
||||
svc_stop_daemon "$NGINX_PID"
|
||||
svc_say "stopped"
|
||||
}
|
||||
|
||||
status() {
|
||||
# Asked of the supervisor when there is one, because it is the only thing
|
||||
# that knows. s6-svstat's own wording is passed through -- "up (pid 1234
|
||||
# pgid 1234) 71 seconds", "down (signal SIGTERM) 4 seconds, normally up" --
|
||||
# for the reason anvil-service.sh gives for svc_s6_stat: it is the answer
|
||||
# you came for, in the shape you would have typed the command to get.
|
||||
#
|
||||
# The words "running" and "not running" are still the first thing on the
|
||||
# line, though, and that is not decoration either. It is the vocabulary
|
||||
# every other service in init.d/ answers in, it is what a human skims a
|
||||
# boot log for, and it is what case-moonraker.sh looks for when it checks
|
||||
# that stopping moonraker did not take the web server down with it. The
|
||||
# s6 detail is appended to it rather than replacing it.
|
||||
if supervised; then
|
||||
_ng_stat=`svc_s6_stat "$SVC_NAME"`
|
||||
case "$_ng_stat" in
|
||||
up*) svc_say "running -- $_ng_stat" ;;
|
||||
*) svc_say "not running -- $_ng_stat" ;;
|
||||
esac
|
||||
unset _ng_stat
|
||||
return 0
|
||||
fi
|
||||
# No supervisor, so the pidfile is all there is -- and it is worth saying
|
||||
# that the answer is coming from a weaker source than usual, because "not
|
||||
# running" from a pidfile and "not running" from s6 are not the same claim.
|
||||
svc_pid_alive "$NGINX_PID" \
|
||||
&& svc_say "running (unsupervised, pid `cat "$NGINX_PID" 2>/dev/null`)" \
|
||||
|| svc_say "not running (no supervisor; asked the pidfile)"
|
||||
}
|
||||
|
||||
svc_dispatch "$@"
|
||||
@@ -1,421 +0,0 @@
|
||||
#!/bin/sh
|
||||
# moonraker -- the API on :7125 that Mainsail and HelixScreen both talk to.
|
||||
#
|
||||
# It already ships on stock firmware -- FlashForge simply comments it out of
|
||||
# /usr/prog/klipper/start.sh and serves its own HTTP API from firmwareExe
|
||||
# instead. The mod replaces firmwareExe, so that API is gone: this is the only
|
||||
# thing on the printer that can answer a UI, and a printer whose moonraker is
|
||||
# not running is a printer with no UI at all, however healthy klippy is.
|
||||
#
|
||||
# THIS AND S60nginx WERE ONE SCRIPT, S60web -- see the header there for why
|
||||
# they are two now, and why the numbering has to keep this one after S60nginx
|
||||
# and before S65camera.
|
||||
#
|
||||
# MOD_WEB=0 gates both scripts. It does not fall back to anything; it is a
|
||||
# debugging switch, not a way back to stock.
|
||||
#
|
||||
# WHAT CHANGED WHEN s6 ARRIVED, and what deliberately did not. moonraker is now
|
||||
# a supervised service: $MODDIR/etc/s6/moonraker/ holds a `run` script,
|
||||
# s6-supervise runs it, and when moonraker dies s6 starts it again. That is the
|
||||
# whole point -- before this, a server that fell over stayed fallen until
|
||||
# somebody noticed the UI was dead and ssh'd in, and this script had no way to
|
||||
# do anything about it because a shell that has returned cannot restart
|
||||
# anything. Three pieces of hand-rolled machinery went with it:
|
||||
#
|
||||
# the pidfile -> s6-svstat, asked of the supervisor. /run/moonraker.pid
|
||||
# is no longer written by anything; it survives below
|
||||
# only as the upgrade path, see stop().
|
||||
# moonraker_check -> a real READINESS notification. The old check slept
|
||||
# two seconds five times and called the service "up"
|
||||
# if the pid was still there; the run script now
|
||||
# notifies s6 when the API is LISTENING, and the
|
||||
# detached reporter below blocks on that. See the long
|
||||
# note in etc/s6/moonraker/run for why "listening" is
|
||||
# the right definition and "klippy is ready" is not.
|
||||
# stop-and-wait -> s6-svc -wD -d, which marks the service down BEFORE
|
||||
# signalling it and returns only when it is gone.
|
||||
#
|
||||
# THE INTERFACE IS UNCHANGED, on purpose. `S62moonraker restart` over ssh -- the
|
||||
# one you type over and over while chasing a moonraker config -- is in the docs
|
||||
# and in people's hands, and changing the supervisor is not a reason to change
|
||||
# what people type. start/stop/restart/status all still exist, still say
|
||||
# "moonraker: ...", and still mean what they meant.
|
||||
#
|
||||
# WHERE THE MOD_WEB GATE LIVES NOW. In this file, exactly as before, and NOT in
|
||||
# the scandir -- $MODDIR/etc/s6/moonraker/down ships in the payload precisely so
|
||||
# that the scanner never makes that decision. anvil.conf is read at runtime and
|
||||
# the payload is built once; the long version of that argument is written in
|
||||
# the `down` file itself, next to the thing it explains.
|
||||
MODDIR=/usr/data/anvil
|
||||
[ -f $MODDIR/anvil.conf ] && . $MODDIR/anvil.conf
|
||||
# The library path and the interpreter, from the one file that defines them.
|
||||
# See anvil-env.sh for why naming them here instead was a bug: this is the
|
||||
# script that had its own four-directory copy of the list while the installer
|
||||
# checked with ten, and moonraker is the process that dies on
|
||||
# "libpython3.8.so.1.0: cannot open shared object file" when it is wrong.
|
||||
if [ -f $MODDIR/anvil-env.sh ]; then
|
||||
. $MODDIR/anvil-env.sh
|
||||
else
|
||||
echo "moonraker: !! no $MODDIR/anvil-env.sh -- the mod payload is not installed"
|
||||
fi
|
||||
# start/stop/status/liveness, shared with the other services. See
|
||||
# anvil-service.sh for the whole API -- including the svc_s6_* helpers, which
|
||||
# are what the verbs below are built out of.
|
||||
if [ -f $MODDIR/anvil-service.sh ]; then
|
||||
. $MODDIR/anvil-service.sh
|
||||
else
|
||||
echo "moonraker: !! no $MODDIR/anvil-service.sh -- the mod payload is not installed"
|
||||
exit 1
|
||||
fi
|
||||
SVC_NAME=moonraker
|
||||
|
||||
MOONRAKER_LOG=/usr/data/logs/moonraker.log
|
||||
MOONRAKER_TMP=/usr/data/tmp
|
||||
MOONRAKER_DATA=/usr/data
|
||||
|
||||
# THE PIDFILE THE OLD SCRIPT WROTE. Nothing here writes it any more -- the
|
||||
# supervised service is identified by s6-svstat and the fallback path below
|
||||
# writes it only on a printer that has no supervisor at all. It is named
|
||||
# unconditionally because stop() has to be able to clean up after a payload
|
||||
# that DID write it: see the comment there. Same shape, same reason, as
|
||||
# S65camera's LEGACY_PIDFILE.
|
||||
MOONRAKER_PID=/run/moonraker.pid
|
||||
|
||||
# ONE moonraker, at one path, and that path is on the DATA partition.
|
||||
#
|
||||
# bin/patch.sh stages Moonraker's python package straight into the payload
|
||||
# (`cp -a work/.moonraker/moonraker "$MOD_PAYLOAD/moonraker"`), so the tarball
|
||||
# the installer extracts already puts the entry point here. Nothing else has to
|
||||
# happen for this to exist, and if it does not exist the install did not
|
||||
# complete.
|
||||
#
|
||||
# It used to run from /usr/prog/moonraker/moonraker/moonraker/moonraker.py --
|
||||
# a byte-identical SECOND copy of this same tree that run-append.sh made after
|
||||
# extracting the first one. That copy is gone, and the reasons it seemed
|
||||
# necessary did not survive being checked:
|
||||
#
|
||||
# * /usr/prog is the small firmware partition, and it is what a stock
|
||||
# FlashForge flash overwrites. Running from there meant that flashing stock
|
||||
# firmware silently reverted Moonraker to FlashForge's 2022 build while the
|
||||
# rest of the mod -- which lives on /usr/data and survives -- carried on
|
||||
# assuming the new one. That is the "which moonraker is this printer
|
||||
# actually running?" ambiguity we spent a release removing, reintroduced by
|
||||
# the one file nobody looked at.
|
||||
# * The moonraker-env virtualenv sitting beside it is unused. Imports resolve
|
||||
# from /usr/prog/Python-3.8.2/lib/python3.8/site-packages instead --
|
||||
# verified by running the printer's own interpreter on the real image,
|
||||
# where moonraker-env is not on sys.path at all.
|
||||
# * moonrakerDaemon, the one thing that did name the /usr/prog tree by
|
||||
# absolute path, is never invoked: this script starts moonraker itself.
|
||||
#
|
||||
# There is no fallback here and there must not be one. The mod ships exactly
|
||||
# one moonraker and installs it; if it is not at this path, that is a failed
|
||||
# install to be reported, not a condition to route around. The `run` script
|
||||
# names the same path, and the two must not drift.
|
||||
MOONRAKER_MAIN=$MODDIR/moonraker/moonraker.py
|
||||
|
||||
# The service directory the scanner watches, spelled out of the two variables
|
||||
# anvil-service.sh defines so that this script and S40s6 cannot disagree about
|
||||
# where the scandir is. S6NAME is a separate variable from SVC_NAME even though
|
||||
# the two are spelled the same today: SVC_NAME is what this script calls itself
|
||||
# in the log, S6NAME is what s6 calls the thing it supervises, and keeping them
|
||||
# apart is what stops a future rename of one silently renaming the other.
|
||||
S6NAME=moonraker
|
||||
SVCDIR=$SVC_S6_SCANDIR/$S6NAME
|
||||
|
||||
# Is there a live s6-supervise for our service directory RIGHT NOW?
|
||||
#
|
||||
# Two separate questions hide in "is s6 there", and both have to be yes before
|
||||
# any of the verbs below can use it. svc_s6_running answers the first -- is the
|
||||
# SCANNER alive and listening -- and does it by behaviour, by pinging the
|
||||
# control FIFO. This answers the second: has that scanner actually picked our
|
||||
# directory up and started a supervisor for it? A scandir the scanner has not
|
||||
# read yet (the payload was extracted after S40s6 ran; somebody dropped the
|
||||
# directory in by hand) looks exactly like a scandir with no moonraker in it,
|
||||
# and `s6-svc` against it fails with "supervisor not running" -- a true
|
||||
# statement about a state that fixes itself the moment the scanner rescans.
|
||||
#
|
||||
# s6-svok is that question in one syscall: it opens the supervise/control FIFO
|
||||
# and exits 0 only if a supervisor has it open at the other end. Same technique
|
||||
# as svc_s6_running, one level down, and the same reason for preferring it to a
|
||||
# test for the directory: a servicedir on disk with no supervisor behind it is
|
||||
# the stale-pidfile lie all over again.
|
||||
supervised() {
|
||||
[ -x "$SVC_S6_BIN/s6-svok" ] || return 1
|
||||
"$SVC_S6_BIN/s6-svok" "$SVCDIR"
|
||||
}
|
||||
|
||||
# Ask the scanner to pick up the service directory, then wait a moment for the
|
||||
# supervisor to appear. svc_s6_running IS the rescan request -- `s6-svscanctl
|
||||
# -a` is what it pings with -- so this is one call doing two jobs, which is
|
||||
# worth saying out loud because it does not look like it does. The bound is 5
|
||||
# seconds and is only ever paid on a printer where something is already wrong:
|
||||
# on a normal boot the payload was extracted long before S40s6 started the
|
||||
# scanner, so the supervisor is already there and the first test succeeds.
|
||||
# Anything that spends time here is spending firmwareExe's boot, one script at
|
||||
# a time in the foreground, which is why it is 5 and not 30.
|
||||
await_supervisor() {
|
||||
supervised && return 0
|
||||
svc_s6_running || return 1
|
||||
_mr_waited=0
|
||||
while ! supervised && [ $_mr_waited -lt 5 ]; do
|
||||
sleep 1
|
||||
_mr_waited=$((_mr_waited + 1))
|
||||
done
|
||||
unset _mr_waited
|
||||
supervised
|
||||
}
|
||||
|
||||
# ---- the fallback, and why moonraker has one when the camera does not ------
|
||||
#
|
||||
# WHAT HAPPENS WHEN THERE IS NO SUPERVISOR. MOD_S6=0 is a switch S40s6 honours,
|
||||
# so "no scanner" is a state a user can choose; and a payload whose s6 failed
|
||||
# to build, or an install that got half way, is a state nobody chose but that
|
||||
# happens anyway -- the -D_FILE_OFFSET_BITS=64 trap in
|
||||
# tools/supervisor/README.md produces binaries that exec fine and then go blind
|
||||
# on readdir(). The two services already migrated answered this question
|
||||
# differently on purpose: S60nginx falls back to starting nginx directly,
|
||||
# S65camera refuses and explains. moonraker FALLS BACK, with nginx, and the
|
||||
# reason is that it is the same decision, not a second one:
|
||||
#
|
||||
# * MOD_WEB IS ONE SWITCH ACROSS BOTH SCRIPTS, so "the web stack runs" and
|
||||
# "the web stack does not" are the only two states the user asked for. A
|
||||
# dead supervisor that left nginx serving and moonraker refusing would
|
||||
# invent a third one that nobody chose and that is the worst of the three
|
||||
# to be handed: Mainsail loads, connects to nothing, and blames Klipper.
|
||||
# Every minute spent debugging that is spent in the wrong subsystem.
|
||||
# * MOONRAKER IS NOT ONLY THE UI. bin/ff-startup.py -- which S70klipper hands
|
||||
# the whole boot to -- drives the panel from moonraker's API and cannot
|
||||
# learn klippy_state, report an MCU fault, or run the firmware-config
|
||||
# migration without it. Refusing here would turn a supervisor that did not
|
||||
# build into a printer that cannot bring itself up or say why, and the way
|
||||
# you would be told is the screen and the web UI that both just went away.
|
||||
# That is a strictly worse failure than the camera's, which is why the
|
||||
# camera's answer is not copied: a printer with no camera is a printer with
|
||||
# no camera, and a printer with no moonraker is a brick with a login shell.
|
||||
# * The old path is not hypothetical or unmaintained -- it is the code that
|
||||
# shipped for the whole life of this script, it is a dozen lines, and
|
||||
# case-moonraker.sh still exercises it on the no-scanner path.
|
||||
# * The classic reason NOT to fall back is that you end up with two of them:
|
||||
# a directly-started moonraker, then a scanner arriving later and starting a
|
||||
# second one on a port that is already bound. That cannot happen here, and
|
||||
# it is the `down` file that makes it so -- the scanner never starts
|
||||
# moonraker on its own, only this script does. And when this script does,
|
||||
# the `run` script sweeps any moonraker that is already running before
|
||||
# exec'ing its own, so the hand-over from the fallback to the supervised
|
||||
# copy is clean rather than a fight over :7125.
|
||||
#
|
||||
# What it is NOT allowed to be is silent. A printer running the fallback has
|
||||
# lost automatic restart-on-crash without losing the API, which is exactly the
|
||||
# kind of half-degraded state that gets discovered six months later, so it says
|
||||
# so every time, with "!!", which is what people grep a boot log for.
|
||||
start_unsupervised() {
|
||||
svc_warn "no supervisor -- starting moonraker directly, it will NOT be restarted if it dies"
|
||||
# The wipe is guarded on moonraker not already running: re-running this
|
||||
# script over ssh against a live server would otherwise delete the
|
||||
# directory it is streaming an upload into. (The supervised path has no
|
||||
# such guard and does not need one -- see the comment in the run script.)
|
||||
if ! svc_pid_alive "$MOONRAKER_PID"; then
|
||||
rm -rf $MOONRAKER_TMP
|
||||
fi
|
||||
mkdir -p $MOONRAKER_TMP
|
||||
export TMPDIR=$MOONRAKER_TMP
|
||||
mkdir -p `dirname $MOONRAKER_LOG`
|
||||
|
||||
# Yield to klippy on a two-core SoC: see NICE_MOONRAKER in anvil.conf for
|
||||
# why this is insurance rather than a fix. Unset or 0 = normal priority.
|
||||
SVC_NICE="${NICE_MOONRAKER:-0}"
|
||||
svc_start_daemon "$MOONRAKER_PID" "$FF_PYTHON" \
|
||||
"$MOONRAKER_MAIN" -d "$MOONRAKER_DATA" || return 1
|
||||
unset SVC_NICE
|
||||
svc_say "started (unsupervised, $MOONRAKER_MAIN)"
|
||||
# The pre-s6 liveness check, kept whole for the path that still needs it.
|
||||
# It is a guess -- a pid that is still there is not an API that answers --
|
||||
# and it is why the supervised path has a readiness notification instead.
|
||||
svc_detach moonraker_check
|
||||
}
|
||||
|
||||
# Component-load failures happen within seconds of exec, so a server still
|
||||
# alive at 10s is up. Confirming and returning beats holding the boot log open
|
||||
# for a minute: this runs in the background while S65camera, S70klipper and
|
||||
# S80ui are writing to the same file, and an interleaved log is what made one
|
||||
# user's boot unreadable at the point they were trying to read it.
|
||||
moonraker_check() {
|
||||
waited=0
|
||||
while [ $waited -lt 5 ]; do
|
||||
sleep 2
|
||||
waited=$((waited + 1))
|
||||
svc_pid_alive "$MOONRAKER_PID" && continue
|
||||
svc_warn "NOT running after $((waited * 2))s -- last words:"
|
||||
tail -n 15 "$MOONRAKER_LOG" 2>/dev/null | sed 's/^/ /'
|
||||
return 1
|
||||
done
|
||||
svc_say "up"
|
||||
return 0
|
||||
}
|
||||
|
||||
# The detached half of the supervised path, and the thing the whole phase was
|
||||
# for. Nothing reads its exit code; it reports through the boot log only,
|
||||
# exactly as moonraker_check did.
|
||||
#
|
||||
# svc_s6_ready blocks on s6-svwait -U, which returns when the run script writes
|
||||
# to its notification-fd -- and the run script does that only once moonraker has
|
||||
# BOUND :7125, not when it was forked. So the "answering" line below is true
|
||||
# when it is printed, which is the entire difference between this and the old
|
||||
# check that watched a pid and said "up".
|
||||
#
|
||||
# 120 seconds, against the run script's own 180-second probe. A caller-side
|
||||
# bound equal to the callee's would be a coin flip over which one reports
|
||||
# first; a shorter one means this says "not answering yet" while the prober is
|
||||
# still watching, which is the honest way round -- the service is still coming
|
||||
# up and s6 will keep it, and the next `S62moonraker status` tells the truth.
|
||||
# The boot never waits on any of it: this runs detached, which is what
|
||||
# svc_detach is for, and the trade (log lines arriving out of order with the
|
||||
# services that started after this one) is the one S50wifi and S65camera both
|
||||
# already make.
|
||||
ready_report() {
|
||||
if svc_s6_ready "$S6NAME" 120; then
|
||||
svc_say "answering on :7125"
|
||||
return 0
|
||||
fi
|
||||
svc_warn "NOT answering on :7125 after 120s -- `svc_s6_stat "$S6NAME"` -- last words:"
|
||||
tail -n 15 "$MOONRAKER_LOG" 2>/dev/null | sed 's/^/ /'
|
||||
return 1
|
||||
}
|
||||
|
||||
start() {
|
||||
[ "${MOD_WEB:-1}" = "1" ] || { svc_say "disabled in anvil.conf (MOD_WEB=0)"; return 0; }
|
||||
|
||||
# Every branch below says something. There is no stock server left to
|
||||
# quietly take over, so the boot log is the only place that can explain a
|
||||
# printer with no UI. A missing prerequisite is REPORTED and returns
|
||||
# non-zero; it is not a crash and it is not a stack trace out of s6, which
|
||||
# is what handing a supervisor a service it cannot run produces -- once a
|
||||
# second, for ever, in a log nobody is reading.
|
||||
if ! anvil_python_ok; then
|
||||
svc_warn "no interpreter at ${FF_PYTHON:-/usr/prog/Python-3.8.2/bin/python3} -- not started"
|
||||
return 1
|
||||
fi
|
||||
if [ ! -f "$MOONRAKER_MAIN" ]; then
|
||||
svc_warn "nothing at $MOONRAKER_MAIN -- the install did not complete"
|
||||
return 1
|
||||
fi
|
||||
mkdir -p `dirname $MOONRAKER_LOG`
|
||||
|
||||
# The third prerequisite, which is new: the service directory itself. It is
|
||||
# staged into the payload by bin/patch.sh and named in the install
|
||||
# manifest, so its absence means a payload that was built without it or an
|
||||
# install that did not finish -- and it is deliberately NOT created here.
|
||||
# Writing it at runtime would mean the scandir contained a directory no
|
||||
# manifest lists, which is the exact thing the manifest exists to stop, and
|
||||
# it would put the `run` script's text inside this file where nobody would
|
||||
# ever look at it again.
|
||||
if [ ! -x "$SVCDIR/run" ]; then
|
||||
svc_warn "no $SVCDIR/run -- this payload did not ship the s6 service"
|
||||
start_unsupervised
|
||||
return $?
|
||||
fi
|
||||
|
||||
if ! await_supervisor; then
|
||||
start_unsupervised
|
||||
return $?
|
||||
fi
|
||||
|
||||
# And up it goes. svc_s6_up is `s6-svc -wu -u` with a bound: it asks for the
|
||||
# service to be brought up -- overriding the `down` file, which is how the
|
||||
# MOD_WEB gate above turns into a running server -- and does not return
|
||||
# until it IS up. UP, note, not READY: -wu returns as soon as s6 has forked
|
||||
# the run script, which is exactly what we want on the boot path, because
|
||||
# moonraker takes the better part of a minute to import its components on
|
||||
# this box and firmwareExe is running these scripts one at a time in the
|
||||
# foreground. 15 seconds is the shared default and is a bound on the FORK,
|
||||
# not on the startup; anything that takes longer than that to fork is a
|
||||
# scandir problem, not a moonraker problem.
|
||||
if ! svc_s6_up "$S6NAME" 15; then
|
||||
# Up it did not come. s6 has kept the reason where a reason belongs:
|
||||
# the `run` script's stderr is the scanner's stderr, which S40s6 points
|
||||
# at /usr/data/logs/s6.log. Say where, rather than paraphrasing
|
||||
# something we have not read.
|
||||
svc_warn "FAILED to come up -- `svc_s6_stat "$S6NAME"` -- see /usr/data/logs/s6.log"
|
||||
return 1
|
||||
fi
|
||||
# Forked, not serving. The difference is the whole point of the next two
|
||||
# lines, and of the notification-fd in the service directory.
|
||||
svc_detach ready_report
|
||||
svc_say "started ($MOONRAKER_MAIN); waiting for the API in the background"
|
||||
return 0
|
||||
}
|
||||
|
||||
stop() {
|
||||
# A stop that the supervisor undoes is the classic mistake of this
|
||||
# migration, and `s6-svc -d` is what makes it not happen: it does not just
|
||||
# signal the process, it tells s6 the service is WANTED DOWN, and s6 then
|
||||
# does not start it again until something says otherwise. Killing the pid
|
||||
# instead -- which is all this script could do before, and all
|
||||
# svc_stop_daemon still does below -- would get moonraker restarted a
|
||||
# second later by a supervisor doing exactly what it was asked to do.
|
||||
#
|
||||
# -wD (inside svc_s6_down) is the other half: it returns when the service is
|
||||
# really down, not when the signal has been sent. That is the busybox
|
||||
# start-stop-daemon correction from anvil-service.sh, except in C, and it is
|
||||
# what makes `restart` safe without a sleep in front of it: the 15-second
|
||||
# bound is the same one svc_stop_daemon used, long enough for moonraker to
|
||||
# finish writing its database and short enough not to hang a boot.
|
||||
if supervised; then
|
||||
svc_s6_down "$S6NAME" 15 >/dev/null 2>&1 \
|
||||
|| svc_warn "s6 did not report $S6NAME down: `svc_s6_stat "$S6NAME"`"
|
||||
fi
|
||||
|
||||
# AND THE PIDFILE, which covers two live cases rather than being tidiness.
|
||||
#
|
||||
# * The fallback above really did start a moonraker with
|
||||
# start-stop-daemon on a printer with no scanner, and that copy has no
|
||||
# supervisor to take it down. The pidfile is the only handle on it.
|
||||
# * THE UPGRADE PATH. A printer running the pre-phase-5 payload has a
|
||||
# start-stop-daemon'd moonraker alive with its pid in this file.
|
||||
# Install this payload, then type `S62moonraker restart` instead of
|
||||
# rebooting, and without this line that server is still there holding
|
||||
# :7125 -- and the supervised copy this script is about to start cannot
|
||||
# bind, exits, gets respawned, and loops for ever while s6-svstat says
|
||||
# "up". (The run script sweeps for exactly that leftover as a second
|
||||
# line of defence; this is the first, and it is the polite one.)
|
||||
#
|
||||
# svc_stop_daemon returns 0 immediately when the pidfile does not exist,
|
||||
# which is every printer that has rebooted once since the upgrade, so this
|
||||
# costs nothing in the normal case. It also removes the file, which is what
|
||||
# keeps `status` from answering out of a pid the kernel has since handed to
|
||||
# something else.
|
||||
svc_stop_daemon "$MOONRAKER_PID"
|
||||
svc_say "stopped"
|
||||
}
|
||||
|
||||
status() {
|
||||
# Asked of the supervisor when there is one, because it is the only thing
|
||||
# that knows. s6-svstat's own wording is passed through -- "up (pid 1234
|
||||
# pgid 1234) 71 seconds", "down (signal SIGTERM) 4 seconds, normally up" --
|
||||
# for the reason anvil-service.sh gives for svc_s6_stat: it is the answer
|
||||
# you came for, in the shape you would have typed the command to get.
|
||||
#
|
||||
# The words "running" and "not running" are still the first thing on the
|
||||
# line, though, and that is not decoration either. It is the vocabulary
|
||||
# every other service in init.d/ answers in, it is what a human skims a
|
||||
# boot log for, and it is what case-moonraker.sh looks for. The s6 detail
|
||||
# is appended to it rather than replacing it.
|
||||
if supervised; then
|
||||
_mr_stat=`svc_s6_stat "$S6NAME"`
|
||||
case "$_mr_stat" in
|
||||
up*) svc_say "running -- $_mr_stat" ;;
|
||||
*) svc_say "not running -- $_mr_stat" ;;
|
||||
esac
|
||||
unset _mr_stat
|
||||
return 0
|
||||
fi
|
||||
# No supervisor, so the pidfile is all there is -- and it is worth saying
|
||||
# that the answer is coming from a weaker source than usual, because "not
|
||||
# running" from a pidfile and "not running" from s6 are not the same claim.
|
||||
svc_pid_alive "$MOONRAKER_PID" \
|
||||
&& svc_say "running (unsupervised, pid `cat "$MOONRAKER_PID" 2>/dev/null`)" \
|
||||
|| svc_say "not running (no supervisor; asked the pidfile)"
|
||||
}
|
||||
|
||||
svc_dispatch "$@"
|
||||
@@ -1,270 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Camera -- mjpg-streamer on :8080, supervised by s6.
|
||||
#
|
||||
# WHY THIS EXISTS -- the same trap as WiFi. /usr/prog/mjpg-streamer ships on
|
||||
# stock firmware (mjpg_streamer, input_uvc.so, output_http.so, its own
|
||||
# libjpeg.so.9 and a www/ dir), and app_startup.sh only ever exports
|
||||
# LD_LIBRARY_PATH for it -- it never launches it. firmwareExe did, and in fact
|
||||
# served the stream from its own OpenCV capture, labelling itself
|
||||
# "Server: MJPG-Streamer/0.2". So the moment the mod replaces firmwareExe the
|
||||
# camera goes dark, with a perfectly good streamer sitting unused on the disk.
|
||||
#
|
||||
# Nothing has to be built or shipped for this: we start the binary FlashForge
|
||||
# already put there. Mainsail then finds it through the [webcam] entry in
|
||||
# moonraker.conf, which points at nginx's /webcam/ proxy rather than :8080
|
||||
# directly, so a tunnel that forwards one port still shows the stream.
|
||||
#
|
||||
# WHAT CHANGED IN PHASE 4. This script used to BE the supervisor. It carried a
|
||||
# `while true; do mjpg_streamer ...; sleep 3; done` respawn loop and a poll
|
||||
# that waited up to thirty seconds for /dev/video0, both inside a background
|
||||
# subshell held open by a pidfile -- a pidfile that pointed at THE LOOP rather
|
||||
# than at the streamer, because killing only the streamer got it restarted
|
||||
# three seconds later. All of that now lives in s6:
|
||||
#
|
||||
# the respawn loop -> s6-supervise, which is C and does not race itself
|
||||
# the 30s poll -> the top of $MODDIR/etc/s6/camera/run, where a
|
||||
# timeout means "exit and let s6 try again" instead of
|
||||
# "give up until a human reruns this script"
|
||||
# "is it running?" -> s6-svstat, asked of the supervisor rather than
|
||||
# guessed from `ps | grep mjpg_streamer`
|
||||
# the stop dance -> s6-svc -wD -d, which marks the service down BEFORE
|
||||
# signalling it and returns only when it is gone
|
||||
#
|
||||
# What is left here is the INTERFACE. `S65camera restart` over ssh is in the
|
||||
# docs and in people's hands, and changing the supervisor is not a reason to
|
||||
# change the thing people type. Every verb still works, still says "camera:",
|
||||
# and still returns promptly.
|
||||
#
|
||||
# IT STILL MUST NOT MAKE THE BOOT WAIT, and the shape of that has changed. It
|
||||
# is no longer this script that waits for the camera at all -- the run script
|
||||
# does. But somebody has to SAY, in the boot log, whether the camera came up,
|
||||
# and that answer takes up to thirty seconds to exist. So start() asks s6 to
|
||||
# bring the service up (fast: -wu returns as soon as the streamer is forked)
|
||||
# and detaches a reporter that blocks on READINESS and prints the verdict when
|
||||
# it arrives. The boot never waits on hardware; the log still gets the truth,
|
||||
# just out of order with the services that started after this one. That is
|
||||
# svc_detach, and it is the same trade S50wifi makes -- see anvil-service.sh.
|
||||
MODDIR=/usr/data/anvil
|
||||
[ -f $MODDIR/anvil.conf ] && . $MODDIR/anvil.conf
|
||||
[ -f $MODDIR/anvil-env.sh ] && . $MODDIR/anvil-env.sh
|
||||
# start/stop/status/liveness, shared with the other services -- including the
|
||||
# svc_s6_* helpers this script is now written almost entirely in. See
|
||||
# anvil-service.sh for the whole API.
|
||||
if [ -f $MODDIR/anvil-service.sh ]; then
|
||||
. $MODDIR/anvil-service.sh
|
||||
else
|
||||
echo "camera: !! no $MODDIR/anvil-service.sh -- the mod payload is not installed"
|
||||
exit 1
|
||||
fi
|
||||
SVC_NAME=camera
|
||||
|
||||
# The name of the service DIRECTORY inside SVC_S6_SCANDIR, which is a different
|
||||
# thing from SVC_NAME even though they are spelled the same today: SVC_NAME is
|
||||
# what this script calls itself in the log, S6NAME is what s6 calls the thing
|
||||
# it supervises. Keeping them as two variables is what stops a future rename of
|
||||
# one from silently renaming the other.
|
||||
S6NAME=camera
|
||||
S6DIR=$SVC_S6_SCANDIR/$S6NAME
|
||||
|
||||
CAMDIR=/usr/prog/mjpg-streamer
|
||||
VIDEO=/dev/video0
|
||||
LOG=/usr/data/logs/mjpg.log
|
||||
RES="${MOD_CAM_RES:-1280x720}"
|
||||
FPS="${MOD_CAM_FPS:-30}"
|
||||
PORT="${MOD_CAM_PORT:-8080}"
|
||||
|
||||
# The pidfile the OLD supervisor used. Nothing here writes it any more; it is
|
||||
# named only so stop() can clean up after a payload that did -- see the comment
|
||||
# there. Once no printer in the field can still be running the pre-phase-4
|
||||
# payload this and the two lines that use it can go.
|
||||
LEGACY_PIDFILE=$MODDIR/mjpg.pid
|
||||
|
||||
# ---- what to do when there is no supervisor --------------------------------
|
||||
#
|
||||
# MOD_S6=0 in anvil.conf turns the scanner off, and a payload whose s6 build
|
||||
# went wrong has no scanner either -- the -D_FILE_OFFSET_BITS=64 trap in
|
||||
# tools/supervisor/README.md produces binaries that exec fine and then go blind
|
||||
# on readdir(). Both are real states on a real printer, and both used to be
|
||||
# impossible because this script supervised itself.
|
||||
#
|
||||
# The choice here is to DEGRADE LOUDLY AND DO NOTHING, rather than to fall back
|
||||
# to the old hand-rolled loop. Keeping the loop as a fallback would mean two
|
||||
# implementations of the camera, one of them exercised only on printers that
|
||||
# are already broken and therefore never tested -- which is precisely how the
|
||||
# five services grew five different liveness checks in the first place. It
|
||||
# would also be the worst kind of failure to diagnose: a camera that works
|
||||
# means nobody notices the supervisor is dead until Klipper needs it too.
|
||||
#
|
||||
# So: one line naming the actual problem, and a non-zero exit. firmwareExe
|
||||
# already prints the exit status of a service that fails, S40s6 has already
|
||||
# printed its own "not answering" warning by this point in the boot, and the
|
||||
# two lines together say the whole story. A printer with no camera is a
|
||||
# printer with no camera; a printer with no supervisor is about to have larger
|
||||
# problems, and hiding one behind the other helps nobody.
|
||||
s6_or_complain() {
|
||||
if svc_s6_running; then
|
||||
return 0
|
||||
fi
|
||||
svc_warn "the s6 supervisor is not running -- nothing can start the camera"
|
||||
svc_warn "check MOD_S6 in $MODDIR/anvil.conf, then $MODDIR/init.d/S40s6 status"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Wait for s6-supervise to appear for our service directory, then bring it up.
|
||||
#
|
||||
# WHY THIS IS NOT JUST svc_s6_up. s6-svc talks to $S6DIR/supervise/control, a
|
||||
# FIFO that s6-supervise creates -- and s6-supervise only exists once the
|
||||
# SCANNER has noticed the directory. On a first boot after an update the
|
||||
# directory is brand new: the scandir was scanned before the payload landed, or
|
||||
# scanned when the directory was empty, and the scanner has not looked since.
|
||||
# svc_s6_running is what pokes it -- `s6-svscanctl -a` is literally the request
|
||||
# to rescan, which is why the liveness check and the rescan are the same call
|
||||
# -- but the scan, the fork and the mkfifo take a moment, and a s6-svc that
|
||||
# arrives during that moment fails with "unable to control" rather than
|
||||
# waiting. Five one-second tries covers it with room to spare and costs
|
||||
# nothing on the normal path, where the first attempt succeeds.
|
||||
s6_up_when_ready() {
|
||||
_cam_try=0
|
||||
while [ $_cam_try -lt 5 ]; do
|
||||
if svc_s6_up "$S6NAME" 15; then
|
||||
unset _cam_try
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
_cam_try=$((_cam_try + 1))
|
||||
done
|
||||
unset _cam_try
|
||||
return 1
|
||||
}
|
||||
|
||||
# The detached half. Nothing reads its exit code; it reports through the boot
|
||||
# log only, exactly as the old detached supervise() did.
|
||||
#
|
||||
# svc_s6_ready blocks on s6-svwait -U, which returns when the run script writes
|
||||
# to its notification-fd -- and the run script does that only once the streamer
|
||||
# has BOUND ITS PORT, not when it was forked. So the "streaming" line below is
|
||||
# true when it is printed, which is the entire difference between this and the
|
||||
# old "waiting for /dev/video0 in the background" that was printed hopefully
|
||||
# and never followed up.
|
||||
#
|
||||
# 35 seconds, not 30: the run script waits up to 30 for the device and then
|
||||
# still has to start the streamer and bind the port. A caller-side bound equal
|
||||
# to the callee's would report "not ready" for a camera that was about to work.
|
||||
ready_report() {
|
||||
if svc_s6_ready "$S6NAME" 35; then
|
||||
svc_say "streaming on :$PORT ($RES @ ${FPS}fps)"
|
||||
else
|
||||
svc_warn "not streaming after 35s -- no camera at $VIDEO? see $LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
start() {
|
||||
# MOD_CAM, and the reason the check is here rather than in the payload. The
|
||||
# service directory ships with a `down` file so that s6 never starts the
|
||||
# streamer on its own (see the long note inside that file); this line is
|
||||
# the other half of that decision -- the runtime read of the runtime
|
||||
# config, in the one place that has already sourced anvil.conf. A user who
|
||||
# sets MOD_CAM=0 and reboots gets a supervised service sitting idle, which
|
||||
# costs one ~30KB s6-supervise and no camera at all.
|
||||
if [ "${MOD_CAM:-1}" != "1" ]; then
|
||||
svc_say "disabled by MOD_CAM"
|
||||
# ...and if it happens to be up -- MOD_CAM was flipped and this is a
|
||||
# `restart`, not a boot -- take it down, because "disabled" that leaves
|
||||
# the camera streaming is a lie the old script never had to tell.
|
||||
if svc_s6_running && [ -d "$S6DIR" ]; then
|
||||
svc_s6_down "$S6NAME" 15 >/dev/null 2>&1
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
s6_or_complain || return 1
|
||||
if [ ! -d "$S6DIR" ]; then
|
||||
svc_warn "no service directory at $S6DIR -- this payload did not ship it"
|
||||
return 1
|
||||
fi
|
||||
# Kept from the pre-s6 script, and it still earns its place: without it the
|
||||
# first sign of a stock firmware that moved or dropped mjpg-streamer would
|
||||
# be s6 respawning a run script that fails on exec, once a second, forever.
|
||||
# Saying it once here is cheaper to read than that in a log.
|
||||
[ -x "$CAMDIR/mjpg_streamer" ] || { svc_warn "no $CAMDIR/mjpg_streamer"; return 1; }
|
||||
|
||||
if ! s6_up_when_ready; then
|
||||
svc_warn "s6 would not bring $S6NAME up: `svc_s6_stat "$S6NAME"`"
|
||||
return 1
|
||||
fi
|
||||
# Up, not ready. The difference is the whole point of the next two lines.
|
||||
svc_detach ready_report
|
||||
svc_say "starting under s6; waiting for $VIDEO in the background (:$PORT, $RES @ ${FPS}fps)"
|
||||
return 0
|
||||
}
|
||||
|
||||
stop() {
|
||||
# THE OLD ORDERING PROBLEM IS GONE, and it is worth saying why rather than
|
||||
# just deleting the comment that explained it. The old stop() had to kill
|
||||
# the supervisor FIRST and the streamer second, because the supervisor was
|
||||
# our own `while true; ... sleep 3` loop: signal the streamer while the
|
||||
# loop still lived and the loop simply started another one three seconds
|
||||
# later, so a stop in the wrong order looked like it had worked and had
|
||||
# not. Then it had to wait fifteen seconds for the streamer to actually
|
||||
# die, because killall returns as soon as the signal is sent and a start()
|
||||
# arriving a moment later would see the dying process, say "already
|
||||
# running", and leave the camera dark.
|
||||
#
|
||||
# s6-svc -d makes all three of those disappear, and not by being careful in
|
||||
# a different order -- by not having the race. It sets the service's wanted
|
||||
# state to DOWN and only then signals it, so there is no window in which
|
||||
# something is still trying to bring it back; and -wD does not return until
|
||||
# the process is really gone, which is the wait we were hand-rolling. The
|
||||
# supervisor itself is deliberately NOT killed: it stays, idle, holding the
|
||||
# service in the down state, which is what makes the next `start` a single
|
||||
# s6-svc -u instead of a rescan.
|
||||
if svc_s6_running && [ -d "$S6DIR" ]; then
|
||||
svc_s6_down "$S6NAME" 15 >/dev/null 2>&1 \
|
||||
|| svc_warn "s6 did not report $S6NAME down: `svc_s6_stat "$S6NAME"`"
|
||||
fi
|
||||
|
||||
# THE UPGRADE PATH, and the one place the deleted code still matters. A
|
||||
# printer running the pre-phase-4 payload has our old respawn loop alive
|
||||
# with its pid in $LEGACY_PIDFILE. Install this payload, then type
|
||||
# `S65camera restart` instead of rebooting, and without these two lines
|
||||
# that loop is still there -- respawning mjpg_streamer on :8080 and
|
||||
# fighting the s6 service for the port, with neither side ever winning and
|
||||
# nothing in any log saying why. svc_stop_daemon returns 0 immediately when
|
||||
# the pidfile does not exist, which is every printer that has rebooted
|
||||
# once, so this costs nothing in the normal case.
|
||||
if [ -f "$LEGACY_PIDFILE" ]; then
|
||||
svc_warn "found a pre-s6 respawn loop still running -- stopping it"
|
||||
svc_stop_daemon "$LEGACY_PIDFILE"
|
||||
killall mjpg_streamer 2>/dev/null
|
||||
fi
|
||||
svc_say "stopped"
|
||||
}
|
||||
|
||||
status() {
|
||||
# Asked of the supervisor, not of the process table. `ps | grep
|
||||
# mjpg_streamer` was the old answer and it could not tell a streamer that
|
||||
# is up from one that s6 is about to respawn, nor say anything at all about
|
||||
# a service that is deliberately down. s6-svstat's own wording -- "up (pid
|
||||
# 1234) 71 seconds", "down 5 seconds, normally up" -- is already the answer
|
||||
# you came for, so it is passed through rather than paraphrased, exactly as
|
||||
# S50wifi passes ifconfig through.
|
||||
#
|
||||
# The no-scanner case still prints a line and still names itself: a service
|
||||
# that answers `status` with silence is indistinguishable from one that
|
||||
# crashed, and every service in this directory owes the boot log an answer.
|
||||
if svc_s6_running; then
|
||||
if [ -d "$S6DIR" ]; then
|
||||
svc_say "s6: `svc_s6_stat "$S6NAME"`"
|
||||
else
|
||||
svc_say "no service directory at $S6DIR"
|
||||
fi
|
||||
else
|
||||
svc_say "not running -- the s6 supervisor is not running either"
|
||||
fi
|
||||
# And the hardware, which is the question somebody actually has when the
|
||||
# stream is black. Kept verbatim from the pre-s6 script.
|
||||
[ -e "$VIDEO" ] && svc_say "$VIDEO present" || svc_say "no $VIDEO"
|
||||
[ -f "$LEGACY_PIDFILE" ] && svc_warn "a pre-s6 respawn loop is still around ($LEGACY_PIDFILE)"
|
||||
return 0
|
||||
}
|
||||
|
||||
svc_dispatch "$@"
|
||||
@@ -1,216 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Klipper.
|
||||
#
|
||||
# WHY THIS EXISTS -- the single most important dependency in this whole mod:
|
||||
# on stock firmware nothing in /etc/init.d starts Klipper. It is FlashForge's
|
||||
# firmwareExe that runs "/usr/prog/klipper/start.sh &" (verified with
|
||||
# `strings firmwareExe`). So the moment HelixScreen replaces firmwareExe, the
|
||||
# printer boots to a working screen with NO klippy behind it: no motion, no
|
||||
# heaters, no printing.
|
||||
#
|
||||
# Therefore Klipper startup is owned here, independently of the UI.
|
||||
#
|
||||
# Double-start guard: we only start Klipper if it is not already running, and
|
||||
# start.sh carries the same guard, so a restart over ssh cannot end up with two
|
||||
# klippys against the same MCU.
|
||||
#
|
||||
# WHY IT ALSO SUPERVISES -- the heater board (mcu 'eheaterboard' on
|
||||
# /dev/ttyS4) is frequently not answering yet when klippy first opens the
|
||||
# port. Klipper polls it for 90s (connect_uart) and then gives up with "MCU
|
||||
# error during connect". klippy does NOT exit and does NOT retry on its own:
|
||||
# _connect() sets the error state, returns, and fires FlashForge's own
|
||||
# klippy:notify_mcu_error event over /tmp/uds. On stock, firmwareExe is the
|
||||
# API client listening for that event, and it restarts the firmware -- which
|
||||
# is why FlashForge's UI needs two or three attempts before every board is
|
||||
# found. Nothing in the mod plays that role, so klippy would sit in the error
|
||||
# state forever with no heaters. Restarting klippy achieves what the stock
|
||||
# retry achieves: reopening the port toggles DTR/RTS and the board comes up.
|
||||
#
|
||||
# The retry is the safety net, not the fix: the actual fix is the bootloader
|
||||
# handshake (ff_mcu_bringup.py). A board that missed its window still gets
|
||||
# another chance on each restart here, because start.sh redoes the bring-up
|
||||
# every time it runs.
|
||||
#
|
||||
# WHO NORMALLY DOES ALL THIS NOW. bin/ff-startup.py, which runs from the
|
||||
# firmwareExe wrapper right after these init scripts. It owns the sequence
|
||||
# because the bring-up has to happen before klippy opens the ports and the
|
||||
# retry that fixes a stranded board is "hand it over again, then reopen" --
|
||||
# so the thing doing the handshake has to be the thing restarting klippy. It
|
||||
# also has moonraker to ask, instead of tailing printer.log for markers, and
|
||||
# a screen to say which board is missing.
|
||||
#
|
||||
# This script therefore STANDS ASIDE when that program is going to run, and
|
||||
# does the whole job itself when it cannot -- no interpreter, no script,
|
||||
# MOD_STARTUP=0. Klipper starting is the single most important thing the mod
|
||||
# does and it must not come to depend on python. `force-start` ignores the
|
||||
# hand-off; the wrapper uses it as a last resort if ff-startup returned and
|
||||
# left no klippy behind.
|
||||
#
|
||||
# It is not a GPIO poke: firmwareExe drives only PB07 (wifi module power) and
|
||||
# gpios 34/35, and that pair is the NRST/BOOT0 of the /dev/ttyS3 board, driven
|
||||
# only by AppUpdateThread when flashing MCU firmware from the DeviceOTA
|
||||
# screen. Neither runs on a normal boot.
|
||||
MODDIR=/usr/data/anvil
|
||||
[ -f $MODDIR/anvil.conf ] && . $MODDIR/anvil.conf
|
||||
# start/stop/status/liveness, shared with the other services. See
|
||||
# anvil-service.sh for the whole API.
|
||||
if [ -f $MODDIR/anvil-service.sh ]; then
|
||||
. $MODDIR/anvil-service.sh
|
||||
else
|
||||
echo "klipper: !! no $MODDIR/anvil-service.sh -- the mod payload is not installed"
|
||||
exit 1
|
||||
fi
|
||||
SVC_NAME=klipper
|
||||
# `force-start` is this script's own verb; naming it here keeps the usage line
|
||||
# svc_dispatch prints true. See the case block at the bottom.
|
||||
SVC_EXTRA_VERBS="|force-start"
|
||||
|
||||
KLIPPER_TRIES=${MOD_KLIPPER_TRIES:-6}
|
||||
PRINTER_LOG=/usr/data/logs/printer.log
|
||||
|
||||
# klippy is started by FlashForge's start.sh, not by us, so there is no pidfile
|
||||
# and the only handle on it is the name: svc_proc_alive 'klippy\.py', which is
|
||||
# the pgrep-with-a-ps-fallback this script used to carry itself.
|
||||
#
|
||||
# klippy_running is deliberately MORE than that. /tmp/uds is the socket klippy
|
||||
# serves its API on, and it is what start.sh itself looks at before deciding
|
||||
# whether to start anything -- so a printer with the socket present is one
|
||||
# start.sh will refuse to touch, whatever `ps` says. Both questions matter and
|
||||
# they are not the same question, which is why this one stays here rather than
|
||||
# moving into anvil-service.sh: no other service has a socket to ask about.
|
||||
klippy_running() {
|
||||
[ -S /tmp/uds ] && return 0
|
||||
svc_proc_alive 'klippy\.py' && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# The two outcomes klippy actually writes to printer.log. There is no
|
||||
# "Printer is ready" line to look for -- _set_state() does not log -- but a
|
||||
# healthy klippy emits a "Stats <n>:" line every second.
|
||||
klippy_verdict() {
|
||||
tail -n +$(($1 + 1)) "$PRINTER_LOG" 2>/dev/null | awk '
|
||||
/MCU error during connect/ { print "error"; exit }
|
||||
/Stats [0-9]/ { print "ready"; exit }'
|
||||
}
|
||||
|
||||
supervise() {
|
||||
attempt=1
|
||||
while [ $attempt -le $KLIPPER_TRIES ]; do
|
||||
# NOT `wc -l < "$PRINTER_LOG" 2>/dev/null`: redirections are applied
|
||||
# left to right, so the input redirect fails and the shell prints
|
||||
# "can't open ...: no such file" BEFORE 2>/dev/null is in effect. On
|
||||
# a first boot printer.log does not exist yet, so that message landed
|
||||
# in the boot log every time, interleaved with whatever else was
|
||||
# writing -- which is what made one user's log unreadable at exactly
|
||||
# the point they were trying to diagnose.
|
||||
log_lines=`cat "$PRINTER_LOG" 2>/dev/null | wc -l`
|
||||
[ -n "$log_lines" ] || log_lines=0
|
||||
# A failed connect takes ~90s to be reported, so wait out more than
|
||||
# that before concluding anything.
|
||||
waited=0
|
||||
verdict=""
|
||||
while [ $waited -lt 30 ]; do
|
||||
sleep 5
|
||||
waited=$((waited + 1))
|
||||
verdict=`klippy_verdict "$log_lines"`
|
||||
[ -n "$verdict" ] && break
|
||||
done
|
||||
case "$verdict" in
|
||||
ready)
|
||||
svc_say "ready (attempt $attempt)"
|
||||
return 0
|
||||
;;
|
||||
"")
|
||||
# Neither marker showed up. Something other than an MCU
|
||||
# connect is going on; restarting would only hide it.
|
||||
svc_say "no verdict after attempt $attempt -- leaving it alone"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
svc_warn "MCU connect failed (attempt $attempt/$KLIPPER_TRIES) -- restarting"
|
||||
klippy_down
|
||||
/usr/prog/klipper/start.sh
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
svc_warn "still no MCU after $KLIPPER_TRIES attempts -- giving up"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Take klippy down and leave nothing behind that would stop the next start.
|
||||
#
|
||||
# The wait is not optional. klipperDaemon stop signals and returns; klippy is
|
||||
# still holding /dev/ttyS4 for a moment after that, and start.sh reopening the
|
||||
# port while the old process still has it is exactly the MCU failure this
|
||||
# script exists to retry out of. Bounded at 15s for the reason svc_stop_daemon
|
||||
# is: a stuck klippy should be said out loud, not waited on forever.
|
||||
#
|
||||
# And then the socket. start.sh bails out if /tmp/uds is still there, and
|
||||
# klippy does not unlink it on exit -- so a stale socket is a pidfile that
|
||||
# lies, in precisely the sense svc_stop_daemon removes pidfiles. It is only
|
||||
# removed once klippy is definitely gone: removing it out from under a live
|
||||
# klippy would leave a running printer nothing could talk to.
|
||||
klippy_down() {
|
||||
[ -x /usr/prog/klipper/klipperDaemon ] && /usr/prog/klipper/klipperDaemon stop
|
||||
stopping=0
|
||||
while svc_proc_alive 'klippy\.py' && [ $stopping -lt 15 ]; do
|
||||
sleep 1
|
||||
stopping=$((stopping + 1))
|
||||
done
|
||||
if svc_proc_alive 'klippy\.py'; then
|
||||
svc_warn "klippy still running after ${stopping}s"
|
||||
return 1
|
||||
fi
|
||||
rm -f /tmp/uds
|
||||
return 0
|
||||
}
|
||||
|
||||
# Is bin/ff-startup.py actually going to run and do this? Every condition the
|
||||
# wrapper checks, checked the same way -- if any of them is false this script
|
||||
# keeps the job.
|
||||
startup_owns_klipper() {
|
||||
[ "${MOD_STARTUP:-1}" = 1 ] || return 1
|
||||
[ -f $MODDIR/bin/ff-startup.py ] || return 1
|
||||
[ -x /usr/prog/Python-3.8.2/bin/python3 ] || command -v python3 >/dev/null 2>&1 || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
start() {
|
||||
if klippy_running; then
|
||||
svc_say "already running"
|
||||
return 0
|
||||
fi
|
||||
if [ "$1" != force ] && startup_owns_klipper; then
|
||||
svc_say "left to ff-startup.py (it does the MCU bring-up first)"
|
||||
return 0
|
||||
fi
|
||||
[ -x /usr/prog/klipper/start.sh ] || { svc_warn "start.sh missing"; return 1; }
|
||||
# start.sh does the MCU bring-up itself, and that costs a few seconds per
|
||||
# unresponsive board -- so it goes to the background; boot must not stall.
|
||||
svc_detach /usr/prog/klipper/start.sh
|
||||
svc_say "start.sh launched"
|
||||
# In the background: boot must not wait minutes for the heater board.
|
||||
svc_detach supervise
|
||||
}
|
||||
|
||||
stop() {
|
||||
klippy_down
|
||||
svc_say "stopped"
|
||||
}
|
||||
|
||||
status() {
|
||||
klippy_running && svc_say "running" || svc_say "not running"
|
||||
}
|
||||
|
||||
# force-start and restart are handled here, before svc_dispatch, because both
|
||||
# need the argument svc_dispatch does not pass: `start force`. This script's
|
||||
# start() normally STANDS ASIDE for ff-startup.py, so a plain `stop; start` --
|
||||
# which is what svc_dispatch's restart is -- would stop klippy and then decline
|
||||
# to start it, leaving the printer with no motion and no heaters. A restart
|
||||
# asked for by hand means restart it, so it forces. There is no `sleep 2` in
|
||||
# front of the start any more: stop() now waits for klippy to actually be gone
|
||||
# and clears the stale socket, which is what the sleep was approximating.
|
||||
case "$1" in
|
||||
force-start) start force ;;
|
||||
restart) stop; start force ;;
|
||||
*) svc_dispatch "$@" ;;
|
||||
esac
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Decide WHETHER the UI should run, and record the verdict in $MODDIR/.ui-choice
|
||||
# for the firmwareExe wrapper to act on.
|
||||
#
|
||||
# The decision is separated from the launching because the UI must run in the
|
||||
# FOREGROUND of the firmwareExe wrapper (so app_startup.sh's `ps | grep
|
||||
# firmwareExe` watchdog sees a live process). An init script cannot hold that
|
||||
# foreground, so it only chooses.
|
||||
#
|
||||
# That is also why this script uses none of anvil-service.sh's daemon helpers:
|
||||
# there is no daemon here, no pidfile and nothing to supervise. It takes the
|
||||
# common shape -- svc_say, svc_warn, svc_dispatch -- and nothing else.
|
||||
#
|
||||
# HelixScreen is the only UI: FlashForge's firmwareExe is retired and there is
|
||||
# nothing to fall back to. There is no crash counter and no automatic headless
|
||||
# latch -- the UI is started whenever it is installed and MOD_UI is not 0.
|
||||
# To stop it, set MOD_UI=0 in /usr/data/anvil/anvil.conf and reboot; ssh comes
|
||||
# from the stock /etc/init.d/S50dropbear, which busybox init ran long before
|
||||
# this script, so it is up regardless of what happens here, and so are
|
||||
# S60nginx/S62moonraker (Mainsail) and S70klipper.
|
||||
MODDIR=/usr/data/anvil
|
||||
[ -f $MODDIR/anvil.conf ] && . $MODDIR/anvil.conf
|
||||
# start/stop/status/liveness, shared with the other services. See
|
||||
# anvil-service.sh for the whole API.
|
||||
if [ -f $MODDIR/anvil-service.sh ]; then
|
||||
. $MODDIR/anvil-service.sh
|
||||
else
|
||||
echo "ui: !! no $MODDIR/anvil-service.sh -- the mod payload is not installed"
|
||||
exit 1
|
||||
fi
|
||||
SVC_NAME=ui
|
||||
|
||||
CHOICE=$MODDIR/.ui-choice
|
||||
|
||||
have_helix() { [ -x $MODDIR/helixscreen/bin/helix-launcher.sh ] || [ -x $MODDIR/helixscreen/bin/helix-screen ]; }
|
||||
|
||||
start() {
|
||||
want=helix
|
||||
|
||||
if [ "${MOD_UI:-1}" = 0 ]; then
|
||||
svc_say "MOD_UI=0 in anvil.conf -- staying headless"
|
||||
want=none
|
||||
fi
|
||||
|
||||
if [ "$want" = helix ] && ! have_helix; then
|
||||
svc_say "HelixScreen is not installed -- staying headless"
|
||||
want=none
|
||||
fi
|
||||
if [ "$want" = none ]; then
|
||||
svc_warn "printer will be headless -- no UI on the screen"
|
||||
# A continuation of the line above, so it is indented rather than
|
||||
# prefixed: the "ui: !!" that somebody greps for is on the warning.
|
||||
echo " ssh is still up via /etc/init.d/S50dropbear, and so is Mainsail"
|
||||
fi
|
||||
|
||||
echo "$want" > "$CHOICE"
|
||||
svc_say "chose $want"
|
||||
}
|
||||
|
||||
stop() {
|
||||
killall helix-screen helix-watchdog helix-splash 2>/dev/null
|
||||
svc_say "stopped"
|
||||
}
|
||||
|
||||
status() {
|
||||
svc_say "choice: `cat $CHOICE 2>/dev/null || echo unset`"
|
||||
svc_say "MOD_UI: ${MOD_UI:-1}"
|
||||
}
|
||||
|
||||
svc_dispatch "$@"
|
||||
+56
-23
@@ -63,15 +63,11 @@ if [ -n "$MODTAR" ]; then
|
||||
# nothing else.
|
||||
#
|
||||
# THE PROPERTY THIS HAS TO KEEP, which is why anything is removed at
|
||||
# all rather than just extracted over: init.d scripts used to be
|
||||
# overwritten in place and never removed, which was harmless only
|
||||
# while the set of filenames never changed. It does change: S60web was
|
||||
# split into S60nginx and S62moonraker, and on a printer that already
|
||||
# had the mod the old S60web survived the update and sat next to both
|
||||
# new scripts. firmwareExe runs every executable $MODDIR/init.d/S* in
|
||||
# filename order, so nginx and moonraker would each have been started
|
||||
# twice, by two scripts that disagree about where moonraker even
|
||||
# lives. The installed set must end up exactly the shipped set.
|
||||
# all rather than just extracted over: files used to be overwritten in
|
||||
# place and never removed, which is harmless only while the set of
|
||||
# filenames never changes. It does change -- a renamed script survives
|
||||
# the update and sits next to the one that replaced it. The installed
|
||||
# set must end up exactly the shipped set.
|
||||
#
|
||||
# HOW IT USED TO BE KEPT, and why that had to stop: this line was
|
||||
# rm -rf $MODDIR/bin $MODDIR/www $MODDIR/nginx ... $MODDIR/init.d
|
||||
@@ -153,20 +149,22 @@ if [ -n "$MODTAR" ]; then
|
||||
rm -rf $MODDIR/bin $MODDIR/www $MODDIR/nginx $MODDIR/helixscreen $MODDIR/config $MODDIR/moonraker $MODDIR/init.d
|
||||
echo "previous install removed (no manifest -- pre-manifest layout)"
|
||||
fi
|
||||
# init.d GETS THIS EVEN IN THE MANIFEST BRANCH, and unconditionally --
|
||||
# a diff against the last manifest only knows about files THAT
|
||||
# manifest tracked, so a script from further back than the last
|
||||
# install (planted by hand, restored from a backup, or left over from
|
||||
# the one pre-manifest jump every printer takes exactly once) is
|
||||
# invisible to it and survives forever. That is an acceptable gap for
|
||||
# $MODDIR/bin or $MODDIR/config, where something that is not ours
|
||||
# might legitimately live -- it is not acceptable here: firmwareExe
|
||||
# runs every executable $MODDIR/init.d/S* in filename order, so an
|
||||
# orphan does not just sit unused, it starts a second copy of
|
||||
# whatever it launches. Nothing outside the payload has any business
|
||||
# writing to this directory, so clearing it outright costs nothing a
|
||||
# real install would miss.
|
||||
rm -rf $MODDIR/init.d
|
||||
# init.d/ AND anvil-service.sh ARE GONE, and go unconditionally rather
|
||||
# than by manifest. The payload ships neither any more: s6-rc is the
|
||||
# CLI, and the tree is started by firmwareExe. A leftover S70klipper is
|
||||
# a script that starts an UNSUPERVISED klippy next to the supervised
|
||||
# one, and a leftover anvil-service.sh is a library something stale
|
||||
# could still source, so neither may be left to a diff that only knows
|
||||
# about files the LAST manifest tracked -- a script planted by hand, or
|
||||
# surviving the one pre-manifest jump every printer takes, is invisible
|
||||
# to that.
|
||||
#
|
||||
# No hot migration is attempted. The install runs from app_startup.sh
|
||||
# DURING BOOT, before firmwareExe starts, so there is no supervision
|
||||
# tree up while this runs and the new one comes up from scratch a
|
||||
# moment later. A `sh run.sh` typed over ssh instead is the exception:
|
||||
# that printer needs a reboot, and nothing here forces one.
|
||||
rm -rf $MODDIR/init.d $MODDIR/anvil-service.sh
|
||||
mkdir -p $MODDIR
|
||||
# Try xz first (FlashForge's own factory installer uses `xz -dc`, so
|
||||
# it exists), then fall back to plain tar in case a build shipped it
|
||||
@@ -204,6 +202,41 @@ if [ -n "$MODTAR" ]; then
|
||||
rm -rf $HELIX_KEEP
|
||||
fi
|
||||
chmod a+x $MODDIR/bin/* 2>/dev/null
|
||||
# ---- the s6 scandir, swept -------------------------------------
|
||||
# MEASURED: s6-rc-init creates one symlink per service in the scandir
|
||||
# and fails outright -- "unable to supervise service directories ...:
|
||||
# File exists" -- if a name is taken. A printer upgrading from the
|
||||
# pre-s6-rc payload has nginx, moonraker and camera in there as real
|
||||
# directories, and the manifest cannot remove them: it deletes the
|
||||
# files it listed, but s6-supervise created supervise/ and event/
|
||||
# inside each at RUNTIME, so the rmdir correctly refuses and leaves
|
||||
# exactly the name s6-rc-init collides with.
|
||||
#
|
||||
# After the extraction, so the payload's own empty etc/s6 has landed.
|
||||
rm -rf $MODDIR/etc/s6
|
||||
mkdir -p $MODDIR/etc/s6
|
||||
# /run is a tmpfs, so this matters only for a `sh run.sh` typed over
|
||||
# ssh: a live s6-rc state points at the database just replaced.
|
||||
rm -rf /run/s6-rc
|
||||
# klipperDaemon, replaced: stock's `start` forks a second, unsupervised
|
||||
# klippy beside the s6 one. It cannot ride in the software component --
|
||||
# FlashForge's run.sh copies a fixed list of files and klipperDaemon is
|
||||
# not on it -- so it is installed from here.
|
||||
#
|
||||
# KLIPPER_NICENESS is carried forward off the file being replaced, so
|
||||
# FlashForge's number survives without being guessed. On later updates
|
||||
# that reads our own copy, which is how the value persists.
|
||||
if [ -f $MODDIR/bin/klipperDaemon ] && [ -d /usr/prog/klipper ]; then
|
||||
KN=`sed -n 's/^ *KLIPPER_NICENESS= *\([-0-9][0-9]*\).*/\1/p' \
|
||||
/usr/prog/klipper/klipperDaemon 2>/dev/null | head -n 1`
|
||||
[ -n "$KN" ] || KN=0
|
||||
sed "s/^KLIPPER_NICENESS=.*/KLIPPER_NICENESS=$KN/" \
|
||||
$MODDIR/bin/klipperDaemon > /usr/prog/klipper/klipperDaemon.new \
|
||||
&& mv -f /usr/prog/klipper/klipperDaemon.new /usr/prog/klipper/klipperDaemon \
|
||||
&& chmod +x /usr/prog/klipper/klipperDaemon \
|
||||
&& echo "klipperDaemon: replaced (start is a no-op; niceness $KN)" \
|
||||
|| echo "!! could not replace klipperDaemon -- it can still fork a second klippy"
|
||||
fi
|
||||
echo "mod payload installed"
|
||||
# From here on this script runs the printer's own interpreter, so it
|
||||
# needs the same environment the boot path gets -- and it is a hand-run
|
||||
|
||||
+23
-73
@@ -1,90 +1,40 @@
|
||||
#!/bin/sh
|
||||
# /usr/prog/klipper/start.sh
|
||||
#
|
||||
# Kept as close to stock as possible. The web stack is NOT started here even
|
||||
# though stock has the (commented-out) lines for it -- that is the job of
|
||||
# /usr/data/anvil/init.d/S60nginx and /usr/data/anvil/init.d/S62moonraker, so
|
||||
# there is exactly one place that starts each of them and one place to restart
|
||||
# them from over ssh. They were a single S60web until the split; nginx and
|
||||
# moonraker fail independently and are now stopped and started independently.
|
||||
# Klippy is an s6-rc longrun, so this asks for the service and the graph brings
|
||||
# the MCU bring-up along. It is kept rather than deleted because it is a path
|
||||
# other things take -- FlashForge's own tooling knows this filename, and it is
|
||||
# in every set of notes anybody has written about this printer.
|
||||
#
|
||||
# Three changes from stock:
|
||||
# * klipper_pri.sh is actually invoked. FlashForge ships that script but
|
||||
# never calls it, so klippy runs at normal priority.
|
||||
# * ff_mcu_bringup.py hands the heat, eboard and level boards over from
|
||||
# their bootloaders. Stock never needed it here because firmwareExe did
|
||||
# all three itself; replacing firmwareExe left two of them stranded, and
|
||||
# the third was covered by checkEboard.
|
||||
# * checkEboard is no longer called. It is one function, hard wired to
|
||||
# /dev/ttyS5, and an older build of the routine ff_mcu_bringup.py already
|
||||
# reimplements -- one that treats ANY byte from that port as a bootloader
|
||||
# banner and so sends 'A' at an eboard already running Klipper. The
|
||||
# binary is still on the firmware partition; nothing runs it.
|
||||
# * the /tmp/uds idempotence guard below, so S70klipper's retry loop cannot
|
||||
# start a second klippy against the same MCU.
|
||||
|
||||
cmd_mcu write_firmware /usr/prog/libmcu-bare.bin
|
||||
cmd_mcu bootup
|
||||
sleep 2
|
||||
|
||||
# PATH, LD_LIBRARY_PATH and FF_PYTHON, from the one file that defines them.
|
||||
# This script is on the firmware partition and the env file is on the data
|
||||
# partition, which is the right way round: the list describes /usr/prog and
|
||||
# the mod owns it. See anvil-env.sh.
|
||||
# There is deliberately no direct launch left here. One that still ran
|
||||
# klipperDaemon would be a second way to start klippy: unsupervised, invisible
|
||||
# to s6-rc, and fighting the supervised copy for /dev/ttyS4.
|
||||
#
|
||||
# bin/patch.sh stages this as a software component and FlashForge's own run.sh
|
||||
# copies it onto /usr/prog/klipper, which is why it lives on the firmware
|
||||
# partition and why a stock flash replaces it.
|
||||
MODDIR=/usr/data/anvil
|
||||
|
||||
# This script is on the firmware partition and the env file is on the data
|
||||
# partition, which is the right way round: the list describes /usr/prog and the
|
||||
# mod owns it.
|
||||
if [ -f $MODDIR/anvil-env.sh ]; then
|
||||
. $MODDIR/anvil-env.sh
|
||||
else
|
||||
echo "start.sh: !! no $MODDIR/anvil-env.sh -- klippy will not find its libraries"
|
||||
fi
|
||||
|
||||
# Idempotence guard: init.d/S70klipper runs this script, and so does its own
|
||||
# retry loop. Running it twice would start a second klippy against the same
|
||||
# MCU.
|
||||
# /tmp/uds is klippy's API socket. The transition below would also be a no-op
|
||||
# on a running klippy, but it would not say so.
|
||||
if [ -S /tmp/uds ]; then
|
||||
echo "start.sh: klippy already running, nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# MCU bring-up. Each of these boards answers Klipper only after its
|
||||
# bootloader is told to start the application:
|
||||
#
|
||||
# /dev/ttyS2 mcu cmd_mcu bootup, above
|
||||
# /dev/ttyS4 eheaterboard ff_mcu_bringup.py <- was nobody's job
|
||||
# /dev/ttyS5 eboard ff_mcu_bringup.py <- was checkEboard
|
||||
# /dev/ttyS7 levelboard ff_mcu_bringup.py <- was nobody's job
|
||||
#
|
||||
# This runs on every klippy start, including the restarts S70klipper issues
|
||||
# when a board missed its window.
|
||||
#
|
||||
# $FF_PYTHON is the absolute path anvil-env.sh sets, not a `python3` lookup:
|
||||
# this is the one step Klipper cannot start without. It used to fall back to a
|
||||
# bare `python3` when the absolute path was missing, which never helped -- the
|
||||
# base rootfs ships no other interpreter, so that either resolved to the same
|
||||
# binary or to something untested. LD_LIBRARY_PATH still matters: this
|
||||
# interpreter does not start without it, which is exactly how moonrakerDaemon
|
||||
# used to fail.
|
||||
#
|
||||
# Test -f, not -x. The script is handed to the interpreter by path, so its
|
||||
# execute bit is irrelevant -- and it is not always set: the payload carries
|
||||
# mode 644 in git, and the +x only happens if the file arrived through
|
||||
# patch.sh or run-append.sh. A hand-copied file is perfectly runnable and
|
||||
# used to be skipped with a "missing" message while sitting right there.
|
||||
#
|
||||
# FF_SKIP_MCU_BRINGUP=1 says the caller has already done it. bin/ff-startup.py
|
||||
# sets that: it owns the boot sequence, so it does the bring-up itself, in
|
||||
# process, and then runs this script to launch klippy. Nothing else sets it,
|
||||
# so a start.sh run by hand over ssh -- or by S70klipper's fallback -- still
|
||||
# does the full job.
|
||||
if [ "${FF_SKIP_MCU_BRINGUP:-0}" = 1 ]; then
|
||||
echo "start.sh: MCU bring-up already done by the caller"
|
||||
elif [ -f /usr/data/anvil/bin/ff_mcu_bringup.py ]; then
|
||||
"$FF_PYTHON" /usr/data/anvil/bin/ff_mcu_bringup.py \
|
||||
|| echo "start.sh: MCU bring-up reported a problem ($?)"
|
||||
else
|
||||
echo "start.sh: ff_mcu_bringup.py missing -- the toolhead boards are not brought up"
|
||||
if [ ! -x $MODDIR/bin/s6-rc ]; then
|
||||
echo "start.sh: !! no $MODDIR/bin/s6-rc -- the mod payload is not installed"
|
||||
echo "start.sh: !! nothing here can start Klipper; flash the stock package to get it back"
|
||||
exit 1
|
||||
fi
|
||||
/usr/prog/klipper/klipperDaemon start
|
||||
|
||||
# Real-time priority for klippy (stock ships this but never runs it).
|
||||
sh /usr/prog/klipper/klipper_pri.sh >/dev/null 2>&1 &
|
||||
# The transition pulls mcu-bringup in first, because klipper depends on it.
|
||||
exec $MODDIR/bin/s6-rc -u change klipper
|
||||
@@ -370,8 +370,30 @@ def stopped(box, booted):
|
||||
"nothing was running to stop, so the assertions that depend on this "
|
||||
"would pass vacuously: %s" % before.text)
|
||||
box.svc("S40s6", MODDIR + "/init.d").stop()
|
||||
box.sh("sleep 2")
|
||||
return box
|
||||
|
||||
# A WAIT, NOT A SLEEP, and the distinction is the whole bug this line
|
||||
# replaces. `s6-svscanctl -t` asks the scanner to send its s6-supervise
|
||||
# children a TERM, reap them, and only then exit -- so how long the stop
|
||||
# takes is a function of how many services are UP, and after `booted`
|
||||
# there are three. `box.sh("sleep 2")` was enough when this module ran on
|
||||
# its own and not enough in a full `pytest qa` run, so the two tests below
|
||||
# failed intermittently while describing a printer that was fine: the
|
||||
# scanner really was on its way out, the fixture just handed them the
|
||||
# machine too early. That is the same class of defect as the stale pidfile
|
||||
# this suite has three comments about -- asking the clock a question only
|
||||
# the process table can answer.
|
||||
# Whole seconds: busybox `sleep` takes a fractional argument only when it
|
||||
# was built with FEATURE_FANCY_SLEEP, and a `sleep 0.25` this box rejects
|
||||
# would turn the bound below into a spin.
|
||||
for _ in range(12):
|
||||
if "not running" in box.svc("S40s6", MODDIR + "/init.d").status().text:
|
||||
return box
|
||||
box.sh("sleep 1")
|
||||
pytest.fail(
|
||||
"the scanner was still answering 12s after `S40s6 stop` -- either the "
|
||||
"stop verb does not wait for its supervisors, or one of them is "
|
||||
"refusing to die:\n%s"
|
||||
% box.svc("S40s6", MODDIR + "/init.d").status().text)
|
||||
|
||||
|
||||
class TestSupervisorLifecycle:
|
||||
|
||||
@@ -41,9 +41,16 @@ pytestmark = pytest.mark.static
|
||||
# Everything that must at least parse. bin/ and test/ run under bash on the
|
||||
# build image; payload/ runs under the printer's ash but bash parses it too,
|
||||
# and a file that does not parse under either is broken beyond dialect.
|
||||
SYNTAX_GLOBS = ("bin/*.sh", "payload/*.sh", "payload/init.d/S*",
|
||||
"payload/firmwareExe", "test/integration/printer/*.sh",
|
||||
"qa/replica/actions/*.sh")
|
||||
SYNTAX_GLOBS = ("bin/*.sh", "payload/*.sh", "payload/firmwareExe",
|
||||
"payload/bin/klipperDaemon",
|
||||
"payload/etc/s6-rc/source/*/run",
|
||||
"test/integration/printer/*.sh", "qa/replica/actions/*.sh")
|
||||
|
||||
# NOT the oneshots' `up`/`down`. Those are execline command lines, and the
|
||||
# shell they carry is inside a `/bin/sh -c "..."` argument -- `bash -n` and
|
||||
# shellcheck would both parse the wrapper, pass, and check none of the code
|
||||
# that actually runs. Extracting the body and checking THAT is a real test and
|
||||
# belongs in the replica lane beside the rest of the s6-rc assertions.
|
||||
|
||||
# The subset executed by the printer's busybox ash. bin/ and test/ are
|
||||
# deliberately absent: they run on the build image, where bash is the shell and
|
||||
@@ -55,8 +62,15 @@ SYNTAX_GLOBS = ("bin/*.sh", "payload/*.sh", "payload/init.d/S*",
|
||||
# hold.sh would fail at container start and read as "the replica is broken on
|
||||
# this machine" -- a harness failure wearing a machine failure's clothes, which
|
||||
# is the hardest kind to diagnose.
|
||||
ASH_GLOBS = ("payload/*.sh", "payload/init.d/S*", "payload/firmwareExe",
|
||||
"qa/replica/actions/*.sh")
|
||||
#
|
||||
# The s6-rc `run` scripts are here for the same reason and were missed until
|
||||
# phase 8 moved them: s6-supervise execs them on the printer, under the
|
||||
# printer's ash, and a bashism in one is a service that never starts and says
|
||||
# so only in s6's own log. They lived at payload/etc/s6/*/run before the move
|
||||
# and no glob covered them there either.
|
||||
ASH_GLOBS = ("payload/*.sh", "payload/firmwareExe",
|
||||
"payload/bin/klipperDaemon",
|
||||
"payload/etc/s6-rc/source/*/run", "qa/replica/actions/*.sh")
|
||||
|
||||
PY_GLOBS = ("bin/*.py", "payload/*.py", "payload/bin/*.py",
|
||||
"payload/klipper/extras/*.py", "test/*.py", "test/ffsim/*.py",
|
||||
|
||||
@@ -106,6 +106,31 @@ SKALIBS_SHA256="f9c905e74935c6fe911c7e344e3e89d5fbd2014c1a04650b524b15ce9b5635d1
|
||||
S6_VERSION="2.15.1.0"
|
||||
S6_SHA256="eab9c46e22b66b16135f9a05ec68a0ea287d9060b84d10defaaa2caad158ab52"
|
||||
|
||||
# execline and s6-rc, added by phase 8 of docs/notes/80-s6-migration.md, which
|
||||
# moves the boot order out of init.d filenames and into a compiled database.
|
||||
#
|
||||
# EXECLINE IS NOT OPTIONAL AND IT IS NOT A STYLE CHOICE. s6 alone is built
|
||||
# --disable-execline here and that stays true of the `run` scripts we write --
|
||||
# they are plain #!/bin/sh. But s6-rc GENERATES two service directories of its
|
||||
# own (s6rc-oneshot-runner and s6rc-fdholder) whose `run` scripts are execline,
|
||||
# and every oneshot's `up` executes through the first of them. So the moment
|
||||
# there is one oneshot in the database -- wifi is the first -- execline is on
|
||||
# the printer. Measured, not assumed: the generated servicedirs exec execlineb,
|
||||
# fdmove, pipeline, if, forstdin, exit and redirfd, which is seven of
|
||||
# execline's 53 binaries and not the 2.1MB set.
|
||||
#
|
||||
# THE PREFIX IS PART OF THE ABI HERE, one level worse than s6-ftrigrd's. Both
|
||||
# packages are configured --prefix=$MODDIR: execline bakes its --shebangdir
|
||||
# (defaulting to $bindir) into EXECLINE_SHEBANGPREFIX, and s6-rc-compile pastes
|
||||
# that string into the #! line of the oneshot runner it generates. So the path
|
||||
# that ends up INSIDE the shipped database is a property of the execline the
|
||||
# COMPILER was linked against, not of the execline we ship. Get it wrong and
|
||||
# every oneshot fails at exec with ENOENT while every longrun keeps working.
|
||||
EXECLINE_VERSION="2.9.9.2"
|
||||
EXECLINE_SHA256="908ed4db3a6b3a23a205d8fd4cf2a71089156f2aeae0f54656045aafad2dee32"
|
||||
S6RC_VERSION="0.7.0.0"
|
||||
S6RC_SHA256="bf5b8ce0da5a4ee70d642b818b61d9916a7a9b64a457595f388113e54a188688"
|
||||
|
||||
# The musl cross-toolchain that builds s6: mipsel-buildroot-linux-musl, from
|
||||
# Bootlin's toolchain project (https://toolchains.bootlin.com), NOT musl.cc.
|
||||
# NOT the Ingenic toolchain above either -- that one is glibc, and glibc is
|
||||
|
||||
Reference in new issue
Block a user