Merge pull request 'docs(installer): record Phase 1 as built' (#90) from feat/installer-phase-1 into main

Reviewed-on: #90
This commit is contained in:
2026-08-04 20:00:56 +00:00
2 changed files with 87 additions and 6 deletions

View File

@@ -5,6 +5,11 @@ installation and connects it to a Runic Gateway website.
> **Status: the installer binary is not released yet.**
>
> Phase 1 — the installer core: bundle resolution, ServUO detection, the overlay sync and
> `install.json` — is built and lives on the installer repo's `edge` branch. It is deliberately not
> released: a binary that deploys the overlay but cannot yet install the sidecar would not do what
> this guide describes. The first release follows Phase 2.
>
> Everything it installs *is* released and published — the sidecar, the plugin overlay, and the
> [bundle manifest](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/main/bundles/current.json)
> that names the checked combination of the two. This guide is the operator-facing contract those
@@ -496,6 +501,7 @@ The report is also written to a file, so it survives the scrollback.
| Symptom | Cause and fix |
|---|---|
| **Windows asks for Administrator as soon as you launch it** | Expected, and it needs Administrator anyway. Windows applies *installer detection* to unsigned executables whose file name contains `install` and elevates them before the program starts. Run it from an already-elevated PowerShell and you will not see the prompt. |
| **"ServUO is running — stop it before installing"** | Correct, and not overridable. `ServUO.exe` locks `Scripts.dll` and rewrites `Saves/` on exit; deploying underneath it corrupts one or both. Stop the shard, install, start it again. |
| Shard boots clean but nothing reaches the site | The classic silent failure: ServUO ignores the script build's exit code and reloaded a **stale `Scripts.dll`**. Run `dotnet build Scripts/Scripts.csproj -c Release -p:Platform=x64` and read the errors it prints. |
| `[bridge status` says `connected=False` | The sidecar is not listening on `127.0.0.1:7788`. Check the service is running, and that `[shard] bind` in `sidecar.toml` matches `Host`/`Port` in `Bridge.cfg`. |

View File

@@ -1,11 +1,13 @@
# Runic Gateway Installer — plan
Status: **Phase 0 complete.** Every prerequisite in another repo has landed, the installer repo
publishes the bundle manifest, and [`INSTALL.md`](INSTALL.md) now specifies the operator-facing run
— so *what* the installer installs and *what using it looks like* both exist ahead of the binary.
No installer code exists yet; **Phase 1 is next.** 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
Status: **Phase 1 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 exists and implements the installer core — bundle
resolution, ServUO detection and validation, the overlay sync and `install.json` — on the
`edge` branch, not `main`, so no half-capable binary is released (see
[Phase 1 as built](#phase-1--installer-core)). **Phase 2 (uo-link install + service) is next.**
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)).
| Phase 0 item | State |
@@ -458,6 +460,79 @@ Repo work that must land before an installer can exist.
timestamp.
- Idempotent re-runs; a second run with no upstream change reports "unchanged" and writes nothing.
**As built** ([installer#4](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/4)) — the
crate at the repo root, `install` implemented end to end, `doctor`/`update`/`uninstall` parsed and
answered with the phase they arrive in rather than "unrecognized command". The decisions that were
not already settled above:
- **It lands on `edge`, not `main`.** `release.yml` publishes an installer binary on every push to
`main`, and its crate guard was written to arm "the moment Phase 1 lands the crate" — which would
have published a binary that deploys the overlay but cannot install the sidecar, contradicting
everything `INSTALL.md` promises a release does. Phases 1 and 2 land on `edge`; the `edge → main`
cutover cuts the first release. `pr-checks.yml` gates PRs into `edge` on the same rules, so the
branch where the work happens is not the ungated one. No workflow needed a temporary edit.
- **The run says what it did *not* do.** A Phase 1 `install` ends with an unmissable block naming
the sidecar as not installed, pointing at `INSTALL.md` A3/A4, and printing the bundle's binary URL
and SHA256 so a hand install matches the pair. `--patches` is the sharp edge here: it is accepted
(so the flag surface is the published one) but reports `REQUESTED BUT NOT APPLIED — no stock
ServUO file has been touched`. A `--patches` run that completed quietly would be read as a
patched shard.
- **The crate is a library plus a thin binary, and the library is not named after it.** Windows
applies UAC *installer detection* to unsigned executables whose file name contains `install`: it
demands elevation before the process starts, and a non-interactive session gets `os error 740`
instead of a program. That is survivable for the shipped binary — it needs Administrator anyway,
and `INSTALL.md` already says to run it from an elevated shell — but Cargo names test harnesses
after their target, so a target called `runicgateway_installer` makes `cargo test` **unrunnable on
Windows**, on the machine the shard smoke tests live on. The code therefore sits in a library
called `rgdeploy`, the binary target keeps its published name, and `[[bin]] test = false` stops
Cargo building a harness under it. Nothing an operator sees changes.
- **Dependencies chosen for the MinGW cross-build:** `ureq` (blocking HTTP over rustls/ring — no
OpenSSL to cross-compile, and no async runtime for a tool that makes four sequential requests),
`flate2` on its pure-Rust backend, `tar`, `sha2`, `serde`/`serde_json`, `chrono`, `anyhow`, and
`sysinfo` for the running-shard check.
- **The shard-running check matches by path, not by process name.** `deploy.ps1` can look for a
process called `ServUO` because it only runs on Windows; on Linux the same shard is `mono` or
`dotnet` with `ServUO.exe` as an argument, and a name match would answer "not running" for a live
shard — the one wrong answer that corrupts `Scripts.dll`. The installer requires a process whose
executable or command line names *both* the tree being deployed into and `ServUO.exe`, so a second
shard elsewhere on the host does not block this deploy, and the installer never matches itself.
- **ServUO's version is read from `Server/AssemblyInfo.cs`**, not from `ServUO.exe`'s PE metadata:
it is the same *source* tree the patch tier diffs against, needs no dependency, and works
identically on Linux. `57.4.0.0` and `57.4` are normalized to compare equal. An unreadable version
is reported as `unknown` and treated as **not** supported — an unreadable version is not evidence
of a good one — which is what Phase 3 will gate the tier on.
- **`install.json` records a state, not a verb.** Per-file entries are `deployed` or
`kept-operator-modified`, never `add`/`change`/`unchanged`. Recording the run's verb made the
record differ between a first run and an identical second one, which rewrote the file on every
run and broke "a second run writes nothing" in the least visible way available. What later
commands need is whose copy is in the tree, and that does not change because time passed.
- **The `Bridge.cfg` decision compares against the last hash the installer *deployed*, not the last
hash it *saw*.** Once a file has been kept, the record's on-disk hash is the operator's content —
so a rule phrased as "is the tree still what the record last saw?" matches on the very next run
and overwrites exactly the file it had just protected. A keep has to stay kept for as long as the
edit is there; a live three-run test covers it, because the bug only appears from the second run
on.
- **A prior record is only consulted when it names this tree.** A host whose `install.json` points
at a different ServUO root — a shard moved or rebuilt beside the old one — is treated as having no
prior deployment, which errs toward keeping the operator's file.
- **The download is verified twice, for two different reasons.** The tarball's SHA256 is checked
against the bundle while it is being written (the trust anchor — these artifacts are unsigned);
then every extracted file is re-hashed against the release's own `manifest.json`, which catches a
truncated extraction and is what makes the hashes copied into `install.json` worth trusting. The
manifest's `protocol` and `version` are also cross-checked against the bundle, so an artifact that
disagrees with the matrix that named it stops the run before anything is written.
- **`RUNICGATEWAY_STATE_DIR` relocates the installer's own state**, so a run can be tested without
root. Documented in `--help` rather than hidden: an undocumented variable that moves where a tool
writes is worse than a documented one, and `doctor` must honour the same value to find what
`install` wrote.
Verified on this machine against a real ServUO 57.4 tree (`--verify`, which reported the tree's
`Bridge.cfg` as operator-owned and 23 code files as changed) and end to end into a scratch tree:
24 files deployed, a second run reporting `unchanged` and leaving `install.json` untouched, an
edited `Bridge.cfg` kept across three further runs while a hand-edited `.cs` was overwritten each
time, a pinned `--bundle`, a missing bundle tag, and a refusal — pid and path named, exit 1 — with a
process running out of the tree.
### Phase 2 — uo-link install and service
- Linux: binary → `/usr/bin/runicgateway-link`, config → `/etc/runicgateway/sidecar.toml`, db →