Module-rust phase 18, step 4 of docs/modules/rust/PLAN.md §34.2.7.
The Windows service (D149, §34.2.5): src/windows.rs, ported from link's fix
for error 1053. The same exe tries the SCM handshake and falls through to a
console run on 1063; it reports Running only once the listener and store are
up, and logs to a daily file beside its config. One binary serves every
RunicGatewayRust-<id> instance, because the SCM ignores the dispatcher's name
for an own-process service.
An empty environment variable now counts as unset. A Pterodactyl egg exports
every variable it declares, so a blank RUSTLINK_WEB_TOKEN arrived as "" and
overrode the saved token, and a new one was generated and persisted on every
boot. That breaks D152, which this change makes true.
The egg (R20, R22, D151, D152, §34.2.6), in egg/:
- install.sh is egg 18's script with two changes. A wipe guard moves
rust-link/ to /tmp around `rm -rf ${REMOVE_FILES}`. The bridge block then
fetches a schema-2 Rust bundle (pinnable by RUNICGATEWAY_BUNDLE), checks
every asset's sha256 and the plugin's protocol before placing anything, and
places the plugin by FRAMEWORK. Vanilla installs nothing and does not fail.
- with-sidecar.sh is the launcher. It unsets blank variables, builds the web
bind from RUSTLINK_WEB_PORT, and runs --print-config so that a newly
generated token is printed once. It prints the URL and server id for the
admin page, then execs the game. It no longer uses `set -e`: nothing the
bridge gets wrong may keep the game from booting.
- The startup's launcher prefix is conditional, so a server with no bridge
boots exactly as egg 18 does.
- build.sh assembles egg-rust-runicgateway.json. PR Checks runs it.
The release (D145, §34.2.1) reuses servuo-plugins' engine. It publishes the
static musl Linux binary, the Windows exe, the launcher, the egg and
SHA256SUMS, and dispatches the installer's bundle.yml. PR Checks gains a
clippy run for the Windows target.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The rust-link sidecar: it owns the loopback listener the Oxide bridge plugin
dials into, and serves the website a WebSocket feed plus store-backed reads.
Protocol 1 is deliberately three frames — server.hello, ping/pong, and one
correlated server.status — because phase 1's job is to get every seam working at
once with almost nothing in them.
What is load-bearing rather than incidental:
* The plugin is the TCP client and this process owns the listener, so a Rust
server opens no extra port. Loopback is the trust boundary on that link and
there is no token on it; the website-facing surface is the opposite, with auth
always on and a token generated and persisted on first start.
* Inbound lines are capped at 1 MiB from the start rather than after the first
large frame arrives. An over-long line is discarded and the connection stays
up: one malformed frame is not a reason to drop a link live events flow over.
* Store-backed reads answer while the game is off, which is what lets a website
render a server list during a wipe. /status is the one route that fails when
the game is down, and /server answers 204 rather than a null when the game has
never connected -- those are different answers and a client that cannot tell
them apart renders a server that does not exist.
* The two RPC failures get distinct codes. 503 means the game is down; 504 means
it is up and did not answer. Different fixes.
* rpc::REPLY_TIMEOUT is a ceiling every later command budget sits under: core
classifies a budget overrun as retryable unconditionally, so an action whose
budgetMs does not exceed it can never report retry:false.
One defect found while building, which no unit test would have caught: a
four-connection SQLite pool over :memory: hands out four separate empty
databases, because an in-memory database is per connection. It presents as
'no such table' from a random subset of queries. The pool is now capped at one
connection for an in-memory path, which is the only coherent reading of
:memory: and is what makes it usable at all.
Exercised end to end against a live Rust server: a server.hello travelled game
-> sidecar -> module -> the public website API, and killing this process left
the game untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4