use std::process::ExitCode;
use std::time::Duration;
use clap::builder::PossibleValuesParser;
use clap::{ArgMatches, Args, Command, FromArgMatches, Subcommand};
use super::VerbFamily;
use crate::cli::VerbLogClass;
use crate::network_switch::{NetworkRefusal, SwitchEnv};
#[derive(Subcommand)]
enum DoctorCmd {
Doctor(DoctorArgs),
}
#[derive(Args)]
struct DoctorArgs {
#[arg(long)]
json: bool,
#[arg(long)]
local: bool,
#[arg(
long = "check",
value_name = "ID",
value_parser = PossibleValuesParser::new(cerulion_cli_engine::doctor::check_ids())
)]
checks: Vec<String>,
#[arg(long = "udp-target", value_name = "HOST:PORT", value_parser = parse_host_port)]
udp_targets: Vec<String>,
#[arg(
long = "timeout-ms",
value_name = "MS",
value_parser = clap::value_parser!(u64).range(1..)
)]
timeout_ms: Option<u64>,
}
fn parse_host_port(value: &str) -> Result<String, String> {
let (host, port) = value
.rsplit_once(':')
.ok_or_else(|| format!("`{value}` has no port: write it as HOST:PORT"))?;
if port.is_empty() {
return Err(format!("`{value}` has no port: write it as HOST:PORT"));
}
match port.parse::<u16>() {
Ok(0) | Err(_) => {
return Err(format!(
"`{port}` is not a port from 1 to 65535 in `{value}`"
))
}
Ok(_) => {}
}
let bracketed = host.strip_prefix('[').and_then(|h| h.strip_suffix(']'));
match bracketed {
Some(inner) if inner.parse::<std::net::Ipv6Addr>().is_ok() => Ok(value.to_string()),
Some(_) => Err(format!("`{host}` is not an IPv6 address in `{value}`")),
None if host.is_empty() => Err(format!("`{value}` has no host: write it as HOST:PORT")),
None if host.contains(':') || host.contains('[') || host.contains(']') => {
Err(format!("`{value}`: write an IPv6 address as [ADDR]:PORT"))
}
None => Ok(value.to_string()),
}
}
pub(crate) struct Doctor;
impl VerbFamily for Doctor {
fn name(&self) -> &'static str {
"doctor"
}
fn listed_after(&self) -> Option<&'static str> {
Some("clean")
}
fn command(&self) -> Command {
super::derived_verb::<DoctorCmd>()
}
fn log_class(&self, _matches: &ArgMatches) -> VerbLogClass {
VerbLogClass::OneShot
}
fn network_refusal(&self, _matches: &ArgMatches, _env: &SwitchEnv) -> Option<NetworkRefusal> {
None
}
fn needs_identity(&self, _matches: &ArgMatches) -> bool {
false
}
fn run(&self, matches: &ArgMatches) -> ExitCode {
let args = DoctorArgs::from_arg_matches(matches).unwrap_or_else(|e| e.exit());
run(&args)
}
}
fn run(args: &DoctorArgs) -> ExitCode {
use cerulion_cli_engine::doctor;
let selected = match doctor::select(&args.checks) {
Ok(selected) => selected,
Err(unknown) => {
eprintln!("Error: {unknown}");
return ExitCode::from(2);
}
};
let cwd = match std::env::current_dir() {
Ok(cwd) => cwd,
Err(e) => {
eprintln!("Error: cannot read the current directory: {e}");
return ExitCode::FAILURE;
}
};
let ctx = doctor::Context {
cwd,
udp_targets: args.udp_targets.clone(),
};
let timeout = args
.timeout_ms
.map_or(doctor::DEFAULT_TIMEOUT, Duration::from_millis);
let report = doctor::run(&selected, &ctx, doctor::network_gate(args.local), timeout);
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&report.to_json())
.unwrap_or_else(|_| report.to_json().to_string())
);
} else {
print!("{}", report.render_human());
}
ExitCode::from(report.exit_code())
}
#[cfg(test)]
mod tests {
use super::parse_host_port;
#[test]
fn udp_targets_need_a_host_and_a_port_and_brackets_around_ipv6() {
for ok in [
"robot.local:7447",
"10.0.0.2:7447",
"[::1]:7447",
"[fd00::2]:1",
] {
assert_eq!(parse_host_port(ok).as_deref(), Ok(ok), "{ok}");
}
for bad in [
"robot.local",
"robot.local:",
":7447",
"host:0",
"host:99999",
"fd00::1",
"fd00::1:7447",
"fd00::1]:7447",
"[fd00::1:7447",
"[nope]:7447",
] {
assert!(parse_host_port(bad).is_err(), "{bad} was accepted");
}
}
}