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
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:
@@ -187,22 +187,36 @@ impl Config {
|
||||
|
||||
/// Environment overrides, so a deployment can set secrets without editing the file.
|
||||
fn apply_env(&mut self) {
|
||||
if let Ok(v) = env::var("RUSTLINK_GAME_BIND") {
|
||||
self.apply_env_from(|key| env::var(key).ok());
|
||||
}
|
||||
|
||||
/// [`Self::apply_env`] against any lookup, so the rules below are testable without mutating the
|
||||
/// process environment (which the test harness shares across threads).
|
||||
///
|
||||
/// **An empty or blank value is the same as an unset one.** A Pterodactyl egg exports every
|
||||
/// variable it declares, so an operator who leaves `RUSTLINK_WEB_TOKEN` blank arrives here as
|
||||
/// `RUSTLINK_WEB_TOKEN=""`. Honouring that as an override would blank the token saved in
|
||||
/// `sidecar.toml` on every boot, and a fresh one would be generated and persisted each time:
|
||||
/// the website's copy would go stale at every restart (PLAN.md §34, D152). No variable here has
|
||||
/// a meaningful empty value — an empty bind or database path can only fail later, less clearly.
|
||||
fn apply_env_from(&mut self, get: impl Fn(&str) -> Option<String>) {
|
||||
let get = |key: &str| get(key).filter(|v| !v.trim().is_empty());
|
||||
if let Some(v) = get("RUSTLINK_GAME_BIND") {
|
||||
self.game.bind = v;
|
||||
}
|
||||
if let Ok(v) = env::var("RUSTLINK_SERVER_ID") {
|
||||
if let Some(v) = get("RUSTLINK_SERVER_ID") {
|
||||
self.game.server_id = v;
|
||||
}
|
||||
if let Ok(v) = env::var("RUSTLINK_WEB_BIND") {
|
||||
if let Some(v) = get("RUSTLINK_WEB_BIND") {
|
||||
self.web.bind = v;
|
||||
}
|
||||
if let Ok(v) = env::var("RUSTLINK_WEB_TOKEN") {
|
||||
if let Some(v) = get("RUSTLINK_WEB_TOKEN") {
|
||||
self.web.auth_token = v;
|
||||
}
|
||||
if let Ok(v) = env::var("RUSTLINK_DB_PATH") {
|
||||
if let Some(v) = get("RUSTLINK_DB_PATH") {
|
||||
self.store.path = v;
|
||||
}
|
||||
if let Ok(v) = env::var("RUSTLINK_RETAIN_DAYS") {
|
||||
if let Some(v) = get("RUSTLINK_RETAIN_DAYS") {
|
||||
// A malformed value is ignored rather than fatal: this reaches the process as a panel
|
||||
// variable somebody typed (R22), and refusing to start over a stray character would
|
||||
// take the bridge down for a setting that has a perfectly good default.
|
||||
@@ -449,4 +463,34 @@ mod tests {
|
||||
assert_eq!(cfg.store.path, default_db_path());
|
||||
assert_eq!(cfg.game.server_id, "");
|
||||
}
|
||||
|
||||
/// The egg's case (D152): a blank panel variable must not erase the token already saved in the
|
||||
/// file, or a new one would be generated on every boot.
|
||||
#[test]
|
||||
fn an_empty_variable_does_not_override_the_file() {
|
||||
let mut cfg: Config = toml::from_str(&default_file("saved-token")).unwrap();
|
||||
cfg.apply_env_from(|key| match key {
|
||||
"RUSTLINK_WEB_TOKEN" => Some(String::new()),
|
||||
"RUSTLINK_WEB_BIND" => Some(" ".into()),
|
||||
"RUSTLINK_SERVER_ID" => Some(String::new()),
|
||||
_ => None,
|
||||
});
|
||||
assert_eq!(cfg.web.auth_token, "saved-token");
|
||||
assert_eq!(cfg.web.bind, default_web_bind());
|
||||
assert_eq!(cfg.game.server_id, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_set_variable_still_overrides_the_file() {
|
||||
let mut cfg: Config = toml::from_str(&default_file("saved-token")).unwrap();
|
||||
cfg.apply_env_from(|key| match key {
|
||||
"RUSTLINK_WEB_TOKEN" => Some("from-env".into()),
|
||||
"RUSTLINK_WEB_BIND" => Some("0.0.0.0:21009".into()),
|
||||
"RUSTLINK_SERVER_ID" => Some("alpha".into()),
|
||||
_ => None,
|
||||
});
|
||||
assert_eq!(cfg.web.auth_token, "from-env");
|
||||
assert_eq!(cfg.web.bind, "0.0.0.0:21009");
|
||||
assert_eq!(cfg.game.server_id, "alpha");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user