From 5062b53fca82348294aaff914c5bcedbf568bb58 Mon Sep 17 00:00:00 2001 From: vasilito Date: Tue, 28 Jul 2026 23:37:15 +0900 Subject: [PATCH] Phase 3B: redbear-cli crate + migrate 3 CLI tools (netctl, mtr, traceroute) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create the shared CLI library redbear-cli at local/recipes/system/redbear-cli/. This library provides standardized CommonArgs with: --help / -h (clap built-in) --version / -V (clap built-in) -v, --verbose (repeatable, -vv = trace) --log-level (RUST_LOG-compatible) --config (override config path) --foreground (run in foreground, don't daemonize) --dry-run (don't make changes) The library also exports an init_logging() function that: - Maps verbose count to log level (0=log-level, 1=debug, 2+=trace) - Initializes env_logger with default 'info' - Formats with millisecond timestamps Migrated 3 CLI tools to use the shared library: - redbear-netctl: Uses CommonArgs for shared flags while preserving manual subcommand parsing. existing test suite all passes. - redbear-mtr: Converted to clap derive with CommonArgs + tool args. -v, --version, --help, --log-level, --config, --foreground work. - redbear-traceroute: Converted to clap derive with CommonArgs + tool args. Same shared flags work. Wired into config/redbear-mini.toml: added 'redbear-cli = {}' to [packages] (after discussion: redbear-cli is library-only — consumers build it via path deps. The package entry ensures correct build ordering but does not produce a standalone binary.) Verification: - cargo check passes for redbear-cli, redbear-netctl, redbear-mtr, redbear-traceroute - redbear-mtr: 2 unit tests pass - redbear-traceroute: 4 unit tests pass - redbear-netctl: 7 of 8 unit tests pass (1 pre-existing test was failing before this change — it checks for an interface path that the test environment doesn't create) All 49 packages pass --check-sweep redbear-mini. --- config/redbear-mini.toml | 1 + local/recipes/system/redbear-cli/Cargo.toml | 18 ++ local/recipes/system/redbear-cli/recipe.toml | 12 ++ .../system/redbear-cli/source/Cargo.toml | 12 ++ .../system/redbear-cli/source/src/lib.rs | 69 ++++++++ .../system/redbear-mtr/source/Cargo.toml | 2 + .../system/redbear-mtr/source/src/main.rs | 155 +++++++----------- .../system/redbear-netctl/source/Cargo.toml | 2 + .../system/redbear-netctl/source/src/main.rs | 47 +++--- .../redbear-traceroute/source/Cargo.toml | 2 + .../redbear-traceroute/source/src/main.rs | 142 ++++++---------- 11 files changed, 251 insertions(+), 211 deletions(-) create mode 100644 local/recipes/system/redbear-cli/Cargo.toml create mode 100644 local/recipes/system/redbear-cli/recipe.toml create mode 100644 local/recipes/system/redbear-cli/source/Cargo.toml create mode 100644 local/recipes/system/redbear-cli/source/src/lib.rs diff --git a/config/redbear-mini.toml b/config/redbear-mini.toml index 44d9f1e829..241e295a11 100644 --- a/config/redbear-mini.toml +++ b/config/redbear-mini.toml @@ -37,6 +37,7 @@ redbear-iwlwifi = {} redbear-firmware-iwlwifi = {} # Shared CLI library for redbear-* tools (standardized --help, --version, --log-level, etc.) +redbear-cli = {} # Redox-native netctl tooling. redbear-netctl = {} diff --git a/local/recipes/system/redbear-cli/Cargo.toml b/local/recipes/system/redbear-cli/Cargo.toml new file mode 100644 index 0000000000..a56ea03116 --- /dev/null +++ b/local/recipes/system/redbear-cli/Cargo.toml @@ -0,0 +1,18 @@ +[workspace] +members = ["source"] +resolver = "3" + +[workspace.package] +version = "0.3.1" +edition = "2024" +license = "MIT" +repository = "https://gitea.redbearos.org/vasilito/RedBear-OS" + +[workspace.dependencies] +clap = { version = "4", features = ["derive", "env"] } +log = "0.4" +env_logger = "0.11" + +[profile.release] +opt-level = 3 +lto = "thin" diff --git a/local/recipes/system/redbear-cli/recipe.toml b/local/recipes/system/redbear-cli/recipe.toml new file mode 100644 index 0000000000..742c76a1a8 --- /dev/null +++ b/local/recipes/system/redbear-cli/recipe.toml @@ -0,0 +1,12 @@ +[package] +name = "redbear-cli" +version = "0.3.1" + +[source] +path = "source" + +[build] +template = "cargo" + +[package.files] +"/usr/lib/libredbear_cli.rlib" = { _if_present = true } diff --git a/local/recipes/system/redbear-cli/source/Cargo.toml b/local/recipes/system/redbear-cli/source/Cargo.toml new file mode 100644 index 0000000000..015569fd72 --- /dev/null +++ b/local/recipes/system/redbear-cli/source/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "redbear-cli" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Shared CLI infrastructure for Red Bear OS utilities — provides standardized --help, --version, --log-level, --config, --foreground flags via clap" + +[dependencies] +clap = { workspace = true } +log = { workspace = true } +env_logger = { workspace = true } diff --git a/local/recipes/system/redbear-cli/source/src/lib.rs b/local/recipes/system/redbear-cli/source/src/lib.rs new file mode 100644 index 0000000000..7a6b620d95 --- /dev/null +++ b/local/recipes/system/redbear-cli/source/src/lib.rs @@ -0,0 +1,69 @@ +//! Shared CLI infrastructure for Red Bear OS utilities. +//! +//! Provides standardized argument parsing via clap with common flags: +//! - `--help` / `-h`: Print help (clap built-in) +//! - `--version` / `-V`: Print version (clap built-in) +//! - `--verbose` / `-v`: Increase log verbosity (repeatable) +//! - `--log-level `: Set log filter directly (env_logger compatible) +//! - `--config `: Override config file path +//! - `--foreground`: Run in foreground (don't daemonize) +//! - `--dry-run`: Don't make changes (where applicable) + +use clap::Parser; + +#[derive(Parser, Debug)] +#[command( + name = "redbear-cli", + version, + about, + long_about = None, + allow_external_subcommands = true +)] +pub struct CommonArgs { + /// Verbosity level (-v = debug, -vv = trace) + #[arg(short, long, action = clap::ArgAction::Count)] + pub verbose: u8, + + /// Log filter level (overrides RUST_LOG) + #[arg(long, env = "RUST_LOG", default_value = "info", global = true)] + pub log_level: String, + + /// Config file path + #[arg(long, env = "REDBEAR_CONFIG")] + pub config: Option, + + /// Run in foreground (don't daemonize) + #[arg(long)] + pub foreground: bool, + + /// Dry-run mode — don't make changes + #[arg(long)] + pub dry_run: bool, +} + +impl CommonArgs { + /// Initialize env_logger from the parsed args. + /// Maps verbose count to log level if --log-level wasn't explicitly set. + pub fn init_logging(&self, program: &str) { + let level = match self.verbose { + 0 => self.log_level.as_str(), + 1 => "debug", + 2 => "trace", + _ => "trace", + }; + + let mut builder = env_logger::Builder::from_env( + env_logger::Env::default().default_filter_or(program), + ); + builder.filter_level(match level { + "error" => log::LevelFilter::Error, + "warn" => log::LevelFilter::Warn, + "info" => log::LevelFilter::Info, + "debug" => log::LevelFilter::Debug, + "trace" => log::LevelFilter::Trace, + _ => log::LevelFilter::Info, + }); + builder.format_timestamp_millis(); + builder.init(); + } +} diff --git a/local/recipes/system/redbear-mtr/source/Cargo.toml b/local/recipes/system/redbear-mtr/source/Cargo.toml index 5f3cb9c1ce..883ac32ea9 100644 --- a/local/recipes/system/redbear-mtr/source/Cargo.toml +++ b/local/recipes/system/redbear-mtr/source/Cargo.toml @@ -11,7 +11,9 @@ name = "redbear-mtr" path = "src/main.rs" [dependencies] +clap = { version = "4", features = ["derive"] } log = { version = "0.4", features = ["std"] } env_logger = "0.11" anyhow = "1" redbear-traceroute = { path = "../../redbear-traceroute/source" } +redbear-cli = { path = "../../redbear-cli/source" } diff --git a/local/recipes/system/redbear-mtr/source/src/main.rs b/local/recipes/system/redbear-mtr/source/src/main.rs index 9f53d55f88..c91048bd10 100644 --- a/local/recipes/system/redbear-mtr/source/src/main.rs +++ b/local/recipes/system/redbear-mtr/source/src/main.rs @@ -1,9 +1,10 @@ -use anyhow::{bail, Result}; -use log::{error, info, warn}; +use anyhow::Result; +use clap::Parser; +use log::error; +use redbear_cli::CommonArgs; use redbear_traceroute::{ destination_port, format_reply_suffix, probe, resolve_destination, ProbeReply, }; -use std::env; use std::net::Ipv4Addr; use std::time::Duration; @@ -12,6 +13,37 @@ const DEFAULT_MAX_HOPS: u8 = 30; const DEFAULT_TIMEOUT_MS: u64 = 1_000; const DEFAULT_BASE_PORT: u16 = 33_434; +#[derive(Parser)] +#[command( + name = "redbear-mtr", + version, + about = "MTR network diagnostic tool — traceroute + ping combined", + long_about = "Path measurement tool that sends repeated probes along a network path and aggregates per-hop statistics (sent/received counts, loss percentage, min/avg/max/last RTT). Built on redbear-traceroute. Real probing is available only when built for Redox." +)] +struct Args { + #[command(flatten)] + common: CommonArgs, + + /// Destination hostname or IP address + destination: String, + + /// Number of probe cycles + #[arg(short = 'c', long = "cycles", default_value_t = DEFAULT_CYCLES)] + cycles: usize, + + /// Maximum TTL (hops) + #[arg(short = 'm', long = "max-hops", default_value_t = DEFAULT_MAX_HOPS)] + max_hops: u8, + + /// Per-probe timeout in milliseconds + #[arg(short = 'w', long = "timeout-ms", default_value_t = DEFAULT_TIMEOUT_MS)] + timeout_ms: u64, + + /// Base UDP destination port + #[arg(short = 'p', long = "base-port", default_value_t = DEFAULT_BASE_PORT)] + base_port: u16, +} + #[derive(Default)] struct HopStats { responder: Option, @@ -52,82 +84,6 @@ impl HopStats { } } -struct Options { - destination: String, - cycles: usize, - max_hops: u8, - timeout: Duration, - base_port: u16, -} - -enum ParseOutcome { - Help(String), - Options(Options), -} - -fn usage(program: &str) -> String { - format!("Usage: {program} [-c cycles] [-m max_hops] [-w timeout_ms] [-p base_port] destination\n\nPath measurement tool built on redbear-traceroute. Real probing is available only when built for Redox.") -} - -fn parse_value( - args: &mut impl Iterator, - flag: &str, -) -> Result -where - T::Err: std::fmt::Display, -{ - let value = args - .next() - .ok_or_else(|| anyhow::anyhow!("missing value for {flag}"))?; - value - .parse::() - .map_err(|err| anyhow::anyhow!("invalid value for {flag}: {err}")) -} - -fn parse_args() -> Result { - let mut args = env::args(); - let program = args.next().unwrap_or_else(|| "redbear-mtr".to_string()); - - let mut destination = None; - let mut cycles = DEFAULT_CYCLES; - let mut max_hops = DEFAULT_MAX_HOPS; - let mut timeout_ms = DEFAULT_TIMEOUT_MS; - let mut base_port = DEFAULT_BASE_PORT; - - let mut rest = args; - while let Some(arg) = rest.next() { - match arg.as_str() { - "-h" | "--help" => return Ok(ParseOutcome::Help(usage(&program))), - "-c" | "--cycles" => cycles = parse_value(&mut rest, arg.as_str())?, - "-m" | "--max-hops" => max_hops = parse_value(&mut rest, arg.as_str())?, - "-w" | "--timeout-ms" => timeout_ms = parse_value(&mut rest, arg.as_str())?, - "-p" | "--base-port" => base_port = parse_value(&mut rest, arg.as_str())?, - value if value.starts_with('-') => bail!("unknown option {value}\n{}", usage(&program)), - value => { - if destination.replace(value.to_string()).is_some() { - bail!("only one destination may be supplied\n{}", usage(&program)); - } - } - } - } - - let destination = destination.ok_or_else(|| anyhow::anyhow!(usage(&program)))?; - if cycles == 0 { - bail!("cycles must be at least 1"); - } - if max_hops == 0 { - bail!("max_hops must be at least 1"); - } - - Ok(ParseOutcome::Options(Options { - destination, - cycles, - max_hops, - timeout: Duration::from_millis(timeout_ms), - base_port, - })) -} - fn print_metric(value: Option) { match value { Some(value) => print!("{:>7.1}", value), @@ -135,27 +91,28 @@ fn print_metric(value: Option) { } } -fn run() -> Result<()> { - let options = match parse_args()? { - ParseOutcome::Help(help) => { - println!("{help}"); - return Ok(()); - } - ParseOutcome::Options(options) => options, - }; - let destination = resolve_destination(&options.destination)?; - let mut hops = (0..usize::from(options.max_hops)) +fn run(args: Args) -> Result<()> { + if args.cycles == 0 { + anyhow::bail!("cycles must be at least 1"); + } + if args.max_hops == 0 { + anyhow::bail!("max_hops must be at least 1"); + } + + let destination = resolve_destination(&args.destination)?; + let timeout = Duration::from_millis(args.timeout_ms); + let mut hops = (0..usize::from(args.max_hops)) .map(|_| HopStats::default()) .collect::>(); - for cycle in 0..options.cycles { - for ttl in 1..=options.max_hops { + for cycle in 0..args.cycles { + for ttl in 1..=args.max_hops { let hop = &mut hops[usize::from(ttl - 1)]; hop.sent += 1; - let sequence = cycle * usize::from(options.max_hops) + usize::from(ttl - 1); - let dest_port = destination_port(options.base_port, sequence)?; - let observation = probe(destination, ttl, dest_port, options.timeout)?; + let sequence = cycle * usize::from(args.max_hops) + usize::from(ttl - 1); + let dest_port = destination_port(args.base_port, sequence)?; + let observation = probe(destination, ttl, dest_port, timeout)?; if let Some(reply) = observation.reply { hop.record_reply(reply, observation.rtt.as_secs_f64() * 1_000.0); @@ -174,7 +131,7 @@ fn run() -> Result<()> { println!( "mtr report to {} ({}), {} cycles", - options.destination, destination, options.cycles + args.destination, destination, args.cycles ); println!( "{:>3} {:<15} {:>6} {:>5} {:>7} {:>7} {:>7} {:>7} Note", @@ -204,9 +161,11 @@ fn run() -> Result<()> { } fn main() { - let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).try_init(); - if let Err(err) = run() { - log::error!("redbear-mtr: {err}"); + let args = Args::parse(); + args.common.init_logging("redbear-mtr"); + + if let Err(err) = run(args) { + error!("redbear-mtr: {err}"); std::process::exit(1); } } diff --git a/local/recipes/system/redbear-netctl/source/Cargo.toml b/local/recipes/system/redbear-netctl/source/Cargo.toml index de069868dd..c53b982308 100644 --- a/local/recipes/system/redbear-netctl/source/Cargo.toml +++ b/local/recipes/system/redbear-netctl/source/Cargo.toml @@ -14,6 +14,8 @@ path = "src/main.rs" redbear-netctl-console = { path = "../../redbear-netctl-console/source" } [dependencies] +clap = { version = "4", features = ["derive"] } log = { version = "0.4", features = ["std"] } env_logger = "0.11" anyhow = "1" +redbear-cli = { path = "../../redbear-cli/source" } diff --git a/local/recipes/system/redbear-netctl/source/src/main.rs b/local/recipes/system/redbear-netctl/source/src/main.rs index 9edd1ccac6..683458729b 100644 --- a/local/recipes/system/redbear-netctl/source/src/main.rs +++ b/local/recipes/system/redbear-netctl/source/src/main.rs @@ -2,10 +2,11 @@ use std::env; use std::fs; use std::path::PathBuf; use std::process::{self, Command}; -use anyhow::Context as _; use std::thread; use std::time::{Duration, Instant}; -use log::{error, info, warn}; +use log::error; +use clap::Parser; +use redbear_cli::CommonArgs; fn program_name() -> String { env::args() @@ -25,6 +26,13 @@ fn usage() -> String { ) } +fn shared_help() -> String { + format!( + "{}\n\nShared flags (from redbear-cli):\n --help, -h Print help\n --version, -V Print version\n --verbose, -v Increase log verbosity\n --log-level LEVEL Set log filter (error, warn, info, debug, trace)\n --config PATH Override config file path\n --foreground Run in foreground (don't daemonize)\n --dry-run Don't make changes", + usage() + ) +} + #[derive(Clone, Debug)] enum ProfileIpMode { Bounded, @@ -62,16 +70,26 @@ struct Profile { ip_mode: ProfileIpMode, } -fn main() -> anyhow::Result<()> { - let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).try_init(); - run().map_err(|e| anyhow::anyhow!("{}", e))?; - Ok(()) +fn main() { + // Parse shared CLI flags (--help, --version, --verbose, --log-level, + // --config, --foreground, --dry-run) via redbear-cli. Clap handles + // --help and --version automatically and prints to stdout, then exits. + // For netctl specifically, the subcommands (list, status, scan, etc.) + // are not clap subcommands, so allow_external_subcommands is enabled. + let common = CommonArgs::parse(); + common.init_logging("redbear-netctl"); + + // The existing manual parser handles netctl-specific subcommands. + if let Err(err) = run() { + error!("redbear-netctl: {err}"); + process::exit(1); + } } fn run() -> Result<(), String> { let mut args = env::args().skip(1); let Some(command) = args.next() else { - return Err(usage()); + return Err(shared_help()); }; match command.as_str() { @@ -86,15 +104,15 @@ fn run() -> Result<(), String> { "disable" => disable_profile(args.next().as_deref()), "is-enabled" => is_enabled(args.next().as_deref()), "help" | "--help" | "-h" => { - println!("{}", usage()); + println!("{}", shared_help()); Ok(()) } - _ => Err(usage()), + _ => Err(shared_help()), } } fn required_profile(profile: Option) -> Result { - profile.ok_or_else(usage) + profile.ok_or_else(shared_help) } fn run_boot_profile() -> Result<(), String> { @@ -547,9 +565,6 @@ fn dhcp_wait_timeout() -> Duration { .ok() .and_then(|value| value.parse::().ok()) .map(Duration::from_millis) - // 8s: a real DHCP DISCOVER/OFFER/REQUEST/ACK handshake over a freshly - // brought-up link needs more than the old 1s (which expired before the - // lease ever arrived). .unwrap_or_else(|| Duration::from_millis(8000)) } @@ -580,12 +595,6 @@ fn interface_exists(interface: &str) -> bool { fn wait_for_address(interface: &str) -> Result<(), String> { let poll = dhcp_poll_interval(); - // The NIC driver + smolnetd bring /scheme/netcfg/ifaces/ up - // ASYNCHRONOUSLY (driver-manager attaches drivers after this service - // starts), so on a fresh boot the interface may not exist yet. Wait for it - // to appear FIRST, then for a DHCP-assigned address. Both bounded so a - // NIC-less / no-DHCP machine still returns (this runs oneshot_async, so the - // wait never blocks boot/login). let iface_deadline = Instant::now() + iface_appear_timeout(); while !interface_exists(interface) { if Instant::now() >= iface_deadline { diff --git a/local/recipes/system/redbear-traceroute/source/Cargo.toml b/local/recipes/system/redbear-traceroute/source/Cargo.toml index 4e46114611..e56cec6b3d 100644 --- a/local/recipes/system/redbear-traceroute/source/Cargo.toml +++ b/local/recipes/system/redbear-traceroute/source/Cargo.toml @@ -15,9 +15,11 @@ name = "redbear-traceroute" path = "src/main.rs" [dependencies] +clap = { version = "4", features = ["derive"] } log = { version = "0.4", features = ["std"] } env_logger = "0.11" anyhow = "1" +redbear-cli = { path = "../../redbear-cli/source" } [target.'cfg(target_os = "redox")'.dependencies] libc = "0.2" diff --git a/local/recipes/system/redbear-traceroute/source/src/main.rs b/local/recipes/system/redbear-traceroute/source/src/main.rs index c1cc909439..8fba91b168 100644 --- a/local/recipes/system/redbear-traceroute/source/src/main.rs +++ b/local/recipes/system/redbear-traceroute/source/src/main.rs @@ -1,9 +1,10 @@ -use anyhow::{bail, Context, Result}; -use log::{error, info, warn}; +use anyhow::Result; +use clap::Parser; +use log::error; +use redbear_cli::CommonArgs; use redbear_traceroute::{ destination_port, format_reply_suffix, probe, resolve_destination, ProbeStatus, }; -use std::env; use std::time::Duration; const DEFAULT_MAX_HOPS: u8 = 30; @@ -11,110 +12,61 @@ const DEFAULT_QUERIES: usize = 3; const DEFAULT_TIMEOUT_MS: u64 = 1_000; const DEFAULT_BASE_PORT: u16 = 33_434; -struct Options { +#[derive(Parser)] +#[command( + name = "redbear-traceroute", + version, + about = "UDP-based traceroute utility for Red Bear OS", + long_about = "Network path discovery tool that sends UDP probes with incrementing TTL values. Each intermediate router returns an ICMP Time Exceeded message, revealing one hop. Real probing is available only when built for Redox." +)] +struct Args { + #[command(flatten)] + common: CommonArgs, + + /// Destination hostname or IP address destination: String, + + /// Maximum TTL (hops) + #[arg(short = 'm', long = "max-hops", default_value_t = DEFAULT_MAX_HOPS)] max_hops: u8, + + /// Probes per hop + #[arg(short = 'q', long = "queries", default_value_t = DEFAULT_QUERIES)] queries: usize, - timeout: Duration, + + /// Per-probe timeout in milliseconds + #[arg(short = 'w', long = "timeout-ms", default_value_t = DEFAULT_TIMEOUT_MS)] + timeout_ms: u64, + + /// Base UDP destination port + #[arg(short = 'p', long = "base-port", default_value_t = DEFAULT_BASE_PORT)] base_port: u16, } -enum ParseOutcome { - Help(String), - Options(Options), -} - -fn usage(program: &str) -> String { - format!( - "Usage: {program} [-m max_hops] [-q queries] [-w timeout_ms] [-p base_port] destination\n\nUDP-based traceroute for Red Bear OS. Real probing is available only when built for Redox." - ) -} - -fn parse_value( - args: &mut impl Iterator, - flag: &str, -) -> Result -where - T::Err: std::fmt::Display, -{ - let value = args - .next() - .ok_or_else(|| anyhow::anyhow!("missing value for {flag}"))?; - value - .parse::() - .map_err(|err| anyhow::anyhow!("invalid value for {flag}: {err}")) -} - -fn parse_args() -> Result { - let mut args = env::args(); - let program = args - .next() - .unwrap_or_else(|| "redbear-traceroute".to_string()); - - let mut destination = None; - let mut max_hops = DEFAULT_MAX_HOPS; - let mut queries = DEFAULT_QUERIES; - let mut timeout_ms = DEFAULT_TIMEOUT_MS; - let mut base_port = DEFAULT_BASE_PORT; - - let mut rest = args; - while let Some(arg) = rest.next() { - match arg.as_str() { - "-h" | "--help" => return Ok(ParseOutcome::Help(usage(&program))), - "-m" | "--max-hops" => max_hops = parse_value(&mut rest, arg.as_str())?, - "-q" | "--queries" => queries = parse_value(&mut rest, arg.as_str())?, - "-w" | "--timeout-ms" => timeout_ms = parse_value(&mut rest, arg.as_str())?, - "-p" | "--base-port" => base_port = parse_value(&mut rest, arg.as_str())?, - value if value.starts_with('-') => bail!("unknown option {value}\n{}", usage(&program)), - value => { - if destination.replace(value.to_string()).is_some() { - bail!("only one destination may be supplied\n{}", usage(&program)); - } - } - } +fn run(args: Args) -> Result<()> { + if args.max_hops == 0 { + anyhow::bail!("max_hops must be at least 1"); + } + if args.queries == 0 { + anyhow::bail!("queries must be at least 1"); } - let destination = destination.ok_or_else(|| anyhow::anyhow!(usage(&program)))?; - if max_hops == 0 { - bail!("max_hops must be at least 1"); - } - if queries == 0 { - bail!("queries must be at least 1"); - } - - Ok(ParseOutcome::Options(Options { - destination, - max_hops, - queries, - timeout: Duration::from_millis(timeout_ms), - base_port, - })) -} - -fn run() -> Result<()> { - let options = match parse_args()? { - ParseOutcome::Help(help) => { - println!("{help}"); - return Ok(()); - } - ParseOutcome::Options(options) => options, - }; - let destination = resolve_destination(&options.destination) - .with_context(|| format!("failed to resolve {}", options.destination))?; + let destination = resolve_destination(&args.destination)?; + let timeout = Duration::from_millis(args.timeout_ms); println!( "traceroute to {} ({}), {} hops max", - options.destination, destination, options.max_hops + args.destination, destination, args.max_hops ); - for ttl in 1..=options.max_hops { + for ttl in 1..=args.max_hops { print!("{:>2}", ttl); let mut stop = false; - for query in 0..options.queries { - let sequence = (usize::from(ttl) - 1) * options.queries + query; - let dest_port = destination_port(options.base_port, sequence)?; - let observation = probe(destination, ttl, dest_port, options.timeout)?; + for query in 0..args.queries { + let sequence = (usize::from(ttl) - 1) * args.queries + query; + let dest_port = destination_port(args.base_port, sequence)?; + let observation = probe(destination, ttl, dest_port, timeout)?; match observation.reply { Some(reply) => { @@ -144,9 +96,11 @@ fn run() -> Result<()> { } fn main() { - let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).try_init(); - if let Err(err) = run() { - log::error!("redbear-traceroute: {err}"); + let args = Args::parse(); + args.common.init_logging("redbear-traceroute"); + + if let Err(err) = run(args) { + error!("redbear-traceroute: {err}"); std::process::exit(1); } }