All checks were successful
PR Checks / rust-gates (pull_request) Successful in 7m52s
Phase 0.2 of the installer plan (docs/installer/PLAN.md §5). The installer has to drive this binary non-interactively, and today it cannot: the auth token is only readable by scraping the startup log, the config path can only be named through an environment variable, and a relative db path follows the process working directory — which a service manager, not the operator, chooses. - Add a four-flag CLI (cli.rs): --print-config, --config <PATH>, --version, --help. Hand-rolled; an argument-parsing dependency would be larger than the code it replaced. An unrecognized flag exits 2 rather than starting a sidecar that is not the one that was asked for. - --print-config resolves the configuration exactly as a normal start does — including writing a missing config file and generating a blank auth token — and prints it as JSON on stdout: versions, protocol, both bind addresses, ws path, resolved db path, and the token. config_created / token_generated let a re-run tell "read an existing install" from "provisioned a new one". The log subscriber is deliberately not started in this mode, so the document is the whole output. - Anchor a relative [store].path to the config file's directory instead of the CWD, and report resolved absolute paths. A unit pinning UOLINK_CONFIG now keeps its database beside its config rather than in %SystemRoot%\System32 or a VirtualStore redirect. Development is unaffected: under cargo run the two directories are the same. :memory: and file: URIs are left alone. - Hand the db path to sqlx as a filesystem path instead of formatting it into a sqlite:// URL, which percent-decodes it and splits it on '?'. An installed path containing %20 previously opened a different file; verified it now does not. - Create the config's and the database's parent directories when missing, so a service can name /var/lib/runicgateway on a host where nothing made it yet. - 22 unit tests covering argument parsing, path anchoring, token persistence, the generated config template, and the --print-config document. No protocol change: PROTOCOL_VERSION stays 3. Co-Authored-By: Claude <noreply@anthropic.com>
173 lines
6.3 KiB
Rust
173 lines
6.3 KiB
Rust
//! Command-line surface.
|
|
//!
|
|
//! The sidecar is configured by file and environment (see [`crate::config`]); this is deliberately
|
|
//! not a second configuration mechanism. It exists so the binary can be *driven by an installer*
|
|
//! rather than only by a human reading its logs:
|
|
//!
|
|
//! - `--print-config` resolves the configuration exactly as a normal start would — including
|
|
//! generating the auth token on first run — and prints it as JSON on stdout. That is the
|
|
//! supported way to obtain the token for the website's Admin → Shard form. Before this existed,
|
|
//! the only way to read it back was to scrape the startup log or parse `sidecar.toml`.
|
|
//! - `--config <PATH>` names the config file without having to export `UOLINK_CONFIG`, so a
|
|
//! diagnostic run can point at an installed config from any working directory.
|
|
//!
|
|
//! Hand-rolled rather than pulled from a crate: four flags, no subcommands, no completions. A
|
|
//! dependency here would be larger than the code it replaced.
|
|
|
|
/// What this invocation should do. Everything except `Run` prints and exits.
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub enum Mode {
|
|
/// Normal operation: bind the shard listener and the web server.
|
|
Run,
|
|
/// Resolve config, print it as JSON, exit.
|
|
PrintConfig,
|
|
Help,
|
|
Version,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub struct Cli {
|
|
pub mode: Mode,
|
|
/// `--config <PATH>`, which outranks `$UOLINK_CONFIG`.
|
|
pub config: Option<String>,
|
|
}
|
|
|
|
pub const USAGE: &str = "\
|
|
uo-link sidecar — bridges a ServUO shard to the Runic Gateway website.
|
|
|
|
Usage: uo-link-sidecar [OPTIONS]
|
|
|
|
Options:
|
|
--print-config Resolve the configuration, print it as JSON, and exit.
|
|
Runs first-run setup like a normal start does: if the
|
|
config file is missing it is written, and a blank auth
|
|
token is generated and saved. The JSON CONTAINS THE
|
|
AUTH TOKEN in clear text.
|
|
--config <PATH> Path to sidecar.toml. Overrides $UOLINK_CONFIG;
|
|
defaults to ./sidecar.toml.
|
|
-V, --version Print the sidecar and protocol versions and exit.
|
|
-h, --help Print this help and exit.
|
|
|
|
Configuration lives in sidecar.toml; environment variables override the file:
|
|
UOLINK_CONFIG, UOLINK_SHARD_BIND, UOLINK_WEB_BIND, UOLINK_WEB_TOKEN,
|
|
UOLINK_DB_PATH
|
|
";
|
|
|
|
/// Parses arguments **without** the program name.
|
|
///
|
|
/// Returns the message to print on stderr when the arguments are unusable; the caller exits `2`.
|
|
pub fn parse<I: IntoIterator<Item = String>>(args: I) -> Result<Cli, String> {
|
|
let mut mode = Mode::Run;
|
|
let mut config = None;
|
|
let mut it = args.into_iter();
|
|
|
|
while let Some(arg) = it.next() {
|
|
match arg.as_str() {
|
|
"--print-config" => mode = Mode::PrintConfig,
|
|
"-h" | "--help" => {
|
|
return Ok(Cli {
|
|
mode: Mode::Help,
|
|
config,
|
|
})
|
|
}
|
|
"-V" | "--version" => {
|
|
return Ok(Cli {
|
|
mode: Mode::Version,
|
|
config,
|
|
})
|
|
}
|
|
"--config" => {
|
|
// `--config` with nothing after it would otherwise silently fall through and start
|
|
// the sidecar against the default config — the opposite of what was asked for.
|
|
let path = it
|
|
.next()
|
|
.ok_or_else(|| "--config requires a path".to_string())?;
|
|
config = Some(path);
|
|
}
|
|
_ => match arg.strip_prefix("--config=") {
|
|
Some("") => return Err("--config requires a path".into()),
|
|
Some(path) => config = Some(path.to_string()),
|
|
None => return Err(format!("unrecognized argument: {arg}")),
|
|
},
|
|
}
|
|
}
|
|
|
|
Ok(Cli { mode, config })
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn parse_str(args: &[&str]) -> Result<Cli, String> {
|
|
parse(args.iter().map(|s| s.to_string()))
|
|
}
|
|
|
|
#[test]
|
|
fn no_arguments_runs_the_sidecar() {
|
|
let cli = parse_str(&[]).unwrap();
|
|
assert_eq!(cli.mode, Mode::Run);
|
|
assert_eq!(cli.config, None);
|
|
}
|
|
|
|
#[test]
|
|
fn print_config_is_recognized() {
|
|
assert_eq!(
|
|
parse_str(&["--print-config"]).unwrap().mode,
|
|
Mode::PrintConfig
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn config_accepts_both_spellings() {
|
|
let spaced = parse_str(&["--config", "/etc/runicgateway/sidecar.toml"]).unwrap();
|
|
let equals = parse_str(&["--config=/etc/runicgateway/sidecar.toml"]).unwrap();
|
|
assert_eq!(
|
|
spaced.config.as_deref(),
|
|
Some("/etc/runicgateway/sidecar.toml")
|
|
);
|
|
assert_eq!(spaced, equals);
|
|
}
|
|
|
|
#[test]
|
|
fn config_combines_with_print_config() {
|
|
let cli = parse_str(&["--config", "c.toml", "--print-config"]).unwrap();
|
|
assert_eq!(cli.mode, Mode::PrintConfig);
|
|
assert_eq!(cli.config.as_deref(), Some("c.toml"));
|
|
}
|
|
|
|
#[test]
|
|
fn a_path_is_never_swallowed_as_a_flag() {
|
|
// `--config --print-config` takes the next token as the path, wrong as that path is. The
|
|
// alternative — treating it as a missing value — guesses at intent.
|
|
let cli = parse_str(&["--config", "--print-config"]).unwrap();
|
|
assert_eq!(cli.mode, Mode::Run);
|
|
assert_eq!(cli.config.as_deref(), Some("--print-config"));
|
|
}
|
|
|
|
#[test]
|
|
fn config_without_a_value_is_an_error() {
|
|
assert!(parse_str(&["--config"]).is_err());
|
|
assert!(parse_str(&["--config="]).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_arguments_are_rejected() {
|
|
// Silently ignoring a typo'd flag would start a sidecar that is not what was asked for.
|
|
let err = parse_str(&["--pirnt-config"]).unwrap_err();
|
|
assert!(err.contains("--pirnt-config"), "{err}");
|
|
assert!(parse_str(&["/etc/runicgateway/sidecar.toml"]).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn help_and_version_win_immediately() {
|
|
assert_eq!(parse_str(&["--help", "--bogus"]).unwrap().mode, Mode::Help);
|
|
assert_eq!(parse_str(&["-h"]).unwrap().mode, Mode::Help);
|
|
assert_eq!(
|
|
parse_str(&["--version", "--bogus"]).unwrap().mode,
|
|
Mode::Version
|
|
);
|
|
assert_eq!(parse_str(&["-V"]).unwrap().mode, Mode::Version);
|
|
}
|
|
}
|