feat(installer): implement Phase 4 — doctor, update and uninstall #7

Merged
whitlocktech merged 1 commits from feat/phase4-diagnostics-updates into edge 2026-08-05 08:49:47 +00:00
Member

What & why

Phase 4 of docs/installer/PLAN.md — the three day-two commands. With this, edge cuts a binary that does everything INSTALL.md §2 published before the binary existed, and the edge → main cutover is next. Docs half: docs#93.

doctorsrc/doctor.rs

Reads only. Every row is answered by asking the thing itself — the installed binary (--version, --print-config), the service manager, the sidecar's /health — because the record says what install did, which is a different question from what is true now, and the gap between those two is the whole reason to run it.

  • --print-config is run only when the config already exists: that flag provisions (writes the file, mints a token), and a diagnosis must not create the state it reports on. It is also run under the UOLINK_DB_PATH the unit pins, so the config and database it names are the ones the service opens rather than the ones the binary would pick on its own — which a bare call gets wrong on Linux.
  • Exit 1 when a row failed, so it can be read from a monitoring script; a never does that. A stopped shard is therefore a ("you have not started it") while a running shard that has not dialed in is the — the silent-failure case. Offline is a too: a shard host with no route to Gitea is a supported way to run this.
  • The patch row re-resolves each recorded patch against the tree from the cached .patch. A core upgrade, a hand revert or a restored backup silently removes the tier's edits and nothing else in the report would notice.

updatesrc/update.rs + install::Mode

The same pipeline as install, not a second one: PLAN.md describes update as "re-resolve the bundle, then move both components to it", which is what an install over an existing deployment already does. A separate implementation would give the sync rules, the two protocol cross-checks and the record-carrying logic somewhere to disagree. What differs is small: a prior record is required, the tree comes from that record rather than detection, the tier's scope narrows, and the close is a diff instead of a handoff.

  • The token is not reprinted — unchanged, and the site has it. A changed protocol version is called out, because a stale value in Admin → Shard is answered 409 and looks like the shard going offline.
  • Tier scope: features an earlier run applied are re-resolved without asking again (the record is the evidence of consent, including on an unsupported ServUO — otherwise unattended updates are impossible on exactly the hosts that most need re-checking); new features are named but not applied without --patches. A feature the record has that the release no longer declares keeps its record, since its edits are still in the tree.

uninstallsrc/uninstall.rs + service::remove

Removes the binary, the service and install.json; prints the overlay files and the exact hunks, rendered from the cached patches with the rung each landed at. Files edited since deployment are flagged so nobody deletes their own work blind. The report is also written to runicgateway-uninstall-<timestamp>.txt in the working directory.

Two deviations from PLAN.md §5, both agreed with the org lead before implementation and now recorded in the plan:

  • The cached patch set and patches/originals/ survive. That table listed them as removed, but the report tells the operator to diff against those originals — advice the same command would have made impossible to follow. --purge removes them, with the config and database.
  • --yes means yes here, not "take the default". The prompt defaults to no (destructive), but the operator typed the verb; reading --yes as "no" leaves an unattended uninstall unable to express itself, and a script that appears to succeed while removing nothing is the worse failure.

Exit 1 if a step could not be carried out — everything else still was. A permission error on the binary says "something may still be running it" rather than sending anyone to look at ACLs.

How it was tested

cargo fmt --check, cargo clippy --all-targets -- -D warnings and cargo test (137 tests) on the Windows host and for Linux in Docker — only half of service.rs compiles on either platform, and the Linux run is what caught a clippy lint the local toolchain does not have.

End to end against a scratch ServUO 57.4 tree built from the real files at C:\Users\colby\Desktop\ServUO, under a relocated RUNICGATEWAY_STATE_DIR:

Case Result
install --patches then doctor against a live sidecar every row except the two honest s (no service in a relocated run, shard not running); exit 0
deleted an overlay file, edited another, reverted a patch → doctor all three found and named, exit 1
update --verify reported the repairs, wrote nothing, install.json byte-identical
update repaired all three; install.json still byte-identical; doctor back to green
uninstall binary + record removed; config, db and patch cache kept; hunks rendered verbatim; report written to the cwd
uninstall --purge config, db and the whole patch dir gone too
uninstall twice, doctor/update with no record each says so; update names the state dir as the likely cause
binary locked by a hand-started sidecar reported as a problem with the running-process hint, exit 1, everything else still removed

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

  • No AI tools were used to produce this contribution.
  • AI tools were used. Tool(s): Claude Code (Opus 5). I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a Co-Authored-By trailer.

