All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m7s
Module-rust phase 18, step 5 of docs/modules/rust/PLAN.md §34.2.7 (D146, D148, D149, D153). Bundles: ServUO is read at schema 2 from v2/servuo/ and lowered into the schema-1 model. Schema 1 at the root is the fallback, so a pin from before schema 2 still reproduces. Rust bundles are read from v2/rust/. v2 reads use the contents API, because /raw/ is CDN-cached for six hours. --game rust runs install, update, doctor and uninstall for Rust servers (src/rustgame/): - the framework is detected from its marker files, which were read off both rigs; both or neither is refused; - --server-id names an instance: its own service (runicgateway-rust@<id>, or RunicGatewayRust-<id>), config, database and ports; - the plugin config is written once, with ServerId and Port only. An existing one is never rewritten, and one naming another server refuses the run; - each instance's sidecar.toml is written once with its ports and an absolute database path, and the sidecar generates the token into it; - one binary per host. update moves every instance, and a replaced binary restarts all of them; - doctor checks the plugin file hash, the plugin config's ServerId, the required uMod plugins (a warning), the service and /health, and passes when the plugin is connected; - uninstall removes our plugin and keeps its config. --purge also removes the sidecar config and database. The last instance takes the binary, the template and the record, and the shared user only when no ServUO record remains. service.rs takes the service name as a parameter internally. The ServUO public API is unchanged. Finding: Carbon 2.0.259's config.json has no folder keys, so carbon/plugins and carbon/configs are what the installer uses. The plan expected a moved directory to be readable there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
126 lines
5.1 KiB
Rust
126 lines
5.1 KiB
Rust
//! Runic Gateway installer.
|
|
//!
|
|
//! Takes a working ServUO installation and connects it to a Runic Gateway website. The design of
|
|
//! record is `docs/installer/PLAN.md`; the operator-facing contract, written before this binary
|
|
//! existed, is `docs/installer/INSTALL.md`.
|
|
//!
|
|
//! **This build implements Phases 1 to 4** — the whole of what `INSTALL.md` describes: bundle
|
|
//! resolution, ServUO detection and validation, the overlay sync, the optional patch tier,
|
|
//! `install.json`, the uo-link sidecar and its service, the token handoff, and the day-two
|
|
//! commands `doctor`, `update` and `uninstall`.
|
|
//!
|
|
//! **`--game rust`** (module-rust phase 18, docs/modules/rust/PLAN.md §34) runs the same four verbs
|
|
//! for Rust servers, in [`rustgame`]: schema-2 Rust bundles, named instances, a Rust-Link sidecar
|
|
//! and service per instance, and the plugin placed for Oxide or Carbon. ServUO's pipeline, files
|
|
//! and record are untouched by it; ServUO bundles are now read at schema 2 with schema 1 as the
|
|
//! fallback ([`bundle::fetch`]).
|
|
//!
|
|
//! Exit codes: `0` success, `1` the run failed, `2` the arguments were unusable — the same
|
|
//! convention as the sidecar's CLI. `doctor` additionally uses `1` for a *completed* run that
|
|
//! found something broken, so it can be read by a monitoring script; a `⚠` row never does that.
|
|
//! `uninstall` does the same for a step it could not carry out — everything else was still removed.
|
|
//!
|
|
//! ## Why the library target is called `rgdeploy`
|
|
//!
|
|
//! Windows applies **UAC installer detection** to unsigned executables whose file name contains
|
|
//! `install`, `setup`, `update` or `patch`: it decides the program is a legacy installer and
|
|
//! demands elevation before the process starts. That is tolerable for the shipped binary, which
|
|
//! needs Administrator anyway and is documented as being run from an elevated shell — but Cargo
|
|
//! names test harnesses after their target, so a target called `runicgateway_installer` produces
|
|
//! `runicgateway_installer-<hash>.exe`, which Windows refuses to launch (`os error 740`) and
|
|
//! `cargo test` cannot run at all on a developer's machine.
|
|
//!
|
|
//! So the code lives in a neutrally-named library, the binary target keeps the published name from
|
|
//! PLAN.md §3, and `[[bin]] test = false` keeps Cargo from building a harness under the triggering
|
|
//! name. Nothing an operator sees changes.
|
|
|
|
pub mod backup;
|
|
pub mod bundle;
|
|
pub mod cli;
|
|
pub mod diff;
|
|
pub mod doctor;
|
|
pub mod install;
|
|
pub mod net;
|
|
pub mod overlay;
|
|
pub mod patch;
|
|
pub mod paths;
|
|
pub mod record;
|
|
pub mod rustgame;
|
|
pub mod service;
|
|
pub mod servuo;
|
|
pub mod sidecar;
|
|
pub mod tier;
|
|
pub mod ui;
|
|
pub mod uninstall;
|
|
pub mod update;
|
|
pub mod util;
|
|
|
|
use cli::{Command, Mode};
|
|
|
|
/// The whole program. Returns the process exit code rather than calling `exit` itself, so the
|
|
/// entry point stays a one-liner and this stays callable from a test.
|
|
pub fn run() -> i32 {
|
|
ui::init_console();
|
|
|
|
let parsed = match cli::parse(std::env::args().skip(1)) {
|
|
Ok(parsed) => parsed,
|
|
Err(message) => {
|
|
eprintln!("error: {message}\n");
|
|
eprint!("{}", cli::USAGE);
|
|
return 2;
|
|
}
|
|
};
|
|
|
|
// Every arm yields the process exit code, because one of them has more than two outcomes:
|
|
// `doctor` completes successfully while reporting a broken deployment, and a monitoring script
|
|
// has to be able to tell that from a healthy one (see `doctor::run`).
|
|
let result: anyhow::Result<i32> = match parsed.mode {
|
|
Mode::Help => {
|
|
print!("{}", cli::USAGE);
|
|
Ok(0)
|
|
}
|
|
Mode::Version => {
|
|
println!("runicgateway-installer {}", env!("CARGO_PKG_VERSION"));
|
|
Ok(0)
|
|
}
|
|
Mode::Run(command) if parsed.game == cli::Game::Rust => rustgame::run(&parsed, command),
|
|
Mode::Run(Command::Install) => install::run(&parsed).map(|()| 0),
|
|
Mode::Run(Command::Update) => update::run(&parsed).map(|()| 0),
|
|
Mode::Run(Command::Doctor) => doctor::run(&parsed),
|
|
Mode::Run(Command::Uninstall) => uninstall::run(&parsed),
|
|
};
|
|
|
|
match result {
|
|
Ok(code) => code,
|
|
Err(error) => {
|
|
// The chain is printed, not just the outermost message: "cannot write
|
|
// /etc/runicgateway/install.json" is only actionable with the OS error still attached.
|
|
eprintln!("\nerror: {error}");
|
|
for cause in error.chain().skip(1) {
|
|
eprintln!(" caused by: {cause}");
|
|
}
|
|
1
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn every_documented_command_has_an_implementation() {
|
|
// The published contract is INSTALL.md §2's four commands. This build answers all of them,
|
|
// so the parser and the dispatcher must not be able to drift apart — an unhandled arm here
|
|
// used to be a "not implemented" message, and is now a compile error by construction.
|
|
for command in [
|
|
Command::Install,
|
|
Command::Doctor,
|
|
Command::Update,
|
|
Command::Uninstall,
|
|
] {
|
|
assert!(cli::USAGE.contains(&command.to_string()), "{command}");
|
|
}
|
|
}
|
|
}
|