#!/bin/sh
# appctl — control the service in this AWD gamebox.
#
#   appctl status    is it running, and on the source that is on disk now?
#   appctl start     start it from the source as it is now
#   appctl stop      stop it
#   appctl restart   stop, then start from the source as it is now
#   appctl logs [N]  the last N lines of its output (default 50)
#   appctl passwd P  rotate the "open" SSH shell's root password to P
#                    (classic-AWD open exposure only, #1725) — the first
#                    defensive act on a box every team could walk into.
#                    Rivals holding the old password cannot make a NEW
#                    login afterwards; a session already open persists
#                    until it disconnects.
#
# ⚠️ **Nothing restarts the service for you.** PID 1 (`ctfy-appd`) starts
# it once when the container starts and then only does what this command
# asks: it never watches the source and never restarts a service that
# died. So an edit does nothing until `appctl restart`, and a service that
# dies — a bad edit, or an attacker crashing it — stays down, losing SLA
# every round, until you `appctl start` it.
#
# The source this box shipped with stays in the image (`appctl status`
# prints where): copy it back and restart to undo a bad edit.
set -u
RUN=/run/app
LOG=/var/log/app.log

state() { cat "$RUN/$1" 2>/dev/null || true; }

alive() {
    # A zombie answers `kill -0`, so the process state is read instead.
    [ -n "$1" ] && [ -r "/proc/$1/stat" ] || return 1
    st=$(sed 's/.*) //' "/proc/$1/stat" | cut -d' ' -f1)
    [ "$st" != Z ] && [ "$st" != X ]
}

stamp() { stat -c %Y "$(state src)" 2>/dev/null || echo 0; }

# One request to PID 1, one answer. PID 1 owns the service so that it is
# PID 1's child: reaped when it dies, and still running after this shell
# exits.
ask() {
    rm -f "$RUN/reply"
    echo "$1" >"$RUN/request"
    kill -USR1 1
    i=0
    while [ ! -f "$RUN/reply" ] && [ "$i" -lt 120 ]; do
        sleep 0.5
        i=$((i + 1))
    done
    if [ ! -f "$RUN/reply" ]; then
        echo "appctl: the process manager did not answer" >&2
        return 1
    fi
    reply=$(cat "$RUN/reply")
    rm -f "$RUN/reply"
    if [ "$reply" != ok ]; then
        echo "appctl: $reply" >&2
        return 1
    fi
}

build_failed() {
    # A compiled service keeps its last good build. Say so, so that
    # "running" never reads as "your edit is live".
    log=$(state buildlog)
    [ -n "$log" ] && [ -s "$log" ]
}

status() {
    pid=$(state pid)
    if alive "$pid"; then
        echo "running   pid $pid"
    else
        echo "STOPPED   the service is not running — appctl start (appctl logs says why)"
    fi
    echo "source    $(state src)"
    echo "pristine  $(state pristine)"
    if alive "$pid" && [ "$(stamp)" != "$(state stamp)" ]; then
        echo "the source changed after this process started — appctl restart to load it"
    fi
    if build_failed; then
        echo "the last build FAILED, so the previous build is what runs:"
        cat "$(state buildlog)"
    fi
    alive "$pid"
}

started() {
    new=$(state pid)
    # ⚠️ The same pid must survive a few seconds: a service that dies on
    # start was alive for a moment, and that is not "started".
    sleep 3
    if ! alive "$new"; then
        echo "appctl: the service exited right after starting — it is DOWN. Last lines:" >&2
        tail -n 20 "$LOG" >&2
        return 1
    fi
    if build_failed; then
        echo "appctl: running pid $new, but the build FAILED, so it is the previous build:" >&2
        cat "$(state buildlog)" >&2
        return 1
    fi
    echo "running   pid $new"
}

case "${1:-status}" in
    status) status ;;
    start)
        if alive "$(state pid)"; then
            echo "already running (pid $(state pid)) — appctl restart to reload the source"
            exit 0
        fi
        ask start && started
        ;;
    stop) ask stop && echo "stopped" ;;
    restart) ask stop && ask start && started ;;
    logs) tail -n "${2:-50}" "$LOG" ;;
    passwd)
        new="${2:-}"
        if [ -z "$new" ]; then
            echo "usage: appctl passwd <new-password>" >&2
            exit 2
        fi
        if ! command -v chpasswd >/dev/null 2>&1; then
            echo "appctl: chpasswd is not available — this box has no open SSH shell to rotate" >&2
            exit 1
        fi
        if printf 'root:%s\n' "$new" | chpasswd 2>/dev/null; then
            echo "root password rotated. A rival's existing session persists until it"
            echo "disconnects, but no new login works without the new password."
        else
            echo "appctl: could not rotate the root password" >&2
            exit 1
        fi
        ;;
    *)
        echo "usage: appctl status | start | stop | restart | logs [N] | passwd <pw>" >&2
        exit 2
        ;;
esac