License

  • I agree that my contribution is licensed under this project's license (GNU GPL v3.0 or later), and I have the right to contribute it.
## What & why Phase 4 of [`docs/installer/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/installer/PLAN.md) — the three day-two commands. With this, `edge` cuts a binary that does everything `INSTALL.md` §2 published before the binary existed, and the `edge → main` cutover is next. Docs half: **docs#93**. ### `doctor` — `src/doctor.rs` Reads only. Every row is answered by asking the thing itself — the installed binary (`--version`, `--print-config`), the service manager, the sidecar's `/health` — because the record says what `install` *did*, which is a different question from what is true now, and the gap between those two is the whole reason to run it. - `--print-config` is run **only when the config already exists**: that flag provisions (writes the file, mints a token), and a diagnosis must not create the state it reports on. It is also run under the `UOLINK_DB_PATH` the unit pins, so the config and database it names are the ones the *service* opens rather than the ones the binary would pick on its own — which a bare call gets wrong on Linux. - **Exit 1 when a row failed**, so it can be read from a monitoring script; a `⚠` never does that. A stopped shard is therefore a `⚠` ("you have not started it") while a *running* shard that has not dialed in is the `✗` — the silent-failure case. Offline is a `⚠` too: a shard host with no route to Gitea is a supported way to run this. - The patch row **re-resolves each recorded patch against the tree** from the cached `.patch`. A core upgrade, a hand revert or a restored backup silently removes the tier's edits and nothing else in the report would notice. ### `update` — `src/update.rs` + `install::Mode` The same pipeline as `install`, not a second one: PLAN.md describes `update` as "re-resolve the bundle, then move both components to it", which is what an `install` over an existing deployment already does. A separate implementation would give the sync rules, the two protocol cross-checks and the record-carrying logic somewhere to disagree. What differs is small: a prior record is required, the tree comes from that record rather than detection, the tier's scope narrows, and the close is a diff instead of a handoff. - **The token is not reprinted** — unchanged, and the site has it. A changed **protocol version** *is* called out, because a stale value in Admin → Shard is answered `409` and looks like the shard going offline. - **Tier scope:** features an earlier run applied are re-resolved without asking again (the record is the evidence of consent, including on an unsupported ServUO — otherwise unattended updates are impossible on exactly the hosts that most need re-checking); new features are named but not applied without `--patches`. A feature the record has that the release no longer declares keeps its record, since its edits are still in the tree. ### `uninstall` — `src/uninstall.rs` + `service::remove` Removes the binary, the service and `install.json`; prints the overlay files and the exact hunks, rendered from the cached patches with the rung each landed at. Files edited since deployment are flagged so nobody deletes their own work blind. The report is also written to `runicgateway-uninstall-<timestamp>.txt` in the working directory. Two deviations from PLAN.md §5, both agreed with the org lead before implementation and now recorded in the plan: - **The cached patch set and `patches/originals/` survive.** That table listed them as removed, but the report tells the operator to diff against those originals — advice the same command would have made impossible to follow. `--purge` removes them, with the config and database. - **`--yes` means yes here**, not "take the default". The prompt defaults to no (destructive), but the operator typed the verb; reading `--yes` as "no" leaves an unattended uninstall unable to express itself, and a script that appears to succeed while removing nothing is the worse failure. Exit 1 if a step could not be carried out — everything else still was. A permission error on the binary says "something may still be running it" rather than sending anyone to look at ACLs. ## How it was tested `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings` and `cargo test` (137 tests) on the Windows host **and** for Linux in Docker — only half of `service.rs` compiles on either platform, and the Linux run is what caught a clippy lint the local toolchain does not have. End to end against a scratch ServUO 57.4 tree built from the real files at `C:\Users\colby\Desktop\ServUO`, under a relocated `RUNICGATEWAY_STATE_DIR`: | Case | Result | |---|---| | `install --patches` then `doctor` against a live sidecar | every row `✓` except the two honest `⚠`s (no service in a relocated run, shard not running); exit 0 | | deleted an overlay file, edited another, reverted a patch → `doctor` | all three found and named, exit 1 | | `update --verify` | reported the repairs, wrote nothing, `install.json` byte-identical | | `update` | repaired all three; `install.json` still byte-identical; `doctor` back to green | | `uninstall` | binary + record removed; config, db and patch cache kept; hunks rendered verbatim; report written to the cwd | | `uninstall --purge` | config, db and the whole patch dir gone too | | `uninstall` twice, `doctor`/`update` with no record | each says so; `update` names the state dir as the likely cause | | binary locked by a hand-started sidecar | reported as a problem with the running-process hint, exit 1, everything else still removed | ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [ ] No AI tools were used to produce this contribution. - [x] AI tools were used. Tool(s): `Claude Code (Opus 5)`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it.
wtclaude added 1 commit 2026-08-05 08:01:52 +00:00
feat(installer): implement Phase 4 — doctor, update and uninstall
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 59s
80b1c0da24
Completes the command surface INSTALL.md §2 published before the binary
existed. With this, `edge` cuts a binary that does everything that guide
describes.

