#!/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-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.
# The nice value protects klippy from this process. Modest on purpose: this is
# the print path -- HelixScreen and Mainsail both start jobs through it -- so
# starving it delays the UI, not just background work. 0 would be normal.
exec nice -n 5 "$FF_PYTHON" "$MOONRAKER_MAIN" -d "$MOONRAKER_DATA" 3>&-
