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

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
This commit is contained in:
2026-09-25 23:14:57 -05:00
parent 6aa6cdcf97
commit b3b66b1cc2
13 changed files with 1559 additions and 16 deletions

View File

@@ -20,8 +20,10 @@
//! # Layout
//!
//! `main` does argument handling and nothing else; the sidecar proper lives in [`app`], which is
//! parameterised on `ready`/`shutdown` so that a future service wrapper (the installer's phase)
//! can supply the host's own start and stop without restructuring anything.
//! parameterised on `ready`/`shutdown` so a service wrapper can supply the host's own start and
//! stop. On Windows that wrapper is [`windows`] — the SCM handshake, without which a registered
//! service dies with error 1053 (PLAN.md §34.2.5). On Linux there is none: systemd supervises a
//! console program as it is, and stops it with `SIGTERM`, which [`shutdown_signal`] already hears.
mod app;
mod cli;
@@ -30,6 +32,8 @@ mod game;
mod rpc;
mod store;
mod web;
#[cfg(windows)]
mod windows;
use tracing_subscriber::EnvFilter;
@@ -198,13 +202,20 @@ fn main() -> anyhow::Result<()> {
cli::Mode::Run => {}
}
init_console_tracing();
// The SCM's way in. It falls through to a console run when a human started the process.
#[cfg(windows)]
return windows::run(args.config.as_deref());
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
#[cfg(not(windows))]
{
init_console_tracing();
runtime.block_on(app::run(args.config.as_deref(), || {}, shutdown_signal()))
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
runtime.block_on(app::run(args.config.as_deref(), || {}, shutdown_signal()))
}
}
/// Logging for a foreground run: human-readable, on stdout.
@@ -217,7 +228,7 @@ pub fn init_console_tracing() {
}
/// Resolves on Ctrl-C, and on `SIGTERM` where there is one.
async fn shutdown_signal() {
pub(crate) async fn shutdown_signal() {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};