doctor (src/doctor.rs)
  Reads only. Every row is answered by asking the thing itself — the
  installed binary (--version, --print-config), the service manager, and
  the sidecar's /health — because the record says what `install` did,
  which is a different question from what is true now. --print-config is
  run ONLY when the config already exists: that flag provisions, and a
  diagnosis must not create the state it reports on. It is also run under
  the environment the service pins (UOLINK_DB_PATH), so the config and
  database it names are the ones the service opens, not the ones the
  binary would pick on its own.

  Exit 1 when any row failed, so a monitoring script can read it; a ⚠
  never does that. A stopped shard is therefore a ⚠, not a ✗ — "you have
  not started it" and "it is running and the bridge is dead" are
  different problems and only the second is broken. Offline is a ⚠ too:
  a shard host with no route to Gitea is a supported way to run this.

  The patch row re-resolves each recorded patch against the tree from the
  cached .patch, so a core upgrade or a restored backup that silently
  removed the tier's edits is caught — nothing else here would notice.

update (src/update.rs, install.rs::Mode)
  The same pipeline as install, not a second one: PLAN.md describes it as
  "re-resolve the bundle, then move both components to it", which is what
  an install over an existing deployment already does. Writing it twice
  would give the sync rules and the protocol cross-checks two places to
  disagree. What differs is small and lives in Mode — a prior record is
  required, the tree comes from that record rather than detection, the
  patch tier's scope narrows, and the close is a diff instead of a
  handoff.

  The token is not reprinted: it has not changed and the website has it.
  A changed protocol number IS called out, because a stale value in
  Admin → Shard is answered with 409 and looks like the shard going
  offline.

  Tier scope: features an earlier run recorded are re-resolved without
  asking again (the record is the evidence of consent, including on an
  unsupported ServUO); anything new the release offers is named but not
  applied without --patches. A shard that declined stays declined.

uninstall (src/uninstall.rs, service::remove)
  Removes the binary, the service and install.json; prints the overlay
  files and the exact hunks, rendered from the cached patches with the
  rung each landed at. Files edited since deployment are flagged so
  nobody deletes their own work blind. The report is also written to a
  file in the working directory — it is the only thing still needed after
  the command exits, and it arrives at the end of the longest output this
  tool produces.

  Two deviations from PLAN.md §5, both deliberate:

  - The cached patch set and patches/originals/ SURVIVE. That table put
    them under "removed", but the report tells the operator to diff
    against those originals — advice the same command would have made
    impossible to follow. --purge removes them, with the config and the
    database.
  - --yes means yes here, not "take the default". The prompt defaults to
    no (destructive), but the operator typed the verb; reading --yes as
    "no" would leave an unattended uninstall unable to express itself,
    and a script that appears to succeed while removing nothing is the
    worse failure.

  Exit 1 if a step could not be carried out — everything else still was.

Verified on this machine against a scratch tree built from the real
ServUO 57.4 files: a healthy doctor (exit 0), one with a deleted overlay
file, an edited one and a reverted patch (all three found, exit 1), a
--verify update that wrote nothing, a real update that repaired all three
and left install.json byte-identical, uninstall with and without --purge,
a second uninstall, and doctor/update on a host with no record. Linux
fmt/clippy/tests run in Docker as well as the Windows host.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit a0106e2c84 into edge 2026-08-05 08:49:47 +00:00
whitlocktech deleted branch feat/phase4-diagnostics-updates 2026-08-05 08:49:47 +00:00
Sign in to join this conversation.
No description provided.