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

@@ -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.