#!/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 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 goes to a shell that ignores it.
#
# THIS FILE DOES NOT LOOK AT MOD_WEB: a `run` that exits because it disagrees
# is a respawn loop with extra steps. There is no `down` file either --
# MEASURED: s6-rc-compile accepts one and SILENTLY DROPS it. The gate is the
# argument list; svc_rc_enabled reads MOD_WEB at runtime.
MODDIR=/usr/data/anvil

# Read HERE, not inherited: s6-supervise starts this from the SCANNER's
# environment, which is firmwareExe's at boot but whatever an ssh session had
# when the service is restarted by hand.
[ -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
# The mod's data path, not /usr/data itself. Moonraker's config directory is
# <data path>/config and nothing configures that separately -- file_manager
# registers the "config" root off the data path -- so the data path is what
# decides whether the mod writes into FlashForge's /usr/data/config. It must
# not: that directory is what a printer flashed back to stock boots from, and
# nothing of ours runs during a stock flash to undo anything left in it.
#
# This is the same directory klippy's printer.cfg is in, which is also what
# Mainsail's config editor lists and what moonraker compares klippy's config
# file against -- one config directory, one answer everywhere.
MOONRAKER_DATA=/usr/data/anvil-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 execs this only after the previous
# copy was reaped.
rm -rf $MOONRAKER_TMP
mkdir -p $MOONRAKER_TMP $MOONRAKER_DATA /usr/data/logs /usr/data/gcodes
export TMPDIR=$MOONRAKER_TMP

# ---- the data path ---------------------------------------------------------
#
# THE TWO FOLDERS THAT DO NOT MOVE WITH IT. Prints live in /usr/data/gcodes --
# that is klippy's [virtual_sdcard] path and where the stock application put
# them -- and every log on this printer is in /usr/data/logs. Moving either
# would strand what is already there; only the CONFIG had a reason to move.
#
# Symlinks and not a mount or a copy, because moonraker resolves them:
# file_manager.register_directory() calls realpath() on a root that is a link,
# so Mainsail lists the real directory, and the gcode path it compares against
# klippy's is the resolved one -- which is how `validate_gcode_path` stays
# quiet with klippy configured for /usr/data/gcodes.
[ -e $MOONRAKER_DATA/gcodes ] || ln -s /usr/data/gcodes $MOONRAKER_DATA/gcodes
[ -e $MOONRAKER_DATA/logs ]   || ln -s /usr/data/logs   $MOONRAKER_DATA/logs

# 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. Delete both once no pre-s6 install can
# be upgraded in the field.
rm -f /run/moonraker.pid

# A moonraker already holding :7125 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.
# The match is the absolute path of the entry point, which appears in a
# moonraker's command line and in no other process here.
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

# ---- the database ----------------------------------------------------------
#
# It moved with the data path. A printer that ran an older release has one
# under /usr/data -- job history, Mainsail's own stored settings -- and it is
# carried across once rather than started empty.
#
# HERE, BELOW THE STRAY-KILLER, and not in the installer or a package script:
# this is the one point where moonraker is provably not running. s6-supervise
# execs this script only after the previous copy was reaped, and the loop above
# has just killed an unsupervised leftover. Renaming a live lmdb out from under
# a running moonraker -- which is what doing this from an `apk upgrade` would
# mean -- is a corrupt database, not a migration.
if [ -d /usr/data/database ] && [ ! -e $MOONRAKER_DATA/database ]; then
    if mv /usr/data/database $MOONRAKER_DATA/database; then
        echo "moonraker: database moved to $MOONRAKER_DATA/database"
    fi
fi
# Moonraker's own identity for this machine, written beside the database. It is
# regenerated when missing, so this is worth moving and never worth failing on.
if [ -f /usr/data/.moonraker.uuid ] && [ ! -e $MOONRAKER_DATA/.moonraker.uuid ]; then
    mv /usr/data/.moonraker.uuid $MOONRAKER_DATA/.moonraker.uuid 2>/dev/null
fi

# ---- readiness ------------------------------------------------------------
#
# `notification-fd` holds "3": the service is READY once a line is written to
# fd 3, and ff-startup depends on that meaning 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: field 2 is
# LOCAL_ADDRESS:PORT in uppercase hex, field 4 the state, 0A is TCP_LISTEN.
# The address half is not matched, so host: 127.0.0.1 still counts.
#
# The port is read from moonraker's own config because it is user-editable and
# a probe watching the wrong port would never fire. The awk is section-aware.
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. The exec below closes fd 3 in MOONRAKER
# (3>&-) so the only process holding the notification pipe is the one with
# something to say.
#
# 180s: moonraker imports its whole component tree on a 1GHz mipsel box off
# eMMC, and slower again on the qemu replica.
(
    _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` is moonraker's data path, set above. Not optional: moonraker defaults
# to ~/printer_data, a home directory that does not exist here. There is no
# `-c`: the config file follows the data path, which is the point of moving
# it -- a `-c` alone would move the file moonraker READS while file_manager
# kept registering FlashForge's directory as the config root.
#
# 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, so this is still one
# process. The nice value protects klippy from it, and is modest on purpose:
# this is the print path, so starving it delays the UI, not just background
# work.
exec nice -n 5 "$FF_PYTHON" "$MOONRAKER_MAIN" -d "$MOONRAKER_DATA" 3>&-
