docs(installer): record Phase 4 — doctor, update and uninstall as built

The installer crate now implements the whole command surface INSTALL.md
published before the binary existed, so this records what Phase 4 turned
out to be and corrects two places where the plan and the guide had drifted
apart.

PLAN.md
  - Status header: Phases 1–4 are on `edge`; the edge → main cutover now
    cuts a binary that does everything INSTALL.md describes, with Phase 5
    being packaging polish rather than capability.
  - A Phase 4 "as built" section: why `update` is the install pipeline in
    a different mode rather than a second implementation, why it neither
    reprints the token nor stays quiet about a protocol change, the tier's
    scope under `update` (re-resolve what was applied, without re-asking;
    name what is new), how `doctor` asks the binary the way the service
    does, the exit-code rule and why a stopped shard is a ⚠ while a
    running one that has not dialed in is a ✗.
  - §5's uninstall table: the cached patch set and patches/originals/ move
    from "removed" to "kept". The report that command prints tells the
    operator to diff against those originals — deleting them made the
    advice impossible to follow within one command's output. `--purge`
    removes them.

INSTALL.md
  - §2: exit codes stated (`doctor` and `uninstall` use 1 for a completed
    run that found something wrong), `--patches` now applies to `update`,
    `--yes` means yes on `uninstall`, `--purge` covers the patch cache.
  - §7 doctor: the real row set, what ✓/⚠/✗ mean, that it writes nothing
    and is safe to run with the shard up.
  - §7 update: it updates the tree install.json names, needs the shard
    stopped, does not reprint the token, calls out a protocol change, and
    what it does and does not do with the patch tier.
  - §7 uninstall: what survives, that edited files are flagged in the
    listing, the confirmation's default, and where the report file lands.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-05 03:00:45 -05:00
parent 706b450828
commit ecef87f120
2 changed files with 150 additions and 27 deletions

View File

@@ -213,12 +213,17 @@ reports "unchanged" and writes nothing.
| `--verify` | `install`, `update` | Dry run. Report every change that would be made; write nothing. |
| `--servuo <path>` | `install`, `doctor`, `update` | Name the ServUO root instead of detecting or prompting. |
| `--bundle <tag>` | `install`, `update` | Pin an exact published bundle instead of the current one. |
| `--patches` / `--no-patches` | `install` | Decide the patch tier non-interactively. `--patches` never loosens the region check: patches whose target lines are not stock are reported for you to apply by hand, not forced. |
| `--patches` / `--no-patches` | `install`, `update` | Decide the patch tier non-interactively. `--patches` never loosens the region check: patches whose target lines are not stock are reported for you to apply by hand, not forced. On `update` it is what takes up a feature the shard does not already have. |
| `--patches-unsupported-servuo` | `install` | Required *in addition to* `--patches` to run the patch tier on a ServUO that is not 57.4. Unsupported and untested — see [§4](#4-the-patch-tier-optional). Ignored on 57.4. |
| `--host <name>` | `install` | The hostname to print in the website URLs. |
| `--site-url <url>` | `install` | Your site's base URL, for the Admin → Shard link. |
| `--yes` | all | Assume the default answer to every prompt. Combine with the flags above for an unattended run. |
| `--purge` | `uninstall` | Also delete `sidecar.toml` and `uo-link.db`, which are otherwise kept. |
| `--yes` | all | Assume the default answer to every prompt. Combine with the flags above for an unattended run. **On `uninstall` it means yes** — that prompt defaults to no, and typing `uninstall --yes` is not an accident. |
| `--purge` | `uninstall` | Also delete `sidecar.toml`, `uo-link.db` and the cached patch set, all of which are otherwise kept. |
Exit codes are `0` success, `1` the run failed, `2` the arguments were unusable. Two commands also
use `1` for a run that *completed* and found something wrong, so they can be read from a script:
`doctor` when any check failed, and `uninstall` when a step could not be carried out (everything
else still was).
---
@@ -504,21 +509,41 @@ The command that makes this supportable. Run it before asking anyone for help
first thing a maintainer will want.
```
✓ Install record /etc/runicgateway/install.json (bundle 2026.08.04, installer 1.0.0, …)
✓ ServUO found /opt/ServUO (57.4)
✓ Overlay in sync 24 files, all hashes match install.json
⚠ Patch tier 1 of 3 applied (region-match) — vendor.sale unavailable
✓ uo-link installed 1.1.0
✓ Service running, enabled
✓ Sidecar reachable 127.0.0.1:8080 /health ok
⚠ Patch tier 1 applied — moderation-audit (region-match)
✓ uo-link installed uo-link-sidecar 1.1.0 (protocol 3)
config /etc/runicgateway/sidecar.toml database /var/lib/runicgateway/uo-link.db
✓ Service runicgateway-link.service active, enabled as runicgateway
✓ Sidecar reachable 127.0.0.1:8080 /health ok, up 6h, database ok
✓ Protocol sidecar 3 = overlay manifest 3
✗ Shard connected no shard has dialed in since boot
✗ Shard connected no — the shard is running (pid 8123) but has not dialed in
✓ Bundle 2026.08.04 — up to date
```
Three of those rows come from asking the installed sidecar (`--version`, `--print-config`) rather
than from reading `install.json`, so `doctor` reports what the binary would actually do — including
which config and database file the *service* resolves — rather than what the installer believes it
was told. The overlay row compares live file hashes against both `install.json` and the release
manifest, which is how it tells "you edited a deployed file" from "the overlay moved on".
Rows come from asking the installed sidecar (`--version`, `--print-config`) rather than from reading
`install.json`, so `doctor` reports what the binary would actually do — including which config and
database file the *service* resolves — rather than what the installer believes it was told. The
overlay row compares live file hashes against `install.json`, which is how it tells "you edited a
deployed file" from "the file is gone"; the bundle row is what tells you the overlay upstream has
moved on. Each patched file is re-checked against the cached copy of its patch, so a core upgrade or
a restored backup that quietly removed the tier's edits is caught here — nothing else would notice.
It writes nothing at all, and it is safe to run while the shard is up; that is in fact the only
state in which the last row can be `✓`.
**Reading the marks:**
| | |
|---|---|
| `✓` | as it should be |
| `⚠` | worth knowing, not broken — a stopped shard, a service you never registered, an unpatched tier, or no route to Gitea to check for a newer bundle |
| `✗` | broken. `doctor` exits `1` if any row is `✗`, so it can be run from a monitoring script; a `⚠` never causes that |
The distinction on the last row is worth spelling out: **shard not running** is a `⚠` (start it),
while **shard running and not dialed in** is a `✗` — that is the silent failure this whole guide
warns about, where ServUO reports a clean boot over a script build that failed.
### `runicgateway update`
@@ -532,6 +557,21 @@ together — never to two independently-latest artifacts that may disagree.
Your `sidecar.toml`, your `Bridge.cfg` edits and your database are not touched. `Bridge.cfg` is
overwritten only if you have not changed it; a modified copy is reported, not clobbered.
It updates the ServUO tree `install.json` names — not a tree it detects — and it needs the shard
stopped, exactly as `install` does. There is nothing to update on a host that was never installed;
it says so rather than performing a first install under a verb that promises to preserve.
**Your auth token is not reprinted.** It has not changed and your website already has it. The one
thing an update can change that the site must be told about is the **protocol version**, and it says
so plainly when that happens — a stale number in Admin → Shard is answered with `409` and looks
exactly like your shard going offline.
**The patch tier under `update`:** features you already have are re-checked against the new release
(normally nothing to do), without asking you again — you consented when they were installed, and
that includes a shard where the tier ran unsupported. Features you never took are **named, not
applied**; run `update --patches` (or `install --patches`) to take one up. A shard that declined the
tier stays unpatched through every update.
### `runicgateway uninstall`
Removes what it exclusively owns, and **prints** everything else. The installer cannot know what you
@@ -540,12 +580,19 @@ your work.
| | |
|---|---|
| **Removed** | The sidecar binary, its service entry, `install.json`, the cached patch set |
| **Kept** | `sidecar.toml` and `uo-link.db` — config and history survive (`--purge` drops them) |
| **Printed, not done** | Every overlay file deployed into your ServUO tree, by path, for you to delete |
| **Removed** | The sidecar binary, its service entry, `install.json` |
| **Kept** | `sidecar.toml`, `uo-link.db`, and the cached patch set with its pre-patch originals (`--purge` drops all of them) |
| **Printed, not done** | Every overlay file deployed into your ServUO tree, by path, for you to delete — with any file you have edited since deployment flagged, so you do not delete your own work by mistake |
| **Printed, not done** | The exact hunks each applied patch added to `EventSink.cs`, `PlayerVendorGumps.cs` and `Logging.cs`, for you to revert — with how each landed, since one placed into a file you had already modified is worth a closer look. The pre-patch copy kept under `patches/originals/` is there to diff against. |
The report is also written to a file, so it survives the scrollback.
It lists all of that **before** asking, and the prompt defaults to **no**. `--yes` proceeds, which is
what an unattended uninstall needs; nothing else about the command is destructive to your shard,
which is neither stopped nor started.
The report is also written to a file — `runicgateway-uninstall-<timestamp>.txt` in the directory you
ran the command from — so it survives the scrollback. That is why the cached patches and the
originals stay behind by default: they are the only offline record of what the tier changed once the
release tarball is gone, and the report tells you to diff against them.
---

View File

@@ -1,15 +1,16 @@
# Runic Gateway Installer — plan
Status: **Phases 1, 2 and 3 built, on `edge`.** Phase 0's prerequisites all landed, the installer
repo publishes the bundle manifest, and [`INSTALL.md`](INSTALL.md) specified the operator-facing run
Status: **Phases 1 to 4 built, on `edge`.** Phase 0's prerequisites all landed, the installer repo
publishes the bundle manifest, and [`INSTALL.md`](INSTALL.md) specified the operator-facing run
before the binary existed. The crate now implements the installer core (bundle resolution, ServUO
detection and validation, the overlay sync, `install.json` — [Phase 1 as
built](#phase-1--installer-core)), the sidecar half (binary, config, service, token handoff —
[Phase 2 as built](#phase-2--uo-link-install-and-service)), and the patch tier (the rung ladder, the
unsupported-version path, the cached patch set — [Phase 3 as built](#phase-3--patch-tier-opt-in)).
All three are on the `edge` branch, not `main`, so no half-capable binary is released. **The
`edge → main` cutover is next**, and it now cuts a binary that does everything `INSTALL.md`
describes except `doctor`/`update`/`uninstall`, each of which says which phase it arrives in.
[Phase 2 as built](#phase-2--uo-link-install-and-service)), the patch tier (the rung ladder, the
unsupported-version path, the cached patch set — [Phase 3 as built](#phase-3--patch-tier-opt-in)),
and the day-two commands `doctor`, `update` and `uninstall` ([Phase 4 as
built](#phase-4--diagnostics-and-updates)). All four are on the `edge` branch, not `main`, so no
half-capable binary is released. **The `edge → main` cutover is next**, and it now cuts a binary
that does everything `INSTALL.md` describes — Phase 5 is packaging polish, not capability.
This document is the design of record; it supersedes the informal overview it grew out of, which
described a ServUO integration that does not match how `servuo-plugins` actually ships (see
[Corrections](#corrections-to-the-original-overview)).
@@ -106,7 +107,9 @@ most real shards are hand-modified. Therefore:
(`/etc/runicgateway/patches/`, `%ProgramData%\RunicGateway\patches\`). Re-runs stay idempotent,
and uninstall can print the exact hunks offline long after the release tarball is gone (§5,
Phase 4). As built this caches every patch the tier *evaluated*, not only those that applied,
because the refusal message names that path as the file to apply by hand.
because the refusal message names that path as the file to apply by hand. **The cache outlives an
uninstall** — see Phase 4, where the report that would have been left pointing at a deleted
directory is what settled it.
- **Cache the pre-image of every file the tier edits**, under `patches/originals/`, mirroring its
path in the ServUO tree. It is written before the first edit and never overwritten, so a revert
can be verified byte-for-byte rather than reconstructed from a printed diff — which matters most
@@ -754,14 +757,87 @@ a clever automatic revert risks silently eating their work. It removes and it re
| Action | Scope |
|---|---|
| Removed | uo-link binary, its service entry (systemd unit / Windows service), `install.json` and the cached patch set |
| Kept | `sidecar.toml` and `uo-link.db` (config and history survive; `--purge` to drop them) |
| Removed | uo-link binary, its service entry (systemd unit / Windows service), `install.json` |
| Kept | `sidecar.toml`, `uo-link.db`, and the cached patch set with its pre-patch originals (`--purge` to drop them) |
| **Printed, not done** | Every overlay file deployed into the ServUO tree, listed by path, for the operator to delete |
| **Printed, not done** | The exact hunks each applied patch added to `EventSink.cs`, `PlayerVendorGumps.cs`, `Logging.cs`, rendered from the cached `.patch` files — with the rung that applied each one (§2.2.1), since a `region-match` apply means the surrounding file was already the operator's — for them to revert by hand |
The printed report is also written to a file, so it survives the terminal scrollback of a long
uninstall.
**As built** ([installer#7](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/7)) —
`src/doctor.rs`, `src/update.rs` and `src/uninstall.rs`, plus `service::observe`/`service::remove`
and a `Mode` on the install pipeline. The decisions that were not already settled above:
- **`update` is the `install` pipeline in a different mode, not a second implementation.** This
section describes it as "re-resolve the bundle, then move both components to it" — which is what
an `install` over an existing deployment already does, down to keeping a modified `Bridge.cfg`
and restarting the service after replacing its binary. A separate implementation would have given
the sync rules, the two protocol cross-checks and the record-carrying logic a second place to
disagree. What actually differs is four things: a prior record is **required** (an `update` on an
uninstalled host is a typo or a state directory the run cannot see — never a first install under
a verb that promises to preserve), the tree comes from that record rather than from detection (a
host with two shards must not have an update silently move to the other one), the tier's scope
narrows, and the close is a diff instead of a handoff.
- **`update` does not reprint the token, and does call out a protocol change.** The token has not
changed and the website already holds it; reprinting a secret nobody has to act on just puts it
in another scrollback. The protocol number is the one thing an update *can* change that the
website has to be told about — a stale value in Admin → Shard is answered `409` and looks to an
operator exactly like the shard going offline.
- **The tier under `update` re-resolves only what an earlier run applied, without asking again.**
Not a fresh offer: a shard that declined stays unpatched through every update, which is what
opt-in has to mean. Consent is not re-sought for what is already in the tree — including on an
unsupported ServUO, where `install` demands a second flag — because the record *is* the evidence
that the operator opted in, and re-prompting would make an unattended update impossible on
precisely the hosts that most need their patches re-checked when an overlay moves. New features
the release offers are named but not applied; `--patches` is how they are taken up. A feature the
record shows as applied that the release no longer declares keeps its record rather than being
dropped: its edits are still in the tree, and a record that forgot them would stop `uninstall`
printing hunks that are really there.
- **`doctor` asks the thing itself, and asks it the way the service does.** `--print-config` is run
under the same `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 is what §5's
sketch promised and a bare call would have got wrong on Linux. It is also run **only when the
config already exists**, because that flag provisions: a diagnosis must not create the state it
is reporting on.
- **`doctor` exits `1` when a row failed, and a `⚠` never causes that.** The rule makes it readable
from a monitoring script, and the split is what keeps the report worth reading: a stopped shard
is a `⚠` with the reason ("you have not started it"), while a *running* shard that has not dialed
in is the `✗` (§2.1's silent failure). Being offline is a `⚠` too — a shard host with no route to
Gitea is a supported way to run this, and failing a health check over it would report a working
deployment as broken. Both network calls take short timeouts for the same reason.
- **The patch row re-resolves each recorded patch against the tree.** The cached `.patch` makes it
possible offline, and the expected answer is rung 0. A core upgrade, a hand revert or a restored
backup silently removes the tier's edits, and nothing else in the report would notice.
- **The cached patch set and `patches/originals/` survive an uninstall** — a deviation from the
table above, which listed them as removed. The report that same command prints tells the operator
to diff their stock files against those originals; deleting them would have made the advice
impossible to follow within one command's output. They are the only offline record of what the
tier changed once the release tarball is gone, so `--purge` is what removes them, alongside the
config and the database. The report names every path it left behind.
- **`--yes` means yes on `uninstall`, not "take the default".** Everywhere else that flag answers an
offer the *run* made, so taking the safe default is right. Here the operator typed the destructive
verb; reading `--yes` as "no" would leave an unattended uninstall unable to express itself at all,
and a script that appears to succeed while removing nothing is the worse of the two failures. The
interactive prompt still defaults to **no**, after listing exactly what will and will not be
touched.
- **`uninstall` exits `1` for a step it could not carry out**, having done everything else. The
common case is a binary still locked by a sidecar somebody started by hand, so a permission error
on that file says so rather than sending the operator to look at ACLs. The Linux service account
is removed only when the record says this installer created it; Windows' virtual account goes with
the service.
- **The overlay listing flags files edited since deployment.** An operator deleting that list file
by file must not lose their own `Bridge.cfg` settings or a script edit without being told which
ones those are.
Verified on this machine against a scratch ServUO 57.4 tree built from the real files: a healthy
`doctor` (exit 0), one against a tree with a deleted overlay file, an edited one and a reverted
patch (all three found, exit 1), an `update --verify` that wrote nothing, a real `update` that
repaired all three and left `install.json` byte-identical, `uninstall` with and without `--purge`,
a second `uninstall`, a locked binary reported as a problem with exit 1, and `doctor`/`update` on a
host with no record. `fmt`/`clippy -D warnings`/tests were run for Linux in Docker as well as on the
Windows host, since only half of `service.rs` compiles on either.
### Phase 5 — packaging polish
`.deb` packaging, Windows MSI, arm64 cross build, and optional automated backup before upgrade.