From c2dfaab3492bbb4f88564c22a78fe0e6cf22f2ee Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Fri, 28 Aug 2026 17:35:27 -0400 Subject: [PATCH] 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. --- .dev-updater.ron | 14 ++- AGENTS.md | 19 +++- server/service | 233 ----------------------------------------------- 3 files changed, 26 insertions(+), 240 deletions(-) delete mode 100755 server/service diff --git a/.dev-updater.ron b/.dev-updater.ron index 1c249c0..54f3613 100644 --- a/.dev-updater.ron +++ b/.dev-updater.ron @@ -18,9 +18,17 @@ components: [ name: "backend", build: "cargo build --release", cwd: "server", - // Installs into whichever user-service manager is here, and is how - // ai-server is started, stopped and asked about. See the script. - service: "server/service", + // Dev Updater's own built-in service implementation, generated + // into its data directory and driven through the same interface a + // 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( name: "app", diff --git a/AGENTS.md b/AGENTS.md index f8e4013..971bdf3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,10 +48,21 @@ repo is in PLAN.md's "Backend layout" section. event model mirror; `ServerConfig.kt` settings + Keystore-sealed token; screens in `SessionListScreen/SessionScreen/SpawnScreen/SettingsScreen`. - `.dev-updater.ron` — what Dev Updater is asked to do with this checkout: - the backend (built in `server/`, installed and controlled through - `server/service`) and then the APK (built in `app/`), in that order. The - project it serves is the repository, not either half of it, which is why - this sits at the root rather than in `app/`. + the backend (built in `server/`, run as `service: Managed(...)`) and then + the APK (built in `app/`), in that order. The project it serves is the + repository, not either half of it, which is why this sits at the root + 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 dev-updater also needed: the pinned CA and leaf (`certs`), QR enrollment and the bearer token (`enroll`), wg0 binding and the certificate's SANs diff --git a/server/service b/server/service deleted file mode 100755 index 7a48358..0000000 --- a/server/service +++ /dev/null @@ -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" </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 - 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