Let Dev Updater supervise the backend instead of shipping a script
dev-updater now carries a built-in service implementation, generated from a template and driven through the identical interface a project-supplied script uses, so a project whose service is unremarkable no longer writes one. ai-app's was unremarkable: `ExecStart=$BINARY` and `command="$BINARY"` with no arguments and no environment. 233 lines of it, and the half that matters most -- the OpenRC branch, which neither project can exercise from a systemd machine -- existed twice, so a fix found by testing would have had two places to land and no way to notice the second. The field keeps its name; `Managed` takes the command, resolved against the component's `cwd`. The one thing the script said that the built-in cannot is kept, in AGENTS.md rather than lost: Stop on this card takes down the server a phone reaches through the tunnel, while Dev Updater itself is unaffected because it uses its own port -- which is exactly what makes that button easy to press and easy to regret. NOT SAFE TO PULL BLIND. A managed service is named after the component, so this one becomes `app-backend` while the installed one still has the name the script gave it. Uninstall from the backend card *before* taking this change, then Install after; pulling first orphans a service that stays enabled and starts at boot with nothing pointing at it.
This commit is contained in:
1 parent
2c925a679f
commit
c2dfaab349
3 files changed
+26
-240
No files matched your search
+11
-3
@@ -18,9 +18,17 @@ components: [
|
|||||||
name: "backend",
|
name: "backend",
|
||||||
build: "cargo build --release",
|
build: "cargo build --release",
|
||||||
cwd: "server",
|
cwd: "server",
|
||||||
// Installs into whichever user-service manager is here, and is how
|
// Dev Updater's own built-in service implementation, generated
|
||||||
// ai-server is started, stopped and asked about. See the script.
|
// into its data directory and driven through the same interface a
|
||||||
service: "server/service",
|
// project-supplied script would be. ai-app carried a script of its
|
||||||
|
// own until 2026-08-28; it ran `target/release/ai-server` with no
|
||||||
|
// arguments and no environment, which is the generic case exactly,
|
||||||
|
// so it was two copies of one thing and the copy that could not be
|
||||||
|
// tested from here -- the OpenRC branch -- was duplicated with it.
|
||||||
|
//
|
||||||
|
// Resolved against this component's `cwd`, so this is
|
||||||
|
// `server/target/release/ai-server`.
|
||||||
|
service: Managed("target/release/ai-server"),
|
||||||
),
|
),
|
||||||
Apk(
|
Apk(
|
||||||
name: "app",
|
name: "app",
|
||||||
|
|||||||
@@ -48,10 +48,21 @@ repo is in PLAN.md's "Backend layout" section.
|
|||||||
event model mirror; `ServerConfig.kt` settings + Keystore-sealed token;
|
event model mirror; `ServerConfig.kt` settings + Keystore-sealed token;
|
||||||
screens in `SessionListScreen/SessionScreen/SpawnScreen/SettingsScreen`.
|
screens in `SessionListScreen/SessionScreen/SpawnScreen/SettingsScreen`.
|
||||||
- `.dev-updater.ron` — what Dev Updater is asked to do with this checkout:
|
- `.dev-updater.ron` — what Dev Updater is asked to do with this checkout:
|
||||||
the backend (built in `server/`, installed and controlled through
|
the backend (built in `server/`, run as `service: Managed(...)`) and then
|
||||||
`server/service`) and then the APK (built in `app/`), in that order. The
|
the APK (built in `app/`), in that order. The project it serves is the
|
||||||
project it serves is the repository, not either half of it, which is why
|
repository, not either half of it, which is why this sits at the root
|
||||||
this sits at the root rather than in `app/`.
|
rather than in `app/`.
|
||||||
|
`Managed` means Dev Updater supervises `ai-server` with its own built-in
|
||||||
|
service implementation rather than a script kept here. ai-app had such a
|
||||||
|
script until 2026-08-28 and it was the generic case exactly — no
|
||||||
|
arguments, no environment — so the two projects were maintaining one
|
||||||
|
behaviour twice, including the OpenRC branch neither can test from a
|
||||||
|
systemd machine.
|
||||||
|
Worth knowing before pressing it: **Stop** on the backend card stops the
|
||||||
|
server that a phone reaches through the tunnel, so on that phone it stays
|
||||||
|
down until someone starts it again from Dev Updater. Dev Updater reaches
|
||||||
|
it over its own port and is unaffected, which is what makes the button
|
||||||
|
safe to press and easy to regret.
|
||||||
- `wg-app-link/` — a **git submodule**, and the half of this backend that
|
- `wg-app-link/` — a **git submodule**, and the half of this backend that
|
||||||
dev-updater also needed: the pinned CA and leaf (`certs`), QR enrollment
|
dev-updater also needed: the pinned CA and leaf (`certs`), QR enrollment
|
||||||
and the bearer token (`enroll`), wg0 binding and the certificate's SANs
|
and the bearer token (`enroll`), wg0 binding and the certificate's SANs
|
||||||
|
|||||||
-233
@@ -1,233 +0,0 @@
|
|||||||
#!/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
|
|
||||||
}
|
|
||||||
|
|
||||||
# Whether the service fell over, as opposed to being stopped on purpose.
|
|
||||||
#
|
|
||||||
# OpenRC prints `crashed` *and* exits non-zero for this, so the word is
|
|
||||||
# read rather than the exit code -- leaning on the code would report
|
|
||||||
# "couldn't check", which is a different and less useful thing to say.
|
|
||||||
# The `running` check above stays on its exit code, which already worked
|
|
||||||
# and does not depend on wording.
|
|
||||||
crashed() {
|
|
||||||
case "$MANAGER" in
|
|
||||||
systemd) systemctl --user --quiet is-failed "$NAME" ;;
|
|
||||||
openrc) rc-service --user "$NAME" status 2>/dev/null | grep -qw crashed ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
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" <<UNIT
|
|
||||||
[Unit]
|
|
||||||
Description=$NAME (installed by $SCRIPT_DIR/service)
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
ExecStart=$BINARY
|
|
||||||
Restart=on-failure
|
|
||||||
WorkingDirectory=$SCRIPT_DIR
|
|
||||||
# Appended rather than truncated: the unit is not the thing that decides
|
|
||||||
# when a log starts over, since the start subcommand rotates. Restarting
|
|
||||||
# inside one run (Restart=on-failure) then keeps the whole story rather
|
|
||||||
# than erasing the reason. Needs systemd 240+.
|
|
||||||
#
|
|
||||||
# No backticks in this heredoc: the delimiter is unquoted so that \$LOG
|
|
||||||
# expands, which means a backtick would run as command substitution while
|
|
||||||
# the unit is being written.
|
|
||||||
StandardOutput=append:$LOG
|
|
||||||
StandardError=append:$LOG
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
||||||
UNIT
|
|
||||||
systemctl --user daemon-reload
|
|
||||||
systemctl --user enable "$NAME" >/dev/null
|
|
||||||
;;
|
|
||||||
openrc)
|
|
||||||
mkdir -p "$(dirname "$OPENRC_UNIT")"
|
|
||||||
cat > "$OPENRC_UNIT" <<UNIT
|
|
||||||
#!/sbin/openrc-run
|
|
||||||
name="$NAME"
|
|
||||||
description="$NAME (installed by $SCRIPT_DIR/service)"
|
|
||||||
command="$BINARY"
|
|
||||||
command_background=true
|
|
||||||
directory="$SCRIPT_DIR"
|
|
||||||
pidfile="\${XDG_RUNTIME_DIR}/$NAME.pid"
|
|
||||||
# command_background discards output otherwise, which is why a crash left
|
|
||||||
# nothing to read.
|
|
||||||
output_log="$LOG"
|
|
||||||
error_log="$LOG"
|
|
||||||
UNIT
|
|
||||||
chmod +x "$OPENRC_UNIT"
|
|
||||||
rc-update --user add "$NAME" >/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
|
|
||||||
elif case "$MANAGER" in
|
|
||||||
systemd) systemctl --user --quiet is-active "$NAME" ;;
|
|
||||||
openrc) rc-service --user "$NAME" status >/dev/null 2>&1 ;;
|
|
||||||
esac then
|
|
||||||
echo running
|
|
||||||
elif crashed; then
|
|
||||||
# Before `stopped`, because a crashed service satisfies
|
|
||||||
# neither of the other two and would otherwise be reported as
|
|
||||||
# a state somebody chose.
|
|
||||||
echo failed
|
|
||||||
else
|
|
||||||
echo stopped
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "usage: $0 install|uninstall|start|stop|restart|status|logs" >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
Reference in new issue
Block a user