feat(installer): implement Phase 4 — doctor, update and uninstall #7
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/phase4-diagnostics-updates"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What & why
Phase 4 of
docs/installer/PLAN.md— the three day-two commands. With this,edgecuts a binary that does everythingINSTALL.md§2 published before the binary existed, and theedge → maincutover is next. Docs half: docs#93.doctor—src/doctor.rsReads 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 whatinstalldid, which is a different question from what is true now, and the gap between those two is the whole reason to run it.--print-configis 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 theUOLINK_DB_PATHthe 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.⚠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..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::ModeThe same pipeline as
install, not a second one: PLAN.md describesupdateas "re-resolve the bundle, then move both components to it", which is what aninstallover 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.409and looks like the shard going offline.--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::removeRemoves 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 torunicgateway-uninstall-<timestamp>.txtin the working directory.Two deviations from PLAN.md §5, both agreed with the org lead before implementation and now recorded in the plan:
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.--purgeremoves them, with the config and database.--yesmeans yes here, not "take the default". The prompt defaults to no (destructive), but the operator typed the verb; reading--yesas "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 warningsandcargo test(137 tests) on the Windows host and for Linux in Docker — only half ofservice.rscompiles 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 relocatedRUNICGATEWAY_STATE_DIR:install --patchesthendoctoragainst a live sidecar✓except the two honest⚠s (no service in a relocated run, shard not running); exit 0doctorupdate --verifyinstall.jsonbyte-identicalupdateinstall.jsonstill byte-identical;doctorback to greenuninstalluninstall --purgeuninstalltwice,doctor/updatewith no recordupdatenames the state dir as the likely causeChecklist
AI-assisted contributions (required)
Claude Code (Opus 5). I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with aCo-Authored-Bytrailer.License
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>