Compare commits
1 Commits
main
...
docs/insta
| Author | SHA1 | Date | |
|---|---|---|---|
| d0363cd62d |
@@ -5,14 +5,6 @@ installation and connects it to a Runic Gateway website.
|
|||||||
|
|
||||||
> **Status: the installer binary is not released yet.**
|
> **Status: the installer binary is not released yet.**
|
||||||
>
|
>
|
||||||
> Phases 1 to 3 are built and live on the installer repo's `edge` branch: the installer core
|
|
||||||
> (bundle resolution, ServUO detection, the overlay sync, `install.json`), the sidecar half (the
|
|
||||||
> binary, its config, its service, and the token handoff), and the
|
|
||||||
> [patch tier](#4-the-patch-tier-optional). `install` is therefore complete; what is still missing
|
|
||||||
> is [`doctor`, `update` and `uninstall`](#7-day-two), each of which reports which phase it arrives
|
|
||||||
> in rather than failing as though you had mistyped it. The first release is being cut from that
|
|
||||||
> branch now.
|
|
||||||
>
|
|
||||||
> Everything it installs *is* released and published — the sidecar, the plugin overlay, and the
|
> 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)
|
> [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
|
> that names the checked combination of the two. This guide is the operator-facing contract those
|
||||||
@@ -35,7 +27,7 @@ Three things, on the machine that runs your shard:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 1 | **The plugin overlay** — C# source that ServUO compiles at boot, copied into your server tree | [`RunicGateway/servuo-plugins`](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins) release tarball |
|
| 1 | **The plugin overlay** — C# source that ServUO compiles at boot, copied into your server tree | [`RunicGateway/servuo-plugins`](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins) release tarball |
|
||||||
| 2 | **The uo-link sidecar** — a small Rust service that the shard dials out to, and that your website reads from | [`RunicGateway/link`](https://gitea.whitlocktech.com/RunicGateway/link) release binary |
|
| 2 | **The uo-link sidecar** — a small Rust service that the shard dials out to, and that your website reads from | [`RunicGateway/link`](https://gitea.whitlocktech.com/RunicGateway/link) release binary |
|
||||||
| 3 | **A record of what it did** — `install.json`, plus cached copies of the patches and of every file the patch tier edited | Written by the installer |
|
| 3 | **A record of what it did** — `install.json`, plus a cached copy of any patches it applied | Written by the installer |
|
||||||
|
|
||||||
```
|
```
|
||||||
ServUO shard ──loopback TCP 127.0.0.1:7788──► uo-link sidecar ──HTTP + WebSocket──► website
|
ServUO shard ──loopback TCP 127.0.0.1:7788──► uo-link sidecar ──HTTP + WebSocket──► website
|
||||||
@@ -61,7 +53,7 @@ Only the sidecar is exposed, and only to your website.
|
|||||||
| Requirement | Detail |
|
| Requirement | Detail |
|
||||||
|---|---|
|
|---|---|
|
||||||
| A working ServUO install | It must currently boot and compile scripts cleanly. The installer deploys onto a healthy shard; it does not repair a broken one. |
|
| A working ServUO install | It must currently boot and compile scripts cleanly. The installer deploys onto a healthy shard; it does not repair a broken one. |
|
||||||
| ServUO **57.4** *(patch tier only)* | **57.4 is the only supported version.** The base install works on any reasonably current ServUO. The patch tier is written and tested against stock 57.4; on any other version it is **unsupported and untested** — you can still choose to run it, behind an explicit opt-in, and it applies only where the exact lines it patches are unchanged. See [§4](#4-the-patch-tier-optional). |
|
| ServUO **57.4** *(patch tier only)* | The base install works on any reasonably current ServUO. The patch tier is verified against stock 57.4 only, and is skipped with a warning on anything else. |
|
||||||
| ServUO **stopped** | `ServUO.exe` holds a lock on `Scripts.dll` and writes `Saves/` on exit. The installer refuses to deploy under a running shard. |
|
| ServUO **stopped** | `ServUO.exe` holds a lock on `Scripts.dll` and writes `Saves/` on exit. The installer refuses to deploy under a running shard. |
|
||||||
| Administrator / root | It writes into system directories and registers a service. |
|
| Administrator / root | It writes into system directories and registers a service. |
|
||||||
| Outbound HTTPS | To `gitea.whitlocktech.com`, to fetch the bundle and the two artifacts. Nothing inbound is needed, and no Gitea account or git client is required. |
|
| Outbound HTTPS | To `gitea.whitlocktech.com`, to fetch the bundle and the two artifacts. Nothing inbound is needed, and no Gitea account or git client is required. |
|
||||||
@@ -148,9 +140,8 @@ one file); `doctor`, `update` and `uninstall` are run from it later. Examples be
|
|||||||
1. **Your ServUO root** — detected if the installer is run from inside it or from an obvious
|
1. **Your ServUO root** — detected if the installer is run from inside it or from an obvious
|
||||||
sibling, otherwise prompted. A directory qualifies only if it contains `ServUO.exe`, `Scripts/`
|
sibling, otherwise prompted. A directory qualifies only if it contains `ServUO.exe`, `Scripts/`
|
||||||
and `Config/`.
|
and `Config/`.
|
||||||
2. **Whether to apply the patch tier** — off unless you say yes. On a ServUO that is not 57.4 the
|
2. **Whether to apply the patch tier** — off unless you say yes, and not offered at all if your
|
||||||
prompt defaults to **no** and carries an unsupported-version warning you have to answer past.
|
ServUO is not 57.4. See [§4](#4-the-patch-tier-optional).
|
||||||
See [§4](#4-the-patch-tier-optional).
|
|
||||||
3. **The hostname your website should use to reach this machine** — used only to compose the two
|
3. **The hostname your website should use to reach this machine** — used only to compose the two
|
||||||
URLs it prints at the end. The sidecar's bind address is frequently `127.0.0.1` or `0.0.0.0`,
|
URLs it prints at the end. The sidecar's bind address is frequently `127.0.0.1` or `0.0.0.0`,
|
||||||
neither of which is something to hand to a website.
|
neither of which is something to hand to a website.
|
||||||
@@ -174,19 +165,16 @@ Overlay sync
|
|||||||
ADD Config/Bridge.cfg
|
ADD Config/Bridge.cfg
|
||||||
ADD Scripts/Custom/Bridge/*.cs (22 files)
|
ADD Scripts/Custom/Bridge/*.cs (22 files)
|
||||||
CHANGE Scripts/Scripts.csproj
|
CHANGE Scripts/Scripts.csproj
|
||||||
deployed. add=23 change=1 unchanged=0 kept=0
|
deployed. add=23 change=1 unchanged=0
|
||||||
|
|
||||||
Patch tier not selected
|
Patch tier skipped (not selected)
|
||||||
Without it: no vendor.sale events, no in-game moderation audit forwarding.
|
Without it: no vendor.sale events, no in-game moderation audit forwarding.
|
||||||
|
|
||||||
uo-link sidecar
|
uo-link
|
||||||
binary /usr/bin/runicgateway-link install
|
binary /usr/bin/runicgateway-link
|
||||||
✓ sidecar binary verified sha256 27d491ef…
|
config /etc/runicgateway/sidecar.toml (created)
|
||||||
config /etc/runicgateway/sidecar.toml created
|
database /var/lib/runicgateway/uo-link.db
|
||||||
database /var/lib/runicgateway/uo-link.db
|
service runicgateway-link.service enabled, running
|
||||||
listening on shard 127.0.0.1:7788 website 127.0.0.1:8080
|
|
||||||
service runicgateway-link.service active, enabled
|
|
||||||
running as runicgateway
|
|
||||||
|
|
||||||
Recorded /etc/runicgateway/install.json
|
Recorded /etc/runicgateway/install.json
|
||||||
|
|
||||||
@@ -213,8 +201,7 @@ reports "unchanged" and writes nothing.
|
|||||||
| `--verify` | `install`, `update` | Dry run. Report every change that would be made; write 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. |
|
| `--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. |
|
| `--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` | Decide the patch tier non-interactively. `--patches` still refuses on a non-57.4 tree. |
|
||||||
| `--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. |
|
| `--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. |
|
| `--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. |
|
| `--yes` | all | Assume the default answer to every prompt. Combine with the flags above for an unattended run. |
|
||||||
@@ -231,8 +218,7 @@ reports "unchanged" and writes nothing.
|
|||||||
| `/usr/bin/runicgateway-link` | The sidecar binary |
|
| `/usr/bin/runicgateway-link` | The sidecar binary |
|
||||||
| `/etc/runicgateway/sidecar.toml` | Sidecar config, including the auth token |
|
| `/etc/runicgateway/sidecar.toml` | Sidecar config, including the auth token |
|
||||||
| `/etc/runicgateway/install.json` | What the installer deployed: versions, commit, per-file hashes, applied patches, timestamps |
|
| `/etc/runicgateway/install.json` | What the installer deployed: versions, commit, per-file hashes, applied patches, timestamps |
|
||||||
| `/etc/runicgateway/patches/` | Copies of the patches the tier evaluated, so `uninstall` can print the exact hunks long after the release tarball is gone, and a refused one is still on hand to apply yourself |
|
| `/etc/runicgateway/patches/` | Copies of any patches applied, so `uninstall` can print the exact hunks long after the release tarball is gone |
|
||||||
| `/etc/runicgateway/patches/originals/` | Each file the patch tier edited, exactly as it was beforehand — a revert you can verify rather than reconstruct |
|
|
||||||
| `/var/lib/runicgateway/uo-link.db` | The sidecar's SQLite store (event history, cached profiles, link map) |
|
| `/var/lib/runicgateway/uo-link.db` | The sidecar's SQLite store (event history, cached profiles, link map) |
|
||||||
| `/etc/systemd/system/runicgateway-link.service` | The service unit, running as a dedicated user |
|
| `/etc/systemd/system/runicgateway-link.service` | The service unit, running as a dedicated user |
|
||||||
|
|
||||||
@@ -244,9 +230,8 @@ reports "unchanged" and writes nothing.
|
|||||||
| `%ProgramData%\RunicGateway\sidecar.toml` | Sidecar config, including the auth token |
|
| `%ProgramData%\RunicGateway\sidecar.toml` | Sidecar config, including the auth token |
|
||||||
| `%ProgramData%\RunicGateway\install.json` | As above |
|
| `%ProgramData%\RunicGateway\install.json` | As above |
|
||||||
| `%ProgramData%\RunicGateway\patches\` | As above |
|
| `%ProgramData%\RunicGateway\patches\` | As above |
|
||||||
| `%ProgramData%\RunicGateway\patches\originals\` | As above |
|
|
||||||
| `%ProgramData%\RunicGateway\uo-link.db` | The sidecar's SQLite store |
|
| `%ProgramData%\RunicGateway\uo-link.db` | The sidecar's SQLite store |
|
||||||
| Service `RunicGatewayLink` | Automatic start, restart on failure, running as `NT SERVICE\RunicGatewayLink` |
|
| Service `RunicGatewayLink` | Automatic start, restart on failure |
|
||||||
|
|
||||||
**Inside your ServUO tree** (added by the overlay sync — 24 files):
|
**Inside your ServUO tree** (added by the overlay sync — 24 files):
|
||||||
|
|
||||||
@@ -256,30 +241,11 @@ Scripts/Custom/Bridge/*.cs 22 files: the plugin itself
|
|||||||
Scripts/Scripts.csproj OVERWRITES a stock file (see below)
|
Scripts/Scripts.csproj OVERWRITES a stock file (see below)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Both service definitions pin the config path**, because the sidecar's own default is relative to
|
Both service definitions pin `UOLINK_CONFIG` and `UOLINK_DB_PATH` explicitly. The sidecar's own
|
||||||
its working directory — and a service manager's working directory is not somewhere you want a
|
defaults are relative to its working directory, and a service manager's working directory is not
|
||||||
database or a config file. On Windows it can be `%SystemRoot%\System32` or, under
|
somewhere you want a database — on Windows it can be `%SystemRoot%\System32` or, under
|
||||||
`C:\Program Files\`, a silently redirected VirtualStore copy.
|
`C:\Program Files\`, a silently redirected VirtualStore copy.
|
||||||
|
|
||||||
How the *database* path is pinned differs by platform, and that is deliberate:
|
|
||||||
|
|
||||||
| | Config | Database |
|
|
||||||
|---|---|---|
|
|
||||||
| **Linux** | `Environment=UOLINK_CONFIG=` in the unit | `Environment=UOLINK_DB_PATH=` in the unit — `/etc` and `/var/lib` are different directories, so both need naming |
|
|
||||||
| **Windows** | `--config` inside the service's own `binPath` | nothing to set: a relative `[store] path` resolves against the config's directory, which *is* `%ProgramData%\RunicGateway` |
|
|
||||||
|
|
||||||
The Windows service would otherwise need a **machine-wide** environment variable — `sc.exe` has no
|
|
||||||
per-service one — which every process on the host inherits and which outlives an uninstall.
|
|
||||||
|
|
||||||
**Both run as a dedicated, unprivileged account.** Linux gets a `runicgateway` system user; Windows
|
|
||||||
gets a virtual service account, `NT SERVICE\RunicGatewayLink`, which Windows creates as part of
|
|
||||||
registering the service and which has no password. Neither runs as root or `LocalSystem`.
|
|
||||||
|
|
||||||
**`sidecar.toml` is locked down, because it holds your auth token.** Neither default location
|
|
||||||
protects it on its own — `/etc` is world-readable, and `%ProgramData%` grants `Users` read access by
|
|
||||||
inheritance — so the installer sets the permissions itself: `chmod 600` plus `chown` to the service
|
|
||||||
user on Linux, and an explicit ACL of SYSTEM, Administrators and the service account on Windows.
|
|
||||||
|
|
||||||
> **`Scripts.csproj` is overwritten deliberately.** The stock file omits `Scripts/Custom/`, so the
|
> **`Scripts.csproj` is overwritten deliberately.** The stock file omits `Scripts/Custom/`, so the
|
||||||
> plugin would sit in the tree and never compile — and ServUO would not tell you, because it
|
> plugin would sit in the tree and never compile — and ServUO would not tell you, because it
|
||||||
> ignores the script build's exit code and silently reloads the previous `Scripts.dll`. That
|
> ignores the script build's exit code and silently reloads the previous `Scripts.dll`. That
|
||||||
@@ -306,87 +272,15 @@ How the installer handles it:
|
|||||||
|
|
||||||
- **Opt-in.** The base install completes without it, and declining is a supported outcome, not a
|
- **Opt-in.** The base install completes without it, and declining is a supported outcome, not a
|
||||||
degraded one.
|
degraded one.
|
||||||
- **Dry-run first, always.** Every patch is checked before anything is applied, and reported per
|
- **Dry-run first, always.** Every patch is checked (`git apply --check`) before anything is
|
||||||
patch. Most real shards are hand-modified; a patch that does not apply is expected, not alarming.
|
applied, and reported per patch. Most real shards are hand-modified; a patch that does not apply
|
||||||
- **A modified file is not automatically a refusal.** These patches touch three small regions of
|
is expected, not alarming.
|
||||||
three large files. If you have edited `Logging.cs` somewhere else entirely, the installer says so
|
|
||||||
and still applies the patch — it checks whether *the lines the patch edits* are still stock, not
|
|
||||||
whether the whole file is. It applies only where the surrounding lines match the patch exactly and
|
|
||||||
appear exactly once; anything less and it stops and hands you the hunk to apply by hand. It never
|
|
||||||
force-fits a patch by loosening the match.
|
|
||||||
- **All or nothing per feature.** The two vendor-sale patches are one unit and are applied together
|
- **All or nothing per feature.** The two vendor-sale patches are one unit and are applied together
|
||||||
or not at all — and within a patch, if one hunk cannot be placed safely, none are. A patch that
|
or not at all.
|
||||||
could have been placed but was held back by its sibling says exactly that; it is never reported as
|
- **Skipped entirely on a ServUO that is not 57.4**, with a warning. Unverified diffs are never
|
||||||
applied.
|
applied to an unknown tree.
|
||||||
- **It does not need `git`, and does not use it.** The matching and the writing are the installer's
|
|
||||||
own, which is why it can place a patch on a shard where `git apply` refuses — the shipped patches
|
|
||||||
and their target files do not all use the same line endings, and that alone defeats `git apply`.
|
|
||||||
Nothing outside a patched region is touched, down to the byte, and inserted lines take your file's
|
|
||||||
own line ending.
|
|
||||||
- **Your ServUO version is reported, not decisive** — but see the warning below before running this
|
|
||||||
on anything other than 57.4.
|
|
||||||
- **Recorded, and the `.patch` files cached**, so re-runs stay idempotent and `uninstall` can print
|
- **Recorded, and the `.patch` files cached**, so re-runs stay idempotent and `uninstall` can print
|
||||||
the exact hunks to revert — along with how each was applied, since a patch placed into a file you
|
the exact hunks to revert.
|
||||||
had already modified is one to look at more carefully when reverting. Patches that were *not*
|
|
||||||
applied are cached too, because that is the copy the run tells you to apply by hand.
|
|
||||||
- **A copy of every file it edits is kept, exactly as it was beforehand**, under
|
|
||||||
`patches/originals/` in the installer's own directory — not in your ServUO tree. It is written
|
|
||||||
before the first edit and never overwritten, so however many times you re-run `install`, it stays
|
|
||||||
the version from before the tier ever touched the file. That is what lets you verify a revert
|
|
||||||
rather than reconstruct one.
|
|
||||||
- **Re-running is safe.** A patch already in place is recognised and left alone, and the record
|
|
||||||
keeps the way it originally landed rather than relabelling it.
|
|
||||||
|
|
||||||
### ⚠ On any ServUO that is not 57.4: unsupported, untested, no guarantees
|
|
||||||
|
|
||||||
> **Runic Gateway is designed, built and tested against stock ServUO 57.4.** That is the only
|
|
||||||
> supported version.
|
|
||||||
>
|
|
||||||
> On any other version — a newer release, an older one, or a fork — the patch tier is
|
|
||||||
> **UNSUPPORTED, UNTESTED, and NOT GUARANTEED TO WORK.** You may run it. If you do, you are on your
|
|
||||||
> own: it is not covered by support, and a bad outcome may not show up until your shard is live,
|
|
||||||
> because ServUO's script build reports success even when it failed and quietly keeps running the
|
|
||||||
> previous `Scripts.dll`.
|
|
||||||
>
|
|
||||||
> The installer will still refuse to place a patch anywhere the exact lines it edits have changed —
|
|
||||||
> but matching text is not the same as matching behaviour. A hunk can land correctly and still be
|
|
||||||
> wrong for a tree that has diverged around it.
|
|
||||||
>
|
|
||||||
> **Back up your ServUO tree first, and verify your shard boots and compiles afterwards.**
|
|
||||||
|
|
||||||
Because of that, on a non-57.4 tree the tier is off by default and takes a deliberate yes:
|
|
||||||
|
|
||||||
- the interactive prompt defaults to **no** and prints the warning above;
|
|
||||||
- `--patches` on its own is **not** enough — an unattended run must also pass
|
|
||||||
`--patches-unsupported-servuo`;
|
|
||||||
- the choice is recorded, and `doctor` keeps showing an unsupported-version row for the life of the
|
|
||||||
install — so whoever looks after this shard next can see it without being told.
|
|
||||||
|
|
||||||
A run where the tier is selected on a shard that has been worked on looks like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
Patch tier 2 of 3 applied
|
|
||||||
✓ playervendor-sale-eventsink Server/EventSink.cs
|
|
||||||
stock file — applied at line 171, 1521, 1771, 2416
|
|
||||||
✓ playervendor-sale-gump Scripts/Gumps/PlayerVendorGumps.cs
|
|
||||||
file modified, patched region stock — applied at line 95
|
|
||||||
✗ commandlogging-event Scripts/Commands/Logging.cs
|
|
||||||
patched region has been modified (hunk 1) — not applied
|
|
||||||
apply this by hand, then re-run install to record it:
|
|
||||||
/etc/runicgateway/patches/commandlogging-event.patch
|
|
||||||
|
|
||||||
⚠ Server/EventSink.cs — a CORE ServUO file was patched. Rebuild the solution:
|
|
||||||
dotnet build ServUO.sln
|
|
||||||
A shard restart is not enough; ServUO's dynamic script build does not rebuild the core, and it
|
|
||||||
will not tell you so.
|
|
||||||
|
|
||||||
Not applied, so you do not get: no in-game moderation audit forwarding.
|
|
||||||
Everything else works. Apply the hunks by hand if you want them, then re-run install to record it.
|
|
||||||
```
|
|
||||||
|
|
||||||
The line numbers are where each hunk was actually found in *your* file, not where it sits in stock
|
|
||||||
ServUO — they differ as soon as anything above the region has been edited, and yours is the one to
|
|
||||||
go to.
|
|
||||||
|
|
||||||
If it is skipped or fails, you lose exactly two things — **`vendor.sale` events** and **in-game
|
If it is skipped or fails, you lose exactly two things — **`vendor.sale` events** and **in-game
|
||||||
moderation audit forwarding**. Everything else works. You can apply the patches later by hand (see
|
moderation audit forwarding**. Everything else works. You can apply the patches later by hand (see
|
||||||
@@ -431,20 +325,68 @@ Saving restarts the site's ingest client, so the change takes effect immediately
|
|||||||
AES-GCM encrypted at rest and **never returned to any client** — losing it means reading it back
|
AES-GCM encrypted at rest and **never returned to any client** — losing it means reading it back
|
||||||
from `sidecar.toml` on the shard host, not from the website.
|
from `sidecar.toml` on the shard host, not from the website.
|
||||||
|
|
||||||
|
If the sidecar sits behind a reverse proxy, paste your **public** `https://` and `wss://` URLs
|
||||||
|
instead of the two the installer printed — it composes those from the sidecar's own bind address,
|
||||||
|
which knows nothing about what fronts it. Everything else on the page is unchanged.
|
||||||
|
|
||||||
### If your website is on a different machine
|
### If your website is on a different machine
|
||||||
|
|
||||||
The sidecar binds `127.0.0.1:8080` by default, which is reachable only from the shard host. If your
|
The sidecar binds `127.0.0.1:8080` by default, which is reachable only from the shard host. If your
|
||||||
website runs elsewhere, you must widen the bind — and then narrow the access:
|
website runs elsewhere, the recommended arrangement is a **TLS reverse proxy in front of the
|
||||||
|
sidecar** — this is a supported deployment and the one Runic Gateway itself runs, on a real domain
|
||||||
|
name.
|
||||||
|
|
||||||
1. Set `[web] bind` in `sidecar.toml` to `0.0.0.0:8080` (or a specific LAN address) and restart the
|
**Leave `[web] bind` on `127.0.0.1:8080`** and let the proxy be the only thing that talks to it.
|
||||||
service.
|
Widening the bind and firewalling the port is the alternative, not the default (see below).
|
||||||
2. **Firewall port 8080 to your website's address only.** The auth token is always on, but it
|
|
||||||
travels as a plain bearer token — the sidecar speaks HTTP, not HTTPS.
|
|
||||||
3. If the two hosts are not on a trusted network, put the sidecar behind a TLS reverse proxy or a
|
|
||||||
VPN/WireGuard link, and give the website the proxied `https://` / `wss://` URLs.
|
|
||||||
|
|
||||||
The `[shard] bind` line is a different matter: leave it on `127.0.0.1:7788`. That socket accepts
|
Give the website the **proxied** URLs — `https://link.example.com` and
|
||||||
*inbound commands* to the game, and being loopback-only is what makes that safe.
|
`wss://link.example.com/ws` — in place of the `http://` / `ws://` pair the installer prints. Those
|
||||||
|
values are the sidecar's own view of itself; the proxy is what the outside world sees.
|
||||||
|
|
||||||
|
What the proxy must do:
|
||||||
|
|
||||||
|
| Requirement | Why |
|
||||||
|
|---|---|
|
||||||
|
| **Forward the WebSocket upgrade** (`Upgrade` / `Connection` headers, HTTP/1.1 to the upstream) | `/ws` is the live event feed. Without it the site's REST calls work and events never arrive — a confusing half-working state. |
|
||||||
|
| **Pass request headers through unmodified** | Auth is `Authorization: Bearer` (or `X-Api-Key`), and the website sends `X-UOLink-Version`. A proxy that strips unknown headers turns into a `401`, and a stripped version header just silently skips the mismatch check. |
|
||||||
|
| **Do not buffer the WS connection, and allow long-lived ones** | The feed is idle between events. The sidecar sends a WebSocket **Ping every 30 s**, so a read timeout of 60 s or more is safe as it stands — but a proxy that buffers responses will hold events instead of streaming them. |
|
||||||
|
| **Do not log query strings** | The sidecar also accepts `?token=…` (for clients that cannot set headers). If anything in your stack uses that form, a default access-log format writes your auth token to disk on every request. |
|
||||||
|
|
||||||
|
Nothing needs `X-Forwarded-For`: the sidecar never uses the client's IP for authorization, and the
|
||||||
|
browser IP that account provisioning cares about is supplied by the website in the request body.
|
||||||
|
|
||||||
|
An nginx server block that satisfies all of the above:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name link.example.com;
|
||||||
|
|
||||||
|
# your certificate directives here
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade; # "upgrade" for WS, "" otherwise
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(with the usual `map $http_upgrade $connection_upgrade { default upgrade; '' close; }` at `http`
|
||||||
|
level). Caddy and Traefik handle WebSocket upgrades automatically and need no equivalent stanza.
|
||||||
|
|
||||||
|
**Without a proxy**, on a trusted network only: set `[web] bind` to `0.0.0.0:8080` or a specific LAN
|
||||||
|
address, restart the service, and **firewall the port to your website's address**. The auth token is
|
||||||
|
always required, but the sidecar speaks HTTP — on that path the token and every event cross the
|
||||||
|
network in the clear. Do not do this over the public internet.
|
||||||
|
|
||||||
|
The `[shard] bind` line is a different matter entirely: leave it on `127.0.0.1:7788` and never proxy
|
||||||
|
it. That socket accepts *inbound commands* to the game, and being loopback-only is what makes that
|
||||||
|
safe.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -506,7 +448,7 @@ first thing a maintainer will want.
|
|||||||
```
|
```
|
||||||
✓ ServUO found /opt/ServUO (57.4)
|
✓ ServUO found /opt/ServUO (57.4)
|
||||||
✓ Overlay in sync 24 files, all hashes match install.json
|
✓ Overlay in sync 24 files, all hashes match install.json
|
||||||
⚠ Patch tier 1 of 3 applied (region-match) — vendor.sale unavailable
|
⚠ Patch tier 1 of 3 applied — vendor.sale unavailable
|
||||||
✓ uo-link installed 1.1.0
|
✓ uo-link installed 1.1.0
|
||||||
✓ Service running, enabled
|
✓ Service running, enabled
|
||||||
✓ Sidecar reachable 127.0.0.1:8080 /health ok
|
✓ Sidecar reachable 127.0.0.1:8080 /health ok
|
||||||
@@ -543,7 +485,7 @@ your work.
|
|||||||
| **Removed** | The sidecar binary, its service entry, `install.json`, the cached patch set |
|
| **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) |
|
| **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 |
|
| **Printed, not done** | Every overlay file deployed into your ServUO tree, by path, for you to delete |
|
||||||
| **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. |
|
| **Printed, not done** | The exact hunks each applied patch added to `EventSink.cs`, `PlayerVendorGumps.cs` and `Logging.cs`, for you to revert |
|
||||||
|
|
||||||
The report is also written to a file, so it survives the scrollback.
|
The report is also written to a file, so it survives the scrollback.
|
||||||
|
|
||||||
@@ -553,16 +495,15 @@ The report is also written to a file, so it survives the scrollback.
|
|||||||
|
|
||||||
| Symptom | Cause and fix |
|
| 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. |
|
| **"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. |
|
| 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`. |
|
| `[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`. |
|
||||||
| `[bridge` is not a command | The plugin did not compile, or `Bridge.cfg` has the bridge disabled. See the row above. |
|
| `[bridge` is not a command | The plugin did not compile, or `Bridge.cfg` has the bridge disabled. See the row above. |
|
||||||
| Website says the shard is offline; `/health` is fine locally | The website cannot reach port 8080 — bind address or firewall. See [§5](#if-your-website-is-on-a-different-machine). Note that the site is *designed* to render normally with the shard offline, so this fails quietly by design. |
|
| Website says the shard is offline; `/health` is fine locally | The website cannot reach the sidecar — bind address, firewall, or proxy. See [§5](#if-your-website-is-on-a-different-machine). Note that the site is *designed* to render normally with the shard offline, so this fails quietly by design. |
|
||||||
|
| REST reads work but **no live events arrive** | The classic reverse-proxy symptom: the WebSocket upgrade is not being forwarded. Confirm the proxy sets `Upgrade`/`Connection` and speaks HTTP/1.1 upstream, and that the site's WebSocket URL is `wss://…/ws` — not `https://`. |
|
||||||
|
| The event feed connects, then drops every minute or two | A proxy read timeout below the sidecar's 30 s WebSocket ping interval, or response buffering. Raise the timeout and turn buffering off. |
|
||||||
| Website logs `409` from the sidecar | Protocol mismatch: the number in Admin → Shard does not match the sidecar's. The sidecar rejects rather than mis-parsing. Set the field to what `/health` reports (`protocol`). If the *sidecar* and *overlay* disagree, you have a hand-assembled pair — reinstall from a bundle. |
|
| Website logs `409` from the sidecar | Protocol mismatch: the number in Admin → Shard does not match the sidecar's. The sidecar rejects rather than mis-parsing. Set the field to what `/health` reports (`protocol`). If the *sidecar* and *overlay* disagree, you have a hand-assembled pair — reinstall from a bundle. |
|
||||||
| `401` from the sidecar | Wrong or missing auth token. Read the live one back with `uo-link-sidecar --print-config --config <path>`; do not retype it from a screenshot. |
|
| `401` from the sidecar | Wrong or missing auth token. Read the live one back with `uo-link-sidecar --print-config --config <path>`; do not retype it from a screenshot. |
|
||||||
| **"service NOT REGISTERED" at the end of an otherwise successful run** | The host has no service manager the installer can drive — most often no systemd (a container, or a distro that never had it), or the `runicgateway` user could not be created. The binary and config *are* installed; the run prints the exact unit and commands to finish by hand. It never falls back to running the service as root or `LocalSystem`. |
|
|
||||||
| Service registered but stops immediately | It cannot read its config. On Windows check that `sc qc RunicGatewayLink` shows `--config` in `BINARY_PATH_NAME` and that `NT SERVICE\RunicGatewayLink` has read access to `sidecar.toml`; on Linux check the `runicgateway` user can read `/etc/runicgateway/sidecar.toml` and write `/var/lib/runicgateway/`. |
|
|
||||||
| A patch will not apply | Expected on a hand-modified shard. The base install is unaffected; you lose only the two features in [§4](#4-the-patch-tier-optional). Apply the hunks by hand if you want them. |
|
| A patch will not apply | Expected on a hand-modified shard. The base install is unaffected; you lose only the two features in [§4](#4-the-patch-tier-optional). Apply the hunks by hand if you want them. |
|
||||||
| `vendor.sale` events never arrive despite patching | The `EventSink.cs` patch is a **core** change. A shard restart is not enough — rebuild the solution (`dotnet build ServUO.sln`). |
|
| `vendor.sale` events never arrive despite patching | The `EventSink.cs` patch is a **core** change. A shard restart is not enough — rebuild the solution (`dotnet build ServUO.sln`). |
|
||||||
| Sidecar writes its database somewhere unexpected | A relative `[store] path` resolves against the directory holding `sidecar.toml` — not the working directory. Run `--print-config` to see the absolute path it will actually use. |
|
| Sidecar writes its database somewhere unexpected | A relative `[store] path` resolves against the directory holding `sidecar.toml` — not the working directory. Run `--print-config` to see the absolute path it will actually use. |
|
||||||
@@ -622,13 +563,7 @@ cp patches/BridgeModerationAudit.cs Scripts/Custom/Bridge/
|
|||||||
```
|
```
|
||||||
|
|
||||||
`git apply` works in a plain directory — the shard does not need to be a git repo. If you use
|
`git apply` works in a plain directory — the shard does not need to be a git repo. If you use
|
||||||
`patch` instead, note that some core files are CRLF while others are LF: use `patch --binary`.
|
`patch` instead, note the core files are CRLF: use `patch --binary`.
|
||||||
|
|
||||||
**If `git apply` refuses a patch whose target region is visibly untouched, line endings are the
|
|
||||||
usual cause** — the `.patch` files and their targets do not all use the same ones, and `git apply`
|
|
||||||
compares them literally. The installer's own tier normalizes line endings and trailing whitespace
|
|
||||||
for the *comparison* while writing back your file's own endings, which is why it can place patches
|
|
||||||
`git apply` rejects. Running the installer is the easier route here.
|
|
||||||
|
|
||||||
### A3. Install the sidecar
|
### A3. Install the sidecar
|
||||||
|
|
||||||
@@ -711,35 +646,15 @@ Copy-Item .\uo-link-sidecar-windows-x86_64.exe "$env:ProgramFiles\RunicGateway\u
|
|||||||
|
|
||||||
& "$env:ProgramFiles\RunicGateway\uo-link-sidecar.exe" --print-config --config "$env:ProgramData\RunicGateway\sidecar.toml"
|
& "$env:ProgramFiles\RunicGateway\uo-link-sidecar.exe" --print-config --config "$env:ProgramData\RunicGateway\sidecar.toml"
|
||||||
|
|
||||||
# The config file now holds your auth token. Lock it down before anything else can read it:
|
sc.exe create RunicGatewayLink binPath= "\"$env:ProgramFiles\RunicGateway\uo-link-sidecar.exe\"" start= auto
|
||||||
icacls "$env:ProgramData\RunicGateway\sidecar.toml" /inheritance:r /grant:r '*S-1-5-18:(F)' /grant:r '*S-1-5-32-544:(F)'
|
|
||||||
|
|
||||||
# binPath carries the config path. The single quotes matter: the value itself contains the double
|
|
||||||
# quotes the service manager needs around a path with spaces in it.
|
|
||||||
sc.exe create RunicGatewayLink `
|
|
||||||
binPath= '"C:\Program Files\RunicGateway\uo-link-sidecar.exe" --config "C:\ProgramData\RunicGateway\sidecar.toml"' `
|
|
||||||
obj= 'NT SERVICE\RunicGatewayLink' start= auto
|
|
||||||
sc.exe failure RunicGatewayLink reset= 86400 actions= restart/5000
|
sc.exe failure RunicGatewayLink reset= 86400 actions= restart/5000
|
||||||
|
[Environment]::SetEnvironmentVariable('UOLINK_CONFIG', "$env:ProgramData\RunicGateway\sidecar.toml", 'Machine')
|
||||||
# The service account exists only once sc create has created it, so its grants come after:
|
[Environment]::SetEnvironmentVariable('UOLINK_DB_PATH', "$env:ProgramData\RunicGateway\uo-link.db", 'Machine')
|
||||||
icacls "$env:ProgramData\RunicGateway\sidecar.toml" /grant 'NT SERVICE\RunicGatewayLink:(R)'
|
|
||||||
icacls "$env:ProgramData\RunicGateway" /grant 'NT SERVICE\RunicGatewayLink:(OI)(CI)M'
|
|
||||||
|
|
||||||
sc.exe start RunicGatewayLink
|
sc.exe start RunicGatewayLink
|
||||||
```
|
```
|
||||||
|
|
||||||
Three things there are easy to get wrong:
|
Machine environment variables are read at service start, so set them before starting — and never
|
||||||
|
leave the config path to the default, which is relative to the service's working directory.
|
||||||
- **The config path goes in `binPath`, not in a machine environment variable.** `sc.exe` has no
|
|
||||||
per-service environment, and a machine-wide `UOLINK_CONFIG` would be inherited by every process on
|
|
||||||
the host and survive an uninstall. Never leave the config path to the default — it is relative to
|
|
||||||
the service's working directory, which for a service is `%SystemRoot%\System32`.
|
|
||||||
- **The database needs no pinning here.** A relative `[store] path` resolves against the directory
|
|
||||||
holding `sidecar.toml`, which is already `%ProgramData%\RunicGateway`.
|
|
||||||
- **`obj=` is what keeps this off `LocalSystem`.** `NT SERVICE\RunicGatewayLink` is a virtual
|
|
||||||
service account: Windows creates it with the service, it has no password, and it exists only for
|
|
||||||
this service. Omit `obj=` and you get the most privileged local identity there is, for a process
|
|
||||||
listening on two TCP ports.
|
|
||||||
|
|
||||||
### A5. Connect the website, start the shard, verify
|
### A5. Connect the website, start the shard, verify
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
# Runic Gateway Installer — plan
|
# Runic Gateway Installer — plan
|
||||||
|
|
||||||
Status: **Phases 1, 2 and 3 built, on `edge`.** Phase 0's prerequisites all landed, the installer
|
Status: **Phase 0 complete.** Every prerequisite in another repo has landed, the installer repo
|
||||||
repo publishes the bundle manifest, and [`INSTALL.md`](INSTALL.md) specified the operator-facing run
|
publishes the bundle manifest, and [`INSTALL.md`](INSTALL.md) now specifies the operator-facing run
|
||||||
before the binary existed. The crate now implements the installer core (bundle resolution, ServUO
|
— so *what* the installer installs and *what using it looks like* both exist ahead of the binary.
|
||||||
detection and validation, the overlay sync, `install.json` — [Phase 1 as
|
No installer code exists yet; **Phase 1 is next.** This document is the design of record; it
|
||||||
built](#phase-1--installer-core)), the sidecar half (binary, config, service, token handoff —
|
supersedes the informal overview it grew out of, which described a ServUO integration that does not
|
||||||
[Phase 2 as built](#phase-2--uo-link-install-and-service)), and the patch tier (the rung ladder, the
|
match how `servuo-plugins` actually ships (see
|
||||||
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.
|
|
||||||
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)).
|
[Corrections](#corrections-to-the-original-overview)).
|
||||||
|
|
||||||
| Phase 0 item | State |
|
| Phase 0 item | State |
|
||||||
@@ -19,7 +13,7 @@ described a ServUO integration that does not match how `servuo-plugins` actually
|
|||||||
| 0.1 `servuo-plugins` release workflow | ✅ Merged — [servuo-plugins#7](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/7) + [#8](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/8); first overlay release is [`v0.1.1`](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/tag/v0.1.1) |
|
| 0.1 `servuo-plugins` release workflow | ✅ Merged — [servuo-plugins#7](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/7) + [#8](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/8); first overlay release is [`v0.1.1`](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/tag/v0.1.1) |
|
||||||
| 0.2 `link` installable (data paths + `--print-config`) | ✅ Merged — [link#24](https://gitea.whitlocktech.com/RunicGateway/link/pulls/24) (docs half [docs#84](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/84)); released as [`v1.1.0`](https://gitea.whitlocktech.com/RunicGateway/link/releases/tag/v1.1.0) |
|
| 0.2 `link` installable (data paths + `--print-config`) | ✅ Merged — [link#24](https://gitea.whitlocktech.com/RunicGateway/link/pulls/24) (docs half [docs#84](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/84)); released as [`v1.1.0`](https://gitea.whitlocktech.com/RunicGateway/link/releases/tag/v1.1.0) |
|
||||||
| 0.3 Bundle CI in the installer repo | ✅ Merged — [installer#3](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/3), plus the dispatch step in each component ([link#25](https://gitea.whitlocktech.com/RunicGateway/link/pulls/25), [servuo-plugins#9](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/9)). First bundle: [`2026.08.04`](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/main/bundles/current.json) |
|
| 0.3 Bundle CI in the installer repo | ✅ Merged — [installer#3](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/3), plus the dispatch step in each component ([link#25](https://gitea.whitlocktech.com/RunicGateway/link/pulls/25), [servuo-plugins#9](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/9)). First bundle: [`2026.08.04`](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/main/bundles/current.json) |
|
||||||
| 0.4 This file + `INSTALL.md` | ✅ Merged — [docs#87](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/87). [`INSTALL.md`](INSTALL.md) is the operator guide, written before the binary because it *is* the specification of the run |
|
| 0.4 This file + `INSTALL.md` | 🟨 In review — [docs#87](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/87). [`INSTALL.md`](INSTALL.md) is the operator guide, written before the binary because it *is* the specification of the run |
|
||||||
| — Repo bootstrap (governance + CI) | ✅ [`RunicGateway/installer`](https://gitea.whitlocktech.com/RunicGateway/installer) created; workflows merged ([installer#1](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/1), [#2](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/2)) |
|
| — Repo bootstrap (governance + CI) | ✅ [`RunicGateway/installer`](https://gitea.whitlocktech.com/RunicGateway/installer) created; workflows merged ([installer#1](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/1), [#2](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/2)) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -50,7 +44,7 @@ release/start scripts. The installer never writes a launcher.
|
|||||||
| Composition | **Published bundle manifest** (§7.1). CI names an exact, protocol-checked combination of component versions; the installer fetches it at run time and `--bundle <tag>` pins one. Component releases regenerate JSON, not the installer binary |
|
| Composition | **Published bundle manifest** (§7.1). CI names an exact, protocol-checked combination of component versions; the installer fetches it at run time and `--bundle <tag>` pins one. Component releases regenerate JSON, not the installer binary |
|
||||||
| Token handoff | **Print token + prefilled admin URL** at the end of the run |
|
| Token handoff | **Print token + prefilled admin URL** at the end of the run |
|
||||||
| Repo | **New repo**, `RunicGateway/installer`. It deploys *both* other components, so living inside `link/` would invert the dependency |
|
| Repo | **New repo**, `RunicGateway/installer`. It deploys *both* other components, so living inside `link/` would invert the dependency |
|
||||||
| ServUO version | **57.4 is the only supported version.** The patch tier's gate is content, not a version string: a patch applies where the lines it edits are still stock and is handed to the operator where they are not (§2.2.1). Forks and hand-edited trees are the norm in a public audience, so a non-57.4 tree is still *allowed* to attempt the tier — but **unsupported, untested and not guaranteed**, behind a loud banner, a defaulted-to-no prompt and its own opt-in flag (§2.2.2) |
|
| ServUO version | **Warn and skip.** Patches are verified against stock 57.4 only; on anything else the base install proceeds and the patch tier is skipped with a warning. Forks are the norm in a public audience — refusing outright would block most operators |
|
||||||
| Uninstall | **Never touches the ServUO tree.** Removes uo-link and its service entry, then *prints* the overlay files to delete and the patch hunks to revert. Reverting is the operator's call |
|
| Uninstall | **Never touches the ServUO tree.** Removes uo-link and its service entry, then *prints* the overlay files to delete and the patch hunks to revert. Reverting is the operator's call |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -102,100 +96,12 @@ most real shards are hand-modified. Therefore:
|
|||||||
audit forwarding**.
|
audit forwarding**.
|
||||||
- The `EventSink.cs` patch must warn loudly that a **core solution rebuild** is required, not just a
|
- The `EventSink.cs` patch must warn loudly that a **core solution rebuild** is required, not just a
|
||||||
shard restart.
|
shard restart.
|
||||||
- Record applied patches in `install.json`, **and cache the `.patch` files** next to it
|
- On any ServUO version other than stock **57.4**, skip the whole tier with a warning and continue
|
||||||
|
with the base install. Do not attempt to apply unverified diffs to an unknown tree.
|
||||||
|
- Record applied patches in `install.json`, **and cache the applied `.patch` files** next to it
|
||||||
(`/etc/runicgateway/patches/`, `%ProgramData%\RunicGateway\patches\`). Re-runs stay idempotent,
|
(`/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,
|
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,
|
Phase 4).
|
||||||
because the refusal message names that path as the file to apply by hand.
|
|
||||||
- **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
|
|
||||||
after a `region-match` apply, where the surrounding file was already the operator's. It stays out
|
|
||||||
of the ServUO tree, since uninstall has promised never to clean up in there.
|
|
||||||
- **A `.patch` does not carry everything the tier needs.** Which patches form one unit, which
|
|
||||||
companion `.cs` follows which, whether a core rebuild is required and what declining costs are
|
|
||||||
declared by the overlay release and read from its manifest — see §7.0.
|
|
||||||
|
|
||||||
#### 2.2.1 A whole-file hash mismatch is not a verdict — check the region
|
|
||||||
|
|
||||||
A file-level hash compare answers "is this entire file stock?", which is the wrong question. The
|
|
||||||
patches touch three small regions of three large files; an operator who added a custom command to
|
|
||||||
`Logging.cs` or a hook to `EventSink.cs` has changed the file's hash without going anywhere near the
|
|
||||||
lines the patch edits. Refusing on the file hash alone hands most real shards a manual patch job
|
|
||||||
they did not need. So the decision is made in three rungs, cheapest and safest first, and only the
|
|
||||||
last one gives up:
|
|
||||||
|
|
||||||
| Rung | Test | Outcome |
|
|
||||||
|---|---|---|
|
|
||||||
| **0 — already applied** | The hunk's *post*-patch text appears in the file | No-op, recorded as applied. Keeps re-runs idempotent |
|
|
||||||
| **1 — file is stock** | Whole-file hash matches the patch's pre-image (`index <old>..<new>` in the diff — `git hash-object` on the target reproduces it) | Apply verbatim with `git apply` |
|
|
||||||
| **2 — region is stock** | File differs, but every hunk's stock-side region is still byte-identical | Apply hunk-by-hunk at the matched offsets |
|
|
||||||
| **3 — region is modified** | Anything else | **Do not touch the file.** Print the path, the hunks and what is lost; the operator patches by hand |
|
|
||||||
|
|
||||||
Rung 2 is the semantic review, and it needs no new metadata: a unified diff already carries the
|
|
||||||
stock text of the region it edits — the context lines plus the `-` lines *are* the pre-image. For
|
|
||||||
each hunk the installer reconstructs that block and searches the target file for it, under these
|
|
||||||
rules:
|
|
||||||
|
|
||||||
- **Exact match, not fuzzy.** Only line-ending (CRLF/LF) and trailing-whitespace normalization is
|
|
||||||
allowed. No `patch --fuzz`, no context reduction: dropping context to force a match is precisely
|
|
||||||
how a patch lands in the wrong method.
|
|
||||||
- **Exactly one occurrence, or it fails.** Zero means the region moved or was edited. More than one
|
|
||||||
means the anchor is ambiguous and the installer cannot know which the author meant. Both are
|
|
||||||
rung 3.
|
|
||||||
- **Line numbers are advisory.** The hunk header's offsets are used only to prefer the nearest
|
|
||||||
candidate when reporting; the match itself is by content, since insertions above the region shift
|
|
||||||
every number below it.
|
|
||||||
- **All-or-nothing per patch file, and again per feature.** If one hunk of a patch reaches rung 3,
|
|
||||||
none of that patch's hunks are applied — a half-patched `EventSink.cs` compiles against a
|
|
||||||
companion `.cs` that expects the whole thing, and a partial apply is harder for an operator to
|
|
||||||
unpick than an untouched file. The same rule then applies across the patches of one feature: the
|
|
||||||
two vendor-sale patches are a unit (the event, the call site, and the subscriber that needs both),
|
|
||||||
so a patch that *could* have been placed is held back when a sibling cannot be — and the run says
|
|
||||||
that rather than reporting it as applied.
|
|
||||||
- **Rung 0 is checked first and is also all-or-nothing.** A file where some hunks are already
|
|
||||||
present and others are not is a hand-merge in progress, not an idempotent re-run — that is
|
|
||||||
rung 3.
|
|
||||||
|
|
||||||
#### 2.2.2 Non-57.4 is allowed, unsupported, and must say so loudly
|
|
||||||
|
|
||||||
The rung ladder replaces the blanket ServUO-version gate. The old rule skipped the entire tier on
|
|
||||||
anything other than stock 57.4 on the grounds that unverified diffs must not be applied to an
|
|
||||||
unknown tree — but forks are the norm (§1), so that rule skipped the tier for most of the audience.
|
|
||||||
Content matching gives a stronger guarantee than a version string does: on a non-57.4 tree, rung 1
|
|
||||||
is simply unavailable (its pre-image hash cannot be trusted), the tier goes straight to rung 2, and
|
|
||||||
a hunk lands only where the surrounding lines are still character-for-character the ones the patch
|
|
||||||
was written against.
|
|
||||||
|
|
||||||
**That is a mechanical safety guarantee about where text lands. It is not a support commitment, and
|
|
||||||
the installer must never let the two be confused.** Runic Gateway is designed, built and tested
|
|
||||||
against **stock ServUO 57.4**. On anything else the patch tier is **unsupported, untested, and not
|
|
||||||
guaranteed to work** — a hunk can match textually and still be wrong for a tree whose surrounding
|
|
||||||
behaviour has diverged, and neither the shard's silent script build (§2.1) nor the installer will
|
|
||||||
tell you that. So:
|
|
||||||
|
|
||||||
- **The disclaimer is unmissable, not a footnote.** On a non-57.4 tree the tier prints a banner
|
|
||||||
before it is even offered — that 57.4 is the only supported version, that the operator is on their
|
|
||||||
own here, and that a bad outcome may not surface until the shard is running.
|
|
||||||
- **It is off by default and takes an explicit, separate yes.** The interactive prompt defaults to
|
|
||||||
**no** on a non-57.4 tree, and `--patches` alone is **not** consent: an unattended run must pass
|
|
||||||
`--patches-unsupported-servuo` as well. A flag an operator had to look up cannot be hit by
|
|
||||||
accident in a script copied from somewhere else.
|
|
||||||
- **The label follows the install.** `install.json` records the detected version and the fact that
|
|
||||||
the tier ran unsupported; `doctor` shows that row on every subsequent run, not just at install
|
|
||||||
time; and the uninstall report carries it too. An operator who inherits this shard six months
|
|
||||||
later must be able to see it without being told.
|
|
||||||
- **It is the first thing quoted back in a bug report.** The tier's summary line names the detected
|
|
||||||
version, so a pasted install log answers "which ServUO?" before anyone asks.
|
|
||||||
|
|
||||||
The version is detected and reported everywhere; it just no longer *silently* decides. A refusal
|
|
||||||
becomes an informed choice, which is the point — but it stays visibly the operator's choice.
|
|
||||||
|
|
||||||
**What the installer records.** `install.json` stores, per patch, which rung applied it
|
|
||||||
(`stock-hash`, `region-match`, `already-present`) and the hunk offsets it matched. `doctor` and
|
|
||||||
`uninstall` report that: a `region-match` apply on a modified file is a different support story from
|
|
||||||
a clean apply to a stock tree, and the operator should be able to see which one they have without
|
|
||||||
re-deriving it.
|
|
||||||
|
|
||||||
### 2.3 Config paths collide with what the sidecar actually reads
|
### 2.3 Config paths collide with what the sidecar actually reads
|
||||||
|
|
||||||
@@ -216,21 +122,13 @@ Under `C:\Program Files\` that fails or silently lands in VirtualStore. Phase 0.
|
|||||||
half in the sidecar — a relative `[store].path` now resolves against the directory holding
|
half in the sidecar — a relative `[store].path` now resolves against the directory holding
|
||||||
`sidecar.toml`, so pinning the config alone is enough to put the database somewhere deterministic —
|
`sidecar.toml`, so pinning the config alone is enough to put the database somewhere deterministic —
|
||||||
but the config path itself is still CWD-relative by default, and "deterministic" is not the same as
|
but the config path itself is still CWD-relative by default, and "deterministic" is not the same as
|
||||||
"where this install wants it". The service definition therefore always pins the **config** path:
|
"where this install wants it". The service definitions therefore still pin `UOLINK_CONFIG` and
|
||||||
|
`UOLINK_DB_PATH` explicitly:
|
||||||
|
|
||||||
- Linux: config `/etc/runicgateway/sidecar.toml`, db `/var/lib/runicgateway/uo-link.db`, dedicated
|
- Linux: config `/etc/runicgateway/sidecar.toml`, db `/var/lib/runicgateway/uo-link.db`, dedicated
|
||||||
service user
|
service user
|
||||||
- Windows: binary under `%ProgramFiles%\RunicGateway\`, **data under `%ProgramData%\RunicGateway\`**
|
- Windows: binary under `%ProgramFiles%\RunicGateway\`, **data under `%ProgramData%\RunicGateway\`**
|
||||||
|
|
||||||
**How each is pinned differs by platform, and Phase 2 settled it that way deliberately.** Linux's
|
|
||||||
unit carries `Environment=UOLINK_CONFIG=` *and* `Environment=UOLINK_DB_PATH=`, because `/etc` and
|
|
||||||
`/var/lib` are different directories and both need naming. Windows passes the config as `--config`
|
|
||||||
inside the service's own `binPath`, and pins nothing else: config and data are both
|
|
||||||
`%ProgramData%\RunicGateway`, so the sidecar's own anchoring rule already puts the database exactly
|
|
||||||
where the table above says. The alternative on Windows is a **machine-wide** environment variable —
|
|
||||||
`sc.exe` offers no per-service one — which every process on the host would inherit and which would
|
|
||||||
outlive an uninstall. See [Phase 2 as built](#phase-2--uo-link-install-and-service).
|
|
||||||
|
|
||||||
### 2.4 The token handoff was missing entirely
|
### 2.4 The token handoff was missing entirely
|
||||||
|
|
||||||
The whole point is the website reaching the sidecar, and today that is manual and undocumented in
|
The whole point is the website reaching the sidecar, and today that is manual and undocumented in
|
||||||
@@ -329,7 +227,7 @@ The docs must state this up front rather than let users discover it as a scary d
|
|||||||
┌────────┴────────┐ ┌────────┴────────┐
|
┌────────┴────────┐ ┌────────┴────────┐
|
||||||
▼ ▼ ▼ ▼
|
▼ ▼ ▼ ▼
|
||||||
overlay sync patch tier (opt-in) binary install service registration
|
overlay sync patch tier (opt-in) binary install service registration
|
||||||
(never deletes) (per-region rungs) + config + data (systemd / Windows SCM)
|
(never deletes) (git apply + guard) + config + data (systemd / Windows SCM)
|
||||||
```
|
```
|
||||||
|
|
||||||
Each component keeps its own lifecycle. ServUO's existing startup process is untouched.
|
Each component keeps its own lifecycle. ServUO's existing startup process is untouched.
|
||||||
@@ -460,11 +358,19 @@ Repo work that must land before an installer can exist.
|
|||||||
in §6's handoff has a non-interactive equivalent and an unattended install is expressible.
|
in §6's handoff has a non-interactive equivalent and an unattended install is expressible.
|
||||||
- **A modified `Bridge.cfg` must survive an update** — see Phase 1, where this changes the sync
|
- **A modified `Bridge.cfg` must survive an update** — see Phase 1, where this changes the sync
|
||||||
rule inherited from `deploy.ps1`.
|
rule inherited from `deploy.ps1`.
|
||||||
- **Remote-website deployments needed an answer.** `[web] bind` defaults to `127.0.0.1`, which
|
- **Remote-website deployments needed an answer, and it is a reverse proxy.** `[web] bind`
|
||||||
only works when the site runs on the shard host. The guide says to widen it, firewall it to
|
defaults to `127.0.0.1`, which only works when the site runs on the shard host. The guide's
|
||||||
the website's address, and front it with TLS or a VPN off a trusted network — because the
|
recommended arrangement — already in production on a real domain — is to **leave the bind on
|
||||||
token is always required but travels as a plain bearer token over HTTP. `[shard] bind` stays
|
loopback** and put a TLS reverse proxy in front, giving the website the proxied `https://` /
|
||||||
on loopback, since that socket carries inbound commands *into* the game.
|
`wss://` URLs in place of the pair the installer prints from the bind address. Four
|
||||||
|
requirements make that work and are stated with an nginx block that satisfies them: forward
|
||||||
|
the WebSocket upgrade (`/ws` is the whole live feed), pass headers through unmodified (auth is
|
||||||
|
`Authorization: Bearer`, and a stripped `X-UOLink-Version` silently skips the mismatch check),
|
||||||
|
do not buffer and allow long-lived connections (the sidecar pings every 30 s, so a ≥60 s read
|
||||||
|
timeout is safe), and do not log query strings (`?token=` is an accepted auth form). Widening
|
||||||
|
the bind and firewalling the port stays documented as the trusted-LAN alternative, not the
|
||||||
|
default, because on that path the token crosses the network in the clear. `[shard] bind` is
|
||||||
|
never proxied and never widened — that socket carries inbound commands *into* the game.
|
||||||
|
|
||||||
### Phase 1 — installer core
|
### Phase 1 — installer core
|
||||||
|
|
||||||
@@ -485,79 +391,6 @@ Repo work that must land before an installer can exist.
|
|||||||
timestamp.
|
timestamp.
|
||||||
- Idempotent re-runs; a second run with no upstream change reports "unchanged" and writes nothing.
|
- 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
|
### Phase 2 — uo-link install and service
|
||||||
|
|
||||||
- Linux: binary → `/usr/bin/runicgateway-link`, config → `/etc/runicgateway/sidecar.toml`, db →
|
- Linux: binary → `/usr/bin/runicgateway-link`, config → `/etc/runicgateway/sidecar.toml`, db →
|
||||||
@@ -570,148 +403,11 @@ process running out of the tree.
|
|||||||
That both writes the config the service will read and returns the token to print, so the service
|
That both writes the config the service will read and returns the token to print, so the service
|
||||||
never starts against a config that does not exist yet.
|
never starts against a config that does not exist yet.
|
||||||
|
|
||||||
**As built** ([installer#5](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/5)) —
|
|
||||||
`src/sidecar.rs` (binary, config, handoff) and `src/service.rs` (systemd, Windows SCM), wired into
|
|
||||||
the same `install` run. The decisions that were not already settled above:
|
|
||||||
|
|
||||||
- **Both platforms run the sidecar as a dedicated unprivileged identity.** Linux gets the system
|
|
||||||
user this section already specified; Windows gets a **virtual service account**
|
|
||||||
(`sc create … obj= "NT SERVICE\RunicGatewayLink"`), which the SCM creates itself and which has no
|
|
||||||
password. Plain `sc create` would have run it as `LocalSystem` — the most privileged local
|
|
||||||
identity there is, for a process that listens on two TCP ports while its Linux twin deliberately
|
|
||||||
does not run as root. The account only exists *after* `sc create`, which fixes the order of the
|
|
||||||
file permissions below.
|
|
||||||
- **`sidecar.toml` is locked down, because it holds the token.** Neither default location protects
|
|
||||||
it: `/etc` is world-readable and `%ProgramData%` grants `Users` read by inheritance, so an
|
|
||||||
unprivileged local account could read the shard's auth token out of a stock install. Linux gets
|
|
||||||
`chmod 600` plus `chown` to the service user; Windows gets `icacls /inheritance:r` down to SYSTEM
|
|
||||||
and Administrators **before** registration, then a read grant for the service account after it
|
|
||||||
exists. The database directory gets a separate write grant, since SQLite writes journal and WAL
|
|
||||||
files beside the database.
|
|
||||||
- **`--verify` runs no part of the sidecar half.** `--print-config` provisions — it writes the
|
|
||||||
config and mints a token — so a dry run that called it would create exactly the state it claims
|
|
||||||
not to. A `--verify` run reports what would be installed, reads no token, and prints no handoff.
|
|
||||||
It also **carries the existing `link` section of `install.json` through untouched**, so a dry run
|
|
||||||
on an installed host cannot make its service disappear from the record.
|
|
||||||
- **The installed binary's protocol version is checked against the bundle, and a mismatch stops the
|
|
||||||
run before the service is registered.** Gate 1 (§7.1) read that number from source at the release
|
|
||||||
tag; this is the same check applied to the binary that will actually answer the website. The
|
|
||||||
binary is left on disk — harmless without a service — rather than the run pretending to succeed.
|
|
||||||
- **`RUNICGATEWAY_STATE_DIR` now relocates the sidecar binary too, and suppresses service
|
|
||||||
registration.** Phase 1 left the binary path alone because nothing wrote it. A relocated run that
|
|
||||||
still dropped a binary into `/usr/bin` and registered a system service would be exactly the
|
|
||||||
half-in-the-real-system accident the variable exists to avoid — and there is no such thing as a
|
|
||||||
relocated systemd unit or Windows service. Such a run also leaves file permissions alone, because
|
|
||||||
hardening a scratch config against the only account that will ever read it just breaks the next
|
|
||||||
test run.
|
|
||||||
- **A host the installer cannot drive gets the recipe, not a failure or a weaker service.** No
|
|
||||||
systemd (`/run/systemd/system` absent — the correct test, since `systemctl` is present in plenty
|
|
||||||
of containers where PID 1 is not systemd), or a service user that cannot be created: the binary
|
|
||||||
and config are still installed, `install.json` records `service: null`, and the run prints the
|
|
||||||
exact unit text and commands. There is **no fallback to `User=root` or `LocalSystem`** — a service
|
|
||||||
quietly running with more privilege than its own documentation promises is worse than one that was
|
|
||||||
not registered. The printed Windows recipe states plainly whether the run locked the config down
|
|
||||||
or the operator still has to.
|
|
||||||
- **`install.json` never records the token.** The `link` section holds versions, the binary's hash,
|
|
||||||
the config and database paths, and the service's name, unit path and account. The token goes to
|
|
||||||
the terminal and to `sidecar.toml`, and the record is a support artifact people paste into bug
|
|
||||||
reports.
|
|
||||||
- **The service is stopped before its binary is replaced, and restarted rather than started
|
|
||||||
afterwards.** On Windows the file is locked while the service runs (and `sc stop` returns as soon
|
|
||||||
as the stop is *pending*, so the stop is polled, not slept on); on Linux the replacement is
|
|
||||||
permitted but leaves the old code serving until something restarts it. `systemctl start` on an
|
|
||||||
active unit is a no-op, which is precisely the wrong outcome after a replacement.
|
|
||||||
|
|
||||||
Verified on this machine end to end against a relocated layout: the bundle's Windows sidecar
|
|
||||||
downloaded and checksum-verified, `--print-config` provisioning a fresh config and returning a
|
|
||||||
token, the §6 handoff printed with the URLs composed from the host rather than the bind address, a
|
|
||||||
second run reporting `unchanged` / `already present` and leaving `install.json` byte-identical, a
|
|
||||||
`--verify` run over an installed host writing nothing and preserving the `link` section, and a
|
|
||||||
tampered binary detected by hash and replaced with no stray staging file left behind.
|
|
||||||
|
|
||||||
### Phase 3 — patch tier (opt-in)
|
### Phase 3 — patch tier (opt-in)
|
||||||
|
|
||||||
Everything in §2.2. Detect applicability, dry-run, apply, record, warn about the core rebuild, and
|
Everything in §2.2. Detect applicability, dry-run, apply, record, warn about the core rebuild, and
|
||||||
degrade loudly rather than silently.
|
degrade loudly rather than silently.
|
||||||
|
|
||||||
The rung ladder of §2.2.1 is the bulk of the work here: parse each `.patch` into hunks, reconstruct
|
|
||||||
each hunk's pre- and post-image blocks, and resolve the file through rungs 0–3 before writing
|
|
||||||
anything. The unsupported-version path (§2.2.2) is part of this phase, not a later polish — the
|
|
||||||
banner, the defaulted-to-no prompt, the `--patches-unsupported-servuo` flag, and the unsupported
|
|
||||||
marker carried into `install.json`, `doctor` and the uninstall report. Two pieces carry the risk and want direct tests — the hunk parser (headers, `\ No newline
|
|
||||||
at end of file`, CRLF files) and the uniqueness rule (a region that appears twice must fail, not
|
|
||||||
pick the first). Fixtures are cheap: the three stock 57.4 files, each with a hand edit far from the
|
|
||||||
patched region (must reach rung 2), an edit inside it (must reach rung 3), and an already-patched
|
|
||||||
copy (must reach rung 0).
|
|
||||||
|
|
||||||
**As built** ([installer#6](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/6), with the
|
|
||||||
metadata half in [servuo-plugins#10](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/10))
|
|
||||||
— `src/diff.rs` (the parser), `src/patch.rs` (the ladder and the applier) and `src/tier.rs` (consent,
|
|
||||||
writing, reporting, recording), wired into the same `install` run between the overlay sync and the
|
|
||||||
sidecar. The decisions that were not already settled above:
|
|
||||||
|
|
||||||
- **The engine is fully native; `git` is never invoked.** §2.2.1 wrote rung 1 as "apply verbatim
|
|
||||||
with `git apply`", but §1 chose the release tarball precisely so there would be **no git on the
|
|
||||||
shard host**, and rung 2 needs a native applier regardless. One engine now serves both: rung 1
|
|
||||||
keeps its distinct, stronger verdict — the whole file reproduced the diff's `index` pre-image,
|
|
||||||
computed as a git blob SHA1 in process — while the write goes through rung 2's code path. That
|
|
||||||
leaves one set of CRLF and whitespace behaviours to reason about instead of two, and a bug report
|
|
||||||
never has to say which engine ran. It is also not academic: the shipped `.patch` files are CRLF in
|
|
||||||
a Windows checkout while two of their three targets are LF, so `git apply` **refuses** patches
|
|
||||||
this places correctly.
|
|
||||||
- **What a `.patch` cannot say is declared by the release, with a built-in fallback.** Which patches
|
|
||||||
form one all-or-nothing unit, which companion `.cs` follows which, whether a **core** rebuild is
|
|
||||||
needed, and what declining costs are all things a diff does not carry. `servuo-plugins/patches/tier.json`
|
|
||||||
declares them and the release workflow folds them into `manifest.json` as `patch_tier` (§7.0), so
|
|
||||||
adding a patch regenerates release metadata rather than requiring an installer release — the same
|
|
||||||
rule §7.1 applies to the bundle. Overlay `v0.1.1` is in the current bundle and declares nothing,
|
|
||||||
so the installer carries a built-in description of exactly that release; a declared tier always
|
|
||||||
wins. A checked-in fixture of the release workflow's **own jq output** asserts the two descriptions
|
|
||||||
are identical, so the two repos cannot drift apart quietly — the failure mode otherwise is a tier
|
|
||||||
that is silently never offered.
|
|
||||||
- **All-or-nothing gained a second level.** §2.2.1 makes it per *patch file*; the tier is also
|
|
||||||
all-or-nothing per **feature**, because the two vendor-sale patches are one unit — `EventSink.cs`
|
|
||||||
grows the event, `PlayerVendorGumps.cs` raises it, and the companion subscribes to it. Applying
|
|
||||||
either alone yields a tree that does not compile or silently never emits. A patch that could have
|
|
||||||
been placed but was held back by a sibling says so in as many words; reporting it as applied is
|
|
||||||
the exact misreading this tier exists to prevent.
|
|
||||||
- **The pre-image of every patched file is cached**, under `<state>/patches/originals/`, mirroring
|
|
||||||
its path in the ServUO tree. The tier is the only part of the installer that edits a file the
|
|
||||||
operator owns, and this is what turns "here are the hunks we added" into a revert anyone can
|
|
||||||
verify — which matters most for a `region-match` apply, where the surrounding file was already
|
|
||||||
theirs. It lives in the state directory rather than beside the file it copies, because an
|
|
||||||
installer-owned file inside the ServUO tree is one `uninstall` has promised never to clean up. It
|
|
||||||
is written before the first edit and never overwritten, so it stays pre-tier however many times
|
|
||||||
`install` runs.
|
|
||||||
- **Every patch the tier *evaluated* is cached, not only the ones that applied** — a refinement of
|
|
||||||
§2.2's "cache the applied `.patch` files". The refusal message names that path as the file to
|
|
||||||
apply by hand (as §4 of `INSTALL.md` already illustrated), so caching only successes would point
|
|
||||||
an operator at a file the run had decided not to write.
|
|
||||||
- **Rung 0 reuses the previous record whole rather than re-deriving it.** The rung is the
|
|
||||||
support-relevant fact — how did this land? — and a later run re-deriving it answers
|
|
||||||
`already-present` for something that first landed as `region-match`. That flip rewrites
|
|
||||||
`install.json` on the second run of an identical install, which is the same class of bug as the
|
|
||||||
`Bridge.cfg` comparison in Phase 1: a record describing the run instead of the state. A tree
|
|
||||||
patched by hand per `INSTALL.md` Appendix A2 has no prior record, so there `already-present` is
|
|
||||||
correctly what gets minted.
|
|
||||||
- **Declining never erases what an earlier run applied**, and no longer claims a loss that is not
|
|
||||||
real. `--no-patches` and an unselected prompt both carry the previous `patches` section through
|
|
||||||
untouched, as `--verify` does — and the "Without it:" line now names only the features the record
|
|
||||||
does not already show as applied.
|
|
||||||
- **Withholding `--patches-unsupported-servuo` skips the tier loudly rather than failing the run.**
|
|
||||||
By that point the overlay is deployed and the sidecar is about to be installed; turning a completed
|
|
||||||
base install into exit 1 over a tier documented as optional would cost the operator more than the
|
|
||||||
tier is worth. Saying nothing would be the real failure, so it is reported where it happens.
|
|
||||||
|
|
||||||
Verified on this machine against the ServUO 57.4 tree at `C:\Users\colby\Desktop\ServUO`, across
|
|
||||||
four scratch roots built from its real files: a hand-patched tree (rung 0 on both vendor-sale
|
|
||||||
patches), a reverse-applied stock one (**rung 1 on the real `EventSink.cs`**, whose blob hash
|
|
||||||
reproduces the patch's declared `index d30788f` pre-image), a feature resolving at mixed rungs, a
|
|
||||||
tree with edits inside two patched regions (rung 3 — nothing written, the placeable sibling held
|
|
||||||
back, no companions copied, and all three patches cached anyway), and a non-57.4 tree both with and
|
|
||||||
without the extra consent flag. Three consecutive runs left `install.json` byte-identical, the
|
|
||||||
patched files unchanged, and the cached pre-image still pre-patch.
|
|
||||||
|
|
||||||
### Phase 4 — diagnostics and updates
|
### Phase 4 — diagnostics and updates
|
||||||
|
|
||||||
`runicgateway doctor` — the command that makes the whole thing supportable:
|
`runicgateway doctor` — the command that makes the whole thing supportable:
|
||||||
@@ -719,7 +415,7 @@ patched files unchanged, and the cached pre-image still pre-patch.
|
|||||||
```
|
```
|
||||||
✓ ServUO found /opt/ServUO (57.4)
|
✓ ServUO found /opt/ServUO (57.4)
|
||||||
✓ Overlay in sync 24 files, all hashes match install.json
|
✓ Overlay in sync 24 files, all hashes match install.json
|
||||||
⚠ Patch tier 1 of 3 applied (region-match) — vendor.sale unavailable
|
⚠ Patch tier 1 of 3 applied — vendor.sale unavailable
|
||||||
✓ uo-link installed 1.1.0
|
✓ uo-link installed 1.1.0
|
||||||
✓ Service running, enabled
|
✓ Service running, enabled
|
||||||
✓ Sidecar reachable 127.0.0.1:8080 /health ok
|
✓ Sidecar reachable 127.0.0.1:8080 /health ok
|
||||||
@@ -757,7 +453,7 @@ a clever automatic revert risks silently eating their work. It removes and it re
|
|||||||
| Removed | uo-link binary, its service entry (systemd unit / Windows service), `install.json` and the cached patch set |
|
| 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) |
|
| Kept | `sidecar.toml` and `uo-link.db` (config and history survive; `--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** | 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 |
|
| **Printed, not done** | The exact hunks each applied patch added to `EventSink.cs`, `PlayerVendorGumps.cs`, `Logging.cs`, rendered from the cached `.patch` files, for the operator to revert by hand |
|
||||||
|
|
||||||
The printed report is also written to a file, so it survives the terminal scrollback of a long
|
The printed report is also written to a file, so it survives the terminal scrollback of a long
|
||||||
uninstall.
|
uninstall.
|
||||||
@@ -794,6 +490,13 @@ Every value in that block except the host and the site URL comes from one
|
|||||||
`0.0.0.0`, which is not something to hand a website — so the installer composes the URLs from the
|
`0.0.0.0`, which is not something to hand a website — so the installer composes the URLs from the
|
||||||
host it detects or prompts for, rather than echoing the bind address.
|
host it detects or prompts for, rather than echoing the bind address.
|
||||||
|
|
||||||
|
**It does not attempt to detect a reverse proxy**, which is the recommended arrangement for a
|
||||||
|
website on another host (`INSTALL.md` §5). Nothing visible from the sidecar's side says what fronts
|
||||||
|
it, so guessing would produce a confidently wrong `https://` URL. The two printed URLs always
|
||||||
|
describe the sidecar itself, and the guide tells the operator to paste their public `https://` /
|
||||||
|
`wss://` pair instead when there is a proxy. `--host` accepting a full origin later is a cheap
|
||||||
|
improvement if this proves annoying in practice.
|
||||||
|
|
||||||
The installer prompts for the site URL only to build that link; it never contacts the website. A
|
The installer prompts for the site URL only to build that link; it never contacts the website. A
|
||||||
future "installer registers itself with the website" flow (claim code + authenticated endpoint) is
|
future "installer registers itself with the website" flow (claim code + authenticated endpoint) is
|
||||||
explicitly **out of scope** — it is real backend work in a security-sensitive area and can be added
|
explicitly **out of scope** — it is real backend work in a security-sensitive area and can be added
|
||||||
@@ -826,30 +529,14 @@ Shipped inside every `runicgateway-overlay-<ver>.tar.gz`, generated by that repo
|
|||||||
"repo": "RunicGateway/servuo-plugins",
|
"repo": "RunicGateway/servuo-plugins",
|
||||||
"protocol": 3,
|
"protocol": 3,
|
||||||
"servuo": { "min_version": "57.4", "patches_verified_against": "57.4" },
|
"servuo": { "min_version": "57.4", "patches_verified_against": "57.4" },
|
||||||
"patch_tier": {
|
|
||||||
"features": [{
|
|
||||||
"name": "vendor-sale",
|
|
||||||
"summary": "vendor.sale events — player-vendor purchases with buyer, owner, item, price and commission",
|
|
||||||
"lost": "no vendor.sale events",
|
|
||||||
"rebuild": "core",
|
|
||||||
"patches": [
|
|
||||||
{ "name": "playervendor-sale-eventsink", "file": "patches/playervendor-sale-eventsink.patch", "target": "Server/EventSink.cs" },
|
|
||||||
{ "name": "playervendor-sale-gump", "file": "patches/playervendor-sale-gump.patch", "target": "Scripts/Gumps/PlayerVendorGumps.cs" }
|
|
||||||
],
|
|
||||||
"companions": [
|
|
||||||
{ "file": "patches/BridgeVendorSale.cs", "install_to": "Scripts/Custom/Bridge/BridgeVendorSale.cs" }
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"files": { "overlay/Config/Bridge.cfg": "32718424…", "patches/…": "…" }
|
"files": { "overlay/Config/Bridge.cfg": "32718424…", "patches/…": "…" }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`version` and `commit` come from the release engine; `protocol` and the `servuo` block are read from
|
`version` and `commit` come from the release engine; `protocol` and the `servuo` block are read from
|
||||||
`servuo-plugins/overlay.toml`; `patch_tier` is folded in from `servuo-plugins/patches/tier.json`;
|
`servuo-plugins/overlay.toml`; `files` is a SHA256 per shipped file.
|
||||||
`files` is a SHA256 per shipped file.
|
|
||||||
|
|
||||||
Three of these carry weight beyond documentation:
|
Two of these carry weight beyond documentation:
|
||||||
|
|
||||||
- **`protocol` is a hand-maintained declaration, and has to be.** The plugin announces no version on
|
- **`protocol` is a hand-maintained declaration, and has to be.** The plugin announces no version on
|
||||||
the wire and none is queryable before ServUO boots, so nothing in CI can derive it — which makes
|
the wire and none is queryable before ServUO boots, so nothing in CI can derive it — which makes
|
||||||
@@ -860,18 +547,6 @@ Three of these carry weight beyond documentation:
|
|||||||
overlay moved on"** (§5, Phase 4). The installer copies these hashes into `install.json` at deploy
|
overlay moved on"** (§5, Phase 4). The installer copies these hashes into `install.json` at deploy
|
||||||
time; a later mismatch against *both* the manifest and `install.json` means upstream changed, a
|
time; a later mismatch against *both* the manifest and `install.json` means upstream changed, a
|
||||||
mismatch against `install.json` alone means local edits.
|
mismatch against `install.json` alone means local edits.
|
||||||
- **`patch_tier` is everything a `.patch` cannot say about itself**, and is the reason the tier is
|
|
||||||
data rather than code. Which patches form one all-or-nothing unit, which companion `.cs` may only
|
|
||||||
be copied once that unit lands, whether the change needs a **core** solution rebuild or just the
|
|
||||||
dynamic script build, and what the operator loses by declining are none of them derivable from a
|
|
||||||
diff. Declaring them here means adding a patch regenerates release metadata rather than requiring
|
|
||||||
an installer release — the rule §7.1 already applies to the bundle. The maintainer-facing source
|
|
||||||
is `servuo-plugins/patches/tier.json`; the release workflow folds it in and removes the staged
|
|
||||||
copy, so the tarball carries exactly one statement of the table, and gates that every `.patch` is
|
|
||||||
described by exactly one feature, that every named patch and companion exists, and that each
|
|
||||||
declared `target` is the file its diff actually edits. Installers older than this key ignore it;
|
|
||||||
an installer newer than the overlay it is deploying falls back to a built-in description of the
|
|
||||||
release that predates it (see [Phase 3 as built](#phase-3--patch-tier-opt-in)).
|
|
||||||
|
|
||||||
`min_version` and `patches_verified_against` are separate on purpose. The base overlay only *adds*
|
`min_version` and `patches_verified_against` are separate on purpose. The base overlay only *adds*
|
||||||
files and is expected to work broadly; the patch tier diffs stock ServUO files and is verified
|
files and is expected to work broadly; the patch tier diffs stock ServUO files and is verified
|
||||||
@@ -1005,24 +680,18 @@ mismatched pair from being published as a bundle — which is the mechanism that
|
|||||||
|
|
||||||
## 8. Open questions
|
## 8. Open questions
|
||||||
|
|
||||||
1. **Does the installer manage ServUO stop/start?** Currently it refuses while ServUO runs and tells
|
1. **Windows service mechanism** — `sc create` against the plain console binary (simplest, works
|
||||||
|
today), a bundled WinSW/NSSM shim, or a native `--service` mode in the sidecar using the
|
||||||
|
`windows-service` crate (cleanest, but changes `link`). Recommendation: `sc create` for v1,
|
||||||
|
revisit if restart semantics prove inadequate.
|
||||||
|
2. **Does the installer manage ServUO stop/start?** Currently it refuses while ServUO runs and tells
|
||||||
the operator to restart afterward. Offering to stop/start would be friendlier but means owning
|
the operator to restart afterward. Offering to stop/start would be friendlier but means owning
|
||||||
another shard's process lifecycle, and the shard's own start scripts vary.
|
another shard's process lifecycle, and the shard's own start scripts vary.
|
||||||
2. **Co-location assumption** — the shard dials out to the sidecar on loopback `127.0.0.1:7788`, so
|
3. **Co-location assumption** — the shard dials out to the sidecar on loopback `127.0.0.1:7788`, so
|
||||||
sidecar and ServUO must share a host. Should the installer support installing only uo-link on a
|
sidecar and ServUO must share a host. Should the installer support installing only uo-link on a
|
||||||
different host, or hard-assume co-location?
|
different host, or hard-assume co-location?
|
||||||
Resolved and moved into §1 / §2.2 / §5: uninstall scope, and minimum ServUO version.
|
Resolved and moved into §1 / §2.2 / §5: uninstall scope, and minimum ServUO version.
|
||||||
|
|
||||||
**Resolved — Windows service mechanism** (was question 1). `sc create` against the plain console
|
|
||||||
binary, as recommended: it works on a stock host, ships nothing extra, and needs no change to
|
|
||||||
`link`. A WinSW/NSSM shim would be a third binary to keep current, and a native `--service` mode
|
|
||||||
using the `windows-service` crate would put Windows service plumbing inside a component whose whole
|
|
||||||
job is being platform-agnostic. Restart semantics turned out to be adequate —
|
|
||||||
`sc failure … actions= restart/5000` is the direct counterpart of systemd's `Restart=on-failure` /
|
|
||||||
`RestartSec=5`. What the recommendation did *not* anticipate is the service identity: plain
|
|
||||||
`sc create` runs as `LocalSystem`, so Phase 2 registers with `obj= "NT SERVICE\RunicGatewayLink"`
|
|
||||||
instead (see [Phase 2 as built](#phase-2--uo-link-install-and-service)).
|
|
||||||
|
|
||||||
**Resolved — branch targeting for the new repo** (was question 4). The v3 cutover landed:
|
**Resolved — branch targeting for the new repo** (was question 4). The v3 cutover landed:
|
||||||
`servuo-plugins#6` merged, so that repo's `main` and `edge` agree at protocol 3. The release
|
`servuo-plugins#6` merged, so that repo's `main` and `edge` agree at protocol 3. The release
|
||||||
workflow targets `main`, and the installer repo starts clean on `main`. §7.4's caution still applies
|
workflow targets `main`, and the installer repo starts clean on `main`. §7.4's caution still applies
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
# Runic Gateway installer — Project Tree
|
|
||||||
|
|
||||||
> **Auto-generated.** This file is maintained by the `sync-project-tree` CI workflow in
|
|
||||||
> the [`RunicGateway/installer`](https://gitea.whitlocktech.com/RunicGateway/installer) repository, which
|
|
||||||
> opens a pull request here whenever the tracked file layout on `main` changes. Do not edit
|
|
||||||
> by hand — changes will be overwritten by the next sync.
|
|
||||||
|
|
||||||
A snapshot of the tracked files in the repository (build output, dependencies, and other
|
|
||||||
git-ignored paths are excluded).
|
|
||||||
|
|
||||||
```text
|
|
||||||
installer/
|
|
||||||
├── .gitea/
|
|
||||||
│ ├── ISSUE_TEMPLATE/
|
|
||||||
│ │ ├── bug_report.md
|
|
||||||
│ │ ├── config.yaml
|
|
||||||
│ │ └── feature_request.md
|
|
||||||
│ ├── scripts/
|
|
||||||
│ │ └── gen_tree.py
|
|
||||||
│ ├── workflows/
|
|
||||||
│ │ ├── bundle.yml
|
|
||||||
│ │ ├── pr-checks.yml
|
|
||||||
│ │ ├── release.yml
|
|
||||||
│ │ └── sync-project-tree.yml
|
|
||||||
│ └── PULL_REQUEST_TEMPLATE.md
|
|
||||||
├── bundles/
|
|
||||||
│ ├── bundle-2026.08.04.json
|
|
||||||
│ ├── current.json
|
|
||||||
│ └── README.md
|
|
||||||
├── .gitignore
|
|
||||||
├── CODE_OF_CONDUCT.md
|
|
||||||
├── CONTRIBUTING.md
|
|
||||||
├── CONTRIBUTORS.md
|
|
||||||
├── LICENSE.md
|
|
||||||
├── README.md
|
|
||||||
└── SECURITY.md
|
|
||||||
```
|
|
||||||
Reference in New Issue
Block a user