Commit Graph
6 Commits
Author SHA1 Message Date
iris a8b6c13b01 Report a crashed service as failed, which OpenRC was never telling us
The OpenRC branch of this script had never run anywhere. A guest built to
reproduce the host says it was wrong in the way the `failed` state exists
to prevent: a service that fell over reported `stopped`, which reads as a
decision somebody made.

Two causes, both measured rather than reasoned about.

`rc-service status` prints `* status: crashed` to **stderr**. The check
was `status 2>/dev/null | grep -qw crashed`, which discards precisely the
word it is searching for, finds nothing, and falls through to `stopped`.
The old comment argued for reading the word rather than the exit code, and
that argument was sound except that the code turns out to be specific
rather than merely non-zero.

So it now reads the code, which says more than the text did: 0 started,
3 stopped, 32 crashed, and 1 for every way the question cannot be
answered -- an unknown service, XDG_RUNTIME_DIR unset, or a user softlevel
that was never initialised. That last group is a real state and not one of
the other three, so it exits non-zero and says so instead of guessing.

`set -e` is the second cause, found by running the first fix: every answer
except "running" is a non-zero exit, so a bare invocation killed the
script before the code could be looked at. It prints nothing and exits 3,
which is indistinguishable from a crash of this script itself.

Verified in the guest, all four states: not-installed, stopped, failed
after a real crash, and a non-zero exit with nothing on stdout when the
softlevel is removed. Before the fix the crashed case printed `stopped`.
2026-08-28 18:55:05 -04:00
iris d5a0f67a3a Put the service script back until the switch can be sequenced
Reverts the switch to `service: Managed(...)`. The switch is still right
and the reasoning in that commit still holds; what was wrong was doing it
now, unilaterally, to a checkout something is reading live.

A dev-updater is running against this working tree, so deleting
`server/service` did not wait for a pull to take effect -- the backend
card went to "couldn't check -- failed to run the service script: No such
file or directory" immediately, and the pushed declaration still names the
script, so the tree and the declaration disagreed in the one direction
that breaks things. My own commit message had said this change was not
safe to pull blind; it turned out not to need a pull at all.

The switch needs three steps in order, and only the middle one is mine:
Uninstall from the backend card while the script is still declared, then
take the change, then Install. Re-apply when Iris is ready to do that,
which is also when dev-updater's conversion path can be deleted.
2026-08-28 18:18:38 -04:00
iris c2dfaab349 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.
2026-08-28 17:35:27 -04:00
irisandClaude Opus 5 a83dbcff6a Say when the server fell over, and where to read why
Iris found the backend crash-looping by checking rc-service by hand,
because the card could only say `stopped` -- which reads as a state
somebody chose. dev-updater's contract now has a fourth word, `failed`,
and this script implements it.

The OpenRC detail is the one worth not rederiving: it prints `crashed`
*and* exits non-zero, 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 claim. The `running` check stays on its exit code, which
already worked and does not depend on wording.

The script also arranges the logging rather than only reporting it,
because neither unit wrote a file: systemd went to the journal and
OpenRC's `command_background=true` discarded output entirely, which is why
a crash left nothing to read. Output now goes to
$XDG_DATA_HOME/ai-server/ai-server.log -- generated data, outliving any one
build, and not in a repository shared with a machine that should not read
it. `start` rotates one generation aside, so what is kept is exactly this
run and the one before: the pair worth having after a crash and a restart.
`logs` prints the paths, newest first, and nothing else.

Verified on systemd by causing the failure rather than reasoning about it:
installed, started, confirmed `running` on the wg0 bind, wrote an
unparseable config, restarted, and watched status settle on **failed**
rather than stopped -- with the reason, line and column, in the file `logs`
points at, and the crash preserved in .1 after recovery. Then restored,
confirmed `running` again, and uninstalled.

**The OpenRC branch is written from the documentation and is untested**,
here and in dev-updater, since neither machine that can run it is one
either of us can test on. It is also the branch that actually matters,
since the backend runs under OpenRC on the host. `output_log`/`error_log`
in the openrc-run script are the parts to distrust first.

One thing that bit while writing it: the systemd heredoc is unquoted so
$LOG expands, which makes a backtick in a comment inside it run as command
substitution. A comment saying "`start` rotates" executed `start`, and the
unit was written without ever being valid. There is now a note in the
heredoc saying why it contains no backticks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
2026-08-28 15:57:05 -04:00
irisandClaude Opus 5 5f8a2146e6 Ask Dev Updater for the checkout, not for the app inside it
Dev Updater's unit is a project -- a directory in a checkout -- that
produces components, so this declaration belongs at the repository root
where it can say where each half lives. `cwd` is how a component says
that, and it is what the old file was working around: sitting in `app/`,
it reached the backend with `../server/Cargo.toml` and `../server/service`,
which described the layout backwards.

So the file says what this repository produces: the backend, built and
serviced in `server/`, and then the APK, built in `app/`. The order is
unchanged and still the point -- a failing APK build leaves the phone the
APK it already had rather than half of a matched pair.

The service script gains dev-updater's note about lingering. A user
service stops at logout unless `loginctl enable-linger` is set, which for
this one matters more than for a build server: the phone reaches ai-server
whether or not anyone is logged in at the desk.

**This changes the project path, so the phone's existing entry (at
~/repos/ai-app/app) has to be removed and the checkout root added
instead**, and its components accepted once.

Verified against dev-updater's current parser rather than by reading its
schema: the declaration loads, `cargo build --release` resolves into
server/, app/build-apk.sh into app/, the service script to an absolute
path, and APK discovery from the root still finds the built APK.
`./server/service status` prints not-installed and exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
2026-08-28 02:26:02 -04:00
irisandClaude Opus 5 effefdeb03 Carry a service script, and declare the backend as a component
ai-server is now something Dev Updater can install, start and stop from
the phone: the project declares a Server component naming ./server/service,
and that script is where knowing about systemd and OpenRC lives.

Detection asks rather than looks -- `systemctl --user show-environment`
answering, or `rc-service --user` existing -- because a machine can carry
both binaries and an OpenRC below 0.60 has rc-service without --user.
Neither present is an error with a message, not a guess at a fallback.

Two things the script will not do. It never prompts: Dev Updater runs it
with stdin closed, so a sudo prompt would hang rather than fail, and
anything needing root exits telling you to run it yourself once. And on
OpenRC it refuses when XDG_RUNTIME_DIR is unset rather than proceeding --
user services store their state there, and the failure otherwise reads as
a broken service instead of a missing variable.

The server is declared before the app so it is built and delivered first;
a failed APK build then leaves the phone with the APK it already had
rather than half of a matched pair.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQz6R4kBQcWSHBNZMgBnjL
2026-08-26 00:48:25 -04:00