//! 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-.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 = 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}"); } } }