#!/bin/sh # Installs and controls ai-server as a user service, for Dev Updater to # drive from the phone -- and for you to drive by hand, which is the same # thing. # # Declared as the Server component of this checkout in `.dev-updater.ron` # at the repository root, so the card drives it like any other project's. # One button is worth knowing about before pressing it: on a phone that # reaches ai-server through the tunnel rather than through Dev Updater, # Stop leaves this server down until someone starts it again here. # # ./service install | uninstall | start | stop | restart | status | logs # # `status` prints exactly one of `running`, `stopped`, `failed` or # `not-installed` # and exits 0. Anything else it prints, or any non-zero exit, means it # could not tell -- which the card shows as "couldn't check" rather than # as a service that is down. # # Nothing here ever prompts. Dev Updater runs this with stdin closed and no # terminal, so a `sudo` password prompt would not fail, it would hang until # the timeout with the card stuck mid-action. Anything needing root exits # with a message telling you to run it yourself once instead. # # User services on purpose: a system unit needs root to install, and a # build machine's own account is where a dev server belongs. Note that a # user service stops at logout unless lingering is enabled # (`loginctl enable-linger $USER`), which you want for this one: the phone # reaches this server whether or not anyone is logged in at the desk. set -eu SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) NAME=ai-server BINARY="$SCRIPT_DIR/target/release/$NAME" # Where this service's output goes, and the one generation kept behind it. # # Under $XDG_DATA_HOME rather than the checkout: a log is generated data, # it outlives any one build, and the repository is shared with a machine # that should not be able to read it. Rotated on start rather than by size # or age, so what is kept is exactly "this run and the one before" -- which # is the pair worth having after a crash and a restart, and is the reason # the file is not simply appended to forever. LOG_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/$NAME" LOG="$LOG_DIR/$NAME.log" LOG_PREVIOUS="$LOG.1" # Which init system is here, decided by asking rather than by looking for a # binary: a machine can carry both, and an OpenRC older than 0.60 has # rc-service but no --user at all. detect() { if command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then echo systemd elif command -v rc-service >/dev/null 2>&1 && rc-service --user --help >/dev/null 2>&1; then echo openrc else echo none fi } MANAGER=$(detect) if [ "$MANAGER" = none ]; then echo "No user-service manager here: this needs systemd with a user bus," >&2 echo "or OpenRC 0.60+ (older ones have no --user). Run $NAME by hand." >&2 exit 1 fi # OpenRC keeps user-service state under XDG_RUNTIME_DIR and refuses without # it. Worth saying plainly: from a server started outside a login session # it can be unset, and the failure otherwise reads as a broken service # rather than a missing variable. if [ "$MANAGER" = openrc ] && [ -z "${XDG_RUNTIME_DIR:-}" ]; then echo "XDG_RUNTIME_DIR is unset, and OpenRC stores user-service state in it." >&2 echo "Set it at login (elogind or pam_xdg) and try again." >&2 exit 1 fi SYSTEMD_UNIT="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/$NAME.service" OPENRC_UNIT="${XDG_CONFIG_HOME:-$HOME/.config}/rc/init.d/$NAME" installed() { case "$MANAGER" in systemd) [ -f "$SYSTEMD_UNIT" ] ;; openrc) [ -f "$OPENRC_UNIT" ] ;; esac } # What OpenRC's `status` exit code means. Measured on OpenRC 0.63.3 in a # guest built for the purpose, not read from documentation: # # 0 started 3 stopped 32 crashed # 1 could not find out # # 1 covers every way the question cannot be answered -- an unknown # service, XDG_RUNTIME_DIR unset, or a user softlevel that was never # initialised ("openrc did not boot this system"). Distinguishing it is # the whole reason to read the code rather than the text. # # And the text must not be read. OpenRC prints `* status: crashed` to # **stderr**, so the obvious `status 2>/dev/null | grep -qw crashed` # throws away precisely the word it is searching for, finds nothing, and # falls through to `stopped` -- reporting a service that fell over as one # somebody chose to stop. That was this script's bug until it was # measured, and the same shape is worth checking wherever a status is # parsed rather than counted. OPENRC_STARTED=0 OPENRC_STOPPED=3 OPENRC_CRASHED=32 require_binary() { [ -x "$BINARY" ] && return 0 echo "No built server at $BINARY -- build it first." >&2 exit 1 } do_install() { require_binary mkdir -p "$LOG_DIR" case "$MANAGER" in systemd) mkdir -p "$(dirname "$SYSTEMD_UNIT")" cat > "$SYSTEMD_UNIT" </dev/null ;; openrc) mkdir -p "$(dirname "$OPENRC_UNIT")" cat > "$OPENRC_UNIT" </dev/null ;; esac } do_uninstall() { installed || return 0 case "$MANAGER" in systemd) systemctl --user disable --now "$NAME" >/dev/null 2>&1 || true rm -f "$SYSTEMD_UNIT" systemctl --user daemon-reload ;; openrc) rc-service --user "$NAME" stop >/dev/null 2>&1 || true rc-update --user del "$NAME" >/dev/null 2>&1 || true rm -f "$OPENRC_UNIT" ;; esac } # Keeps the finished run and starts a fresh file for the next one. rotate() { mkdir -p "$LOG_DIR" [ -f "$LOG" ] && mv -f "$LOG" "$LOG_PREVIOUS" : > "$LOG" } control() { installed || { echo "$NAME is not installed" >&2; exit 1; } case "$MANAGER" in systemd) systemctl --user "$1" "$NAME" ;; openrc) rc-service --user "$NAME" "$1" ;; esac } case "${1:-}" in install) do_install ;; uninstall) do_uninstall ;; start | restart) # Rotated before the manager is asked, so the file the service # opens is the new one. rotate control "$1" ;; stop) control stop ;; logs) # One path per line, newest first, and nothing else: the caller # wants somewhere to read from, not a formatted report. Printing # nothing at all is the answer for a service with no log yet, # which reads as "not supported here" and costs nobody anything. [ -f "$LOG" ] && echo "$LOG" [ -f "$LOG_PREVIOUS" ] && echo "$LOG_PREVIOUS" exit 0 ;; status) if ! installed; then echo not-installed exit 0 fi case "$MANAGER" in systemd) if systemctl --user --quiet is-active "$NAME"; then echo running # Before `stopped`, because a service that fell over # satisfies neither of the other two and would otherwise # be reported as a state somebody chose. elif systemctl --user --quiet is-failed "$NAME"; then echo failed else echo stopped fi ;; openrc) # `|| code=$?` rather than a bare call: `set -e` is on, and # every answer except "running" is a non-zero exit, so a # plain invocation kills the script before it can say what # the code meant. code=0 rc-service --user "$NAME" status >/dev/null 2>&1 || code=$? case $code in "$OPENRC_STARTED") echo running ;; "$OPENRC_CRASHED") echo failed ;; "$OPENRC_STOPPED") echo stopped ;; # Anything else is "could not find out", which is a # real state and not one of the other three. Saying so # costs a non-zero exit and buys the caller the truth; # guessing `stopped` here is what the contract's # `failed` state exists to stop. *) echo "could not ask OpenRC about $NAME -- is" \ "XDG_RUNTIME_DIR set and the user softlevel" \ "initialised?" >&2 exit 1 ;; esac ;; esac ;; *) echo "usage: $0 install|uninstall|start|stop|restart|status|logs" >&2 exit 2 ;; esac