#!/bin/sh
# /usr/prog/PROGRAM/software/firmwareExe  -- REPLACED by the mod.
#
# The stock boot chain is:
#     busybox init -> /etc/init.d/rcS -> S99factory_test_shell
#         -> /usr/prog/app_startup.sh -> firmwareExe
# 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.
#
# 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"`.
#
# If this script is ever removed, app_startup.sh restores a firmwareExe from a
# version directory -- 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

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. 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/payload.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 ---------------------------------
# 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() {
    [ -f $MODDIR/bin/ffscreen.py ] || return 0
    "$FF_PYTHON" $MODDIR/bin/ffscreen.py "$@" 2>/dev/null || true
}
splash --title "REFORGE" --status "STARTING THE PRINTER" --progress 0.02

# ---- 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
# No payload ships the scandir and runFirmwareExe.sh wipes $MODDIR on every
# update, so this is where it comes back. 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=$!

# 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 an update
# cannot leave behind, since it deletes $MODDIR whole. /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.
# EVERY s6-rc CALL HERE CARRIES -t, and it is not tuning. s6-rc and
# s6-rc-init default to TAIN_INFINITE_RELATIVE, and skalibs converts a
# deadline to a struct timeval before select() -- which cannot represent
# infinity on a 32-bit time_t. glibc 2.29 on mips32 has one, so the call fails
# immediately with EOVERFLOW ("Value too large for defined data type") before
# any service is touched. musl has a 64-bit time_t on every architecture and
# never shows this, which is why it appeared the day the supervision stack
# moved to the printer's own glibc toolchain.
S6RC_T=30000        # laying down servicedirs: local work, generous
S6RC_BOOT_T=300000  # the boot transition: bounded by the slowest service

if "$S6BIN/s6-rc" -t "$S6RC_T" -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" -t "$S6RC_T" -c "$S6DB" "$SCANDIR"; then
    echo "s6-rc: !! could not initialise -- this printer has no services"
    while true; do sleep 3600; done
fi

# ---- one transition, and it is the whole boot ----------------------------
#
#     mcu-bringup -> klipper -> ff-startup -> ui
#                                  ^
#                              moonraker
#
# 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, whole. Every service in it is brought up
# because every component is installed -- there is no per-service switch to
# read, and nothing to read one from. A printer that should not run a component
# does not have it: `apk del`, or a stock FlashForge package.
#
# mcu-bringup is deliberately not in the bundle -- it arrives as klipper's
# dependency, which is the point of having one.
BOOT_SET=ok-all

# 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" -t "$S6RC_BOOT_T" -u change $BOOT_SET \
    || echo "s6-rc: !! at least one service did not come up (see above)"

# ---- 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"
