feat(sidecar): a Windows service, the egg and its launcher, and the first release workflow (phase 18) #13

Merged
whitlocktech merged 1 commits from feat/phase-18-release into edge 2026-09-26 05:34:15 +00:00
Member

Step 4 of the phase-18 build order (docs/modules/rust/PLAN.md §34.2.7), into edge. The cutover follows as its own PR. Its merge cuts this repository's first release, and with Rust-Plugins released first, v2/rust/current.json exists for the first time.

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 back to a console run on error 1063.
    • It reports Running only once the listener and the store are up.
    • It logs to a daily-rolled file beside its config.
  • One binary serves every RunicGatewayRust-<id> instance, because the SCM ignores the dispatcher's name for an own-process service.

A fix D152 needed: an empty variable now counts as unset.

  • A Pterodactyl egg exports every variable it declares, so a blank RUSTLINK_WEB_TOKEN arrives as "".
  • apply_env treated that as an override. It blanked the saved token, and the sidecar generated and persisted a new one on every boot.
  • INSTALL_RIG.md's launcher only avoided exporting empty variables, which does nothing when the panel has already exported them.
  • There are two new unit tests. On Windows, three --print-config runs with RUSTLINK_WEB_TOKEN="" now give token_generated true, false, false, with the same token each time.

The egg (R20, R22, D151, D152, §34.2.6), in egg/:

  • install.sh is egg 18's script with two changes:
    • A wipe guard: rust-link/ is moved to /tmp around rm -rf ${REMOVE_FILES}.
    • The bridge block runs after the wipe:
      • it resolves the schema-2 Rust bundle, pinnable by RUNICGATEWAY_BUNDLE;
      • it checks every asset's sha256, and the plugin manifest's protocol, before placing anything;
      • it places the plugin by FRAMEWORK.
    • It reads the bundle through the contents API rather than /raw/, which is CDN-cached for hours.
  • with-sidecar.sh is the launcher:
    • it unsets blank variables and builds the web bind from RUSTLINK_WEB_PORT;
    • it prints a newly generated token once, then the URL and server id for /admin/rust/servers;
    • it execs the game. The game image has no jq, so the launcher parses with sed.
  • build.sh assembles egg-rust-runicgateway.json, and PR Checks runs it.
  • The variables are the five from §34.2.6's table, with their rules.

The release (D145, §34.2.1)

  • It reuses servuo-plugins' release engine, which is newer than link's: credential preflight, orphan-tag recovery and sweep, retry on 5xx.
  • Assets: a static musl linux-x86_64 binary (checked not to be dynamically linked), the windows-x86_64.exe, with-sidecar.sh, the egg, and SHA256SUMS.
  • It then dispatches the installer's bundle.yml.
  • PR Checks gains clippy for the Windows target, so a service-only fault fails a PR.

Three things for your review

  1. The startup prefix is conditional: $( [ -x ./rust-link/with-sidecar.sh ] && printf %s ./rust-link/with-sidecar.sh ) <egg 18's startup>. §34.4 says vanilla installs nothing and must still boot, but an unconditional prefix would fail to start a vanilla server. This is the same $( … ) pattern the base startup already uses for MAP_URL. Tested through the image's real entrypoint eval: with the launcher present the prefix appears, and without it the startup is egg 18's unchanged.
  2. The launcher has no set -e. Everything before the last line belongs to the bridge, and the last line belongs to the game. My first test hit an unwritable rust-link/, the set -e launcher died before exec, and the game never started. Now each step reports its own failure and the game always starts.
  3. The egg's author is ci@whitlocktech.com, the address the CI jobs already use, because egg 18's author is Pterodactyl's. Tell me if you'd rather it said something else.

