feat(rust): --game rust, named instances, and schema-2 bundles (phase 18)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m7s
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
This commit is contained in:
128
src/cli.rs
128
src/cli.rs
@@ -56,6 +56,14 @@ pub enum PatchChoice {
|
||||
No,
|
||||
}
|
||||
|
||||
/// Which game's shard side this run is about (docs/modules/rust/PLAN.md §34.2.3). ServUO is the
|
||||
/// default, so every command line written before Rust existed means exactly what it meant.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Game {
|
||||
ServUo,
|
||||
Rust,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
Run(Command),
|
||||
@@ -66,6 +74,15 @@ pub enum Mode {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Cli {
|
||||
pub mode: Mode,
|
||||
/// `--game servuo|rust`.
|
||||
pub game: Game,
|
||||
/// `--rust <path>`: a Rust server root (the directory holding `RustDedicated`).
|
||||
pub rust: Option<String>,
|
||||
/// `--server-id <id>`: names one Rust instance — its service, config, database and ports — and
|
||||
/// is the plugin's `ServerId` (D148).
|
||||
pub server_id: Option<String>,
|
||||
/// `--web-port <port>`: the port the website reaches a Rust instance's sidecar on.
|
||||
pub web_port: Option<u16>,
|
||||
/// `--verify`: report every change that would be made, write nothing.
|
||||
pub verify: bool,
|
||||
/// `--servuo <path>`: name the ServUO root instead of detecting or prompting.
|
||||
@@ -92,6 +109,10 @@ impl Default for Cli {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mode: Mode::Help,
|
||||
game: Game::ServUo,
|
||||
rust: None,
|
||||
server_id: None,
|
||||
web_port: None,
|
||||
verify: false,
|
||||
servuo: None,
|
||||
bundle: None,
|
||||
@@ -107,7 +128,8 @@ impl Default for Cli {
|
||||
}
|
||||
|
||||
pub const USAGE: &str = "\
|
||||
Runic Gateway installer — connects a ServUO shard to a Runic Gateway website.
|
||||
Runic Gateway installer — connects a ServUO shard, or Rust servers, to a Runic
|
||||
Gateway website.
|
||||
|
||||
Usage: runicgateway-installer <COMMAND> [OPTIONS]
|
||||
|
||||
@@ -121,6 +143,7 @@ Commands:
|
||||
ServUO tree — it prints what to remove there.
|
||||
|
||||
Options:
|
||||
--game <servuo|rust> Which game's shard side. Default servuo.
|
||||
--verify install, update. Dry run: report every
|
||||
change that would be made, write nothing.
|
||||
--servuo <PATH> install, doctor, update. The ServUO root,
|
||||
@@ -149,6 +172,16 @@ Options:
|
||||
--purge uninstall. Also delete sidecar.toml,
|
||||
uo-link.db, the cached patch set and every
|
||||
backup, all of which are otherwise kept.
|
||||
Rust (with --game rust):
|
||||
--rust <PATH> install. The Rust server root — the
|
||||
directory holding RustDedicated. Oxide or
|
||||
Carbon is detected, never asked.
|
||||
--server-id <ID> install: required. doctor, uninstall:
|
||||
one instance instead of all. The id the
|
||||
website knows this server by, and the
|
||||
name of its sidecar's service.
|
||||
--web-port <PORT> install. The sidecar's website port.
|
||||
Default: the first free from 8090.
|
||||
-V, --version Print the installer version and exit.
|
||||
-h, --help Print this help and exit.
|
||||
|
||||
@@ -199,6 +232,30 @@ pub fn parse<I: IntoIterator<Item = String>>(args: I) -> Result<Cli, String> {
|
||||
"--bundle" => cli.bundle = Some(take_value(&name, inline, &mut it)?),
|
||||
"--host" => cli.host = Some(take_value(&name, inline, &mut it)?),
|
||||
"--site-url" => cli.site_url = Some(take_value(&name, inline, &mut it)?),
|
||||
"--game" => {
|
||||
cli.game = match take_value(&name, inline, &mut it)?.as_str() {
|
||||
"servuo" => Game::ServUo,
|
||||
"rust" => Game::Rust,
|
||||
other => return Err(format!("--game must be servuo or rust, not {other:?}")),
|
||||
}
|
||||
}
|
||||
"--rust" => cli.rust = Some(take_value(&name, inline, &mut it)?),
|
||||
"--server-id" => {
|
||||
let id = take_value(&name, inline, &mut it)?;
|
||||
if !valid_server_id(&id) {
|
||||
return Err(format!(
|
||||
"--server-id {id:?} is not a valid server id: lowercase letters, digits \
|
||||
and '-', 1 to 64 characters, not starting with '-' (the website's rule)"
|
||||
));
|
||||
}
|
||||
cli.server_id = Some(id);
|
||||
}
|
||||
"--web-port" => {
|
||||
let raw = take_value(&name, inline, &mut it)?;
|
||||
cli.web_port = Some(raw.parse::<u16>().ok().filter(|p| *p >= 1024).ok_or_else(
|
||||
|| format!("--web-port must be a port from 1024 to 65535, not {raw:?}"),
|
||||
)?);
|
||||
}
|
||||
other if other.starts_with('-') => {
|
||||
return Err(format!("unrecognized argument: {other}"))
|
||||
}
|
||||
@@ -212,6 +269,20 @@ pub fn parse<I: IntoIterator<Item = String>>(args: I) -> Result<Cli, String> {
|
||||
}
|
||||
}
|
||||
|
||||
// The Rust flags belong to `--game rust` only. Silently ignoring one on a ServUO run would leave
|
||||
// an operator believing they had named a server they had not.
|
||||
if cli.game == Game::ServUo {
|
||||
for (flag, set) in [
|
||||
("--rust", cli.rust.is_some()),
|
||||
("--server-id", cli.server_id.is_some()),
|
||||
("--web-port", cli.web_port.is_some()),
|
||||
] {
|
||||
if set {
|
||||
return Err(format!("{flag} needs --game rust"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match command {
|
||||
Some(c) => cli.mode = Mode::Run(c),
|
||||
// No verb is not an error worth an exit code — it is someone typing the binary's name to
|
||||
@@ -221,6 +292,17 @@ pub fn parse<I: IntoIterator<Item = String>>(args: I) -> Result<Cli, String> {
|
||||
Ok(cli)
|
||||
}
|
||||
|
||||
/// The website's rule for a server id, `^[a-z0-9][a-z0-9-]{0,63}$`. The same rule the plugin and the
|
||||
/// egg apply, so an id that passes here is one all three accept.
|
||||
pub fn valid_server_id(id: &str) -> bool {
|
||||
!id.is_empty()
|
||||
&& id.len() <= 64
|
||||
&& !id.starts_with('-')
|
||||
&& id
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
|
||||
}
|
||||
|
||||
/// Pulls a flag's value, from `--flag=value` or from the next token.
|
||||
///
|
||||
/// A missing value is an error rather than a default: `--servuo` with nothing after it would
|
||||
@@ -339,6 +421,50 @@ mod tests {
|
||||
assert!(parse_str(&["install", "update"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn servuo_stays_the_default_game() {
|
||||
assert_eq!(parse_str(&["install"]).unwrap().game, Game::ServUo);
|
||||
let rust = parse_str(&[
|
||||
"install",
|
||||
"--game",
|
||||
"rust",
|
||||
"--rust",
|
||||
"/srv/rust",
|
||||
"--server-id",
|
||||
"alpha",
|
||||
"--web-port=8091",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(rust.game, Game::Rust);
|
||||
assert_eq!(rust.server_id.as_deref(), Some("alpha"));
|
||||
assert_eq!(rust.web_port, Some(8091));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rust_flag_without_game_rust_is_refused() {
|
||||
for args in [
|
||||
vec!["install", "--rust", "/srv/rust"],
|
||||
vec!["doctor", "--server-id", "alpha"],
|
||||
vec!["install", "--web-port", "8091"],
|
||||
] {
|
||||
let err = parse_str(&args).unwrap_err();
|
||||
assert!(err.contains("--game rust"), "{args:?}: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_server_id_follows_the_websites_rule() {
|
||||
for good in ["main", "alpha", "eu-2", "0", &"a".repeat(64)] {
|
||||
assert!(valid_server_id(good), "{good}");
|
||||
}
|
||||
for bad in ["", "-a", "Alpha", "a_b", "a.b", &"a".repeat(65)] {
|
||||
assert!(!valid_server_id(bad), "{bad}");
|
||||
}
|
||||
assert!(parse_str(&["install", "--game", "rust", "--server-id", "Bad"]).is_err());
|
||||
assert!(parse_str(&["install", "--game", "rust", "--web-port", "80"]).is_err());
|
||||
assert!(parse_str(&["install", "--game", "minecraft"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn order_does_not_matter() {
|
||||
let a = parse_str(&["--verify", "install", "--yes"]).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user