Tested

  • Checks:
    • Windows native: fmt, clippy -D warnings, 63 tests.
    • Linux (Docker): fmt, clippy, 59 tests, and the musl release build.
    • Windows-gnu cross (Docker, MinGW): clippy clean and the release .exe built.
  • End to end, with real artifacts and the real images. A mock Gitea served the musl binary, the real launcher, the built egg and the real Rust-Plugins tarball (#13's package step). installer#27's compose-bundles.sh composed v2/rust from them.
  • Install, in ghcr.io/ptero-eggs/installers:debian:
    • Oxide and Carbon: the plugin was placed in the right directory.
    • vanilla: nothing was placed, and the install exited 0.
    • A tampered plugin: "does not match the bundle's sha256", exit 1, nothing placed.
    • A bad pin: refused with its reason, exit 1.
    • Wipe with REMOVE_FILES="* rust-link rust-link/* .*": the map was deleted, and the token and database hashes were unchanged.
  • Launcher, in ghcr.io/pterodactyl/games:rust, with Carbon's LD_PRELOAD set:
    • Boot 1: the token printed once, /health answered on the typed port, and the game was PID 1.
    • Boot 2: no token was printed, and the token on disk was the same.

Still to do, per §34.3: the real walk, importing the released egg in the panel (yours) and the SCM step from an elevated shell (yours).

  • AI-assisted: Claude Code (Claude Opus 5.5)

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

Step 4 of the phase-18 build order (docs/modules/rust/PLAN.md §34.2.7), into `edge`. The cutover follows as its own PR. Its merge cuts this repository's first release, and with Rust-Plugins released first, `v2/rust/current.json` exists for the first time. **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 back to a console run on error 1063. - It reports `Running` only once the listener and the store are up. - It logs to a daily-rolled file beside its config. - One binary serves every `RunicGatewayRust-<id>` instance, because the SCM ignores the dispatcher's name for an own-process service. **A fix D152 needed: an empty variable now counts as unset.** - A Pterodactyl egg exports every variable it declares, so a blank `RUSTLINK_WEB_TOKEN` arrives as `""`. - `apply_env` treated that as an override. It blanked the saved token, and the sidecar generated and persisted a new one on **every boot**. - `INSTALL_RIG.md`'s launcher only avoided *exporting* empty variables, which does nothing when the panel has already exported them. - There are two new unit tests. On Windows, three `--print-config` runs with `RUSTLINK_WEB_TOKEN=""` now give `token_generated` true, false, false, with the same token each time. **The egg (R20, R22, D151, D152, §34.2.6)**, in `egg/`: - **`install.sh`** is egg 18's script with two changes: - **A wipe guard**: `rust-link/` is moved to `/tmp` around `rm -rf ${REMOVE_FILES}`. - **The bridge block** runs after the wipe: - it resolves the schema-2 Rust bundle, pinnable by `RUNICGATEWAY_BUNDLE`; - it checks every asset's sha256, and the plugin manifest's protocol, **before placing anything**; - it places the plugin by `FRAMEWORK`. - It reads the bundle through the **contents API rather than `/raw/`**, which is CDN-cached for hours. - **`with-sidecar.sh`** is the launcher: - it unsets blank variables and builds the web bind from `RUSTLINK_WEB_PORT`; - it prints a newly generated token **once**, then the URL and server id for `/admin/rust/servers`; - it `exec`s the game. The game image has no `jq`, so the launcher parses with `sed`. - **`build.sh`** assembles `egg-rust-runicgateway.json`, and PR Checks runs it. - **The variables** are the five from §34.2.6's table, with their rules. **The release (D145, §34.2.1)** - It reuses servuo-plugins' release engine, which is newer than `link`'s: credential preflight, orphan-tag recovery and sweep, retry on 5xx. - Assets: a static musl `linux-x86_64` binary (checked not to be dynamically linked), the `windows-x86_64.exe`, `with-sidecar.sh`, the egg, and `SHA256SUMS`. - It then dispatches the installer's `bundle.yml`. - PR Checks gains **clippy for the Windows target**, so a service-only fault fails a PR. **Three things for your review** 1. **The startup prefix is conditional**: `$( [ -x ./rust-link/with-sidecar.sh ] && printf %s ./rust-link/with-sidecar.sh ) <egg 18's startup>`. §34.4 says vanilla installs nothing and must still boot, but an unconditional prefix would fail to start a vanilla server. This is the same `$( … )` pattern the base startup already uses for `MAP_URL`. Tested through the image's real entrypoint `eval`: with the launcher present the prefix appears, and without it the startup is egg 18's unchanged. 2. **The launcher has no `set -e`.** Everything before the last line belongs to the bridge, and the last line belongs to the game. My first test hit an unwritable `rust-link/`, the `set -e` launcher died before `exec`, and **the game never started**. Now each step reports its own failure and the game always starts. 3. **The egg's `author`** is `ci@whitlocktech.com`, the address the CI jobs already use, because egg 18's author is Pterodactyl's. Tell me if you'd rather it said something else. **Tested** - **Checks:** - Windows native: fmt, clippy `-D warnings`, 63 tests. - Linux (Docker): fmt, clippy, 59 tests, and the musl release build. - Windows-gnu cross (Docker, MinGW): clippy clean and the release `.exe` built. - **End to end, with real artifacts and the real images.** A mock Gitea served the musl binary, the real launcher, the built egg and the real Rust-Plugins tarball (#13's package step). installer#27's `compose-bundles.sh` composed `v2/rust` from them. - **Install**, in `ghcr.io/ptero-eggs/installers:debian`: - **Oxide** and **Carbon**: the plugin was placed in the right directory. - **vanilla**: nothing was placed, and the install exited 0. - **A tampered plugin:** "does not match the bundle's sha256", exit 1, nothing placed. - **A bad pin:** refused with its reason, exit 1. - **Wipe** with `REMOVE_FILES="* rust-link rust-link/* .*"`: the map was deleted, and the token and database hashes were unchanged. - **Launcher**, in `ghcr.io/pterodactyl/games:rust`, with Carbon's `LD_PRELOAD` set: - **Boot 1:** the token printed once, `/health` answered on the typed port, and the game was PID 1. - **Boot 2:** no token was printed, and the token on disk was the same. **Still to do, per §34.3:** the real walk, importing the released egg in the panel (yours) and the SCM step from an elevated shell (yours). - [x] AI-assisted: Claude Code (Claude Opus 5.5) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 1 commit 2026-09-26 04:15:48 +00:00
feat(sidecar): a Windows service, the egg and its launcher, and the first release workflow (phase 18)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 3m53s
b3b66b1cc2
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
whitlocktech merged commit d8b638773d into edge 2026-09-26 05:34:15 +00:00
whitlocktech deleted branch feat/phase-18-release 2026-09-26 05:34:15 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/Rust-Link#13
No description provided.