use clap::Parser;
use ntpsec_rs_core::control_client::*;
use std::process;
#[derive(Parser, Debug)]
#[command(name = "ntpq-rs", about = "NTP query tool", version)]
struct Cli {
#[arg(default_value = "127.0.0.1")]
host: String,
#[arg(short = 'P', long, default_value = "123")]
port: u16,
#[arg(short = 'c', long)]
command: Vec<String>,
#[arg(short = 'p')]
peers: bool,
#[arg(short = 'v', long)]
verbose: bool,
#[arg(short = 'n', long)]
numeric: bool,
#[arg(short = 'd', long)]
debug: bool,
#[arg(short = 'a', long)]
auth_key: Option<u32>,
#[arg(short = 'k', long)]
key_file: Option<String>,
#[arg(short = 't', long, default_value = "5")]
timeout: u32,
}
enum CliCommand {
ReadVar { associd: u16 },
Associations,
Peers,
MruList,
Monitor,
Trace,
SysInfo,
}
fn parse_cli_command(input: &str) -> Result<CliCommand, String> {
let mut words = input.split_whitespace();
let command = words.next().unwrap_or_default();
match command {
"rv" => {
let associd_str = words.next();
if words.next().is_some() {
return Err(format!(
"too many arguments for 'rv': expected 0 or 1 associd, got extra arguments"
));
}
let associd = match associd_str {
Some(a) => {
let val = if let Some(stripped) = a.strip_prefix("associd=") {
stripped
} else {
a
};
val.parse::<u16>()
.map_err(|_| format!("invalid associd: '{val}'"))?
}
None => 0u16,
};
Ok(CliCommand::ReadVar { associd })
}
"associations" | "as" => Ok(CliCommand::Associations),
"peers" | "pe" => Ok(CliCommand::Peers),
"mrulist" => Ok(CliCommand::MruList),
"monitor" | "ntpmon" => Ok(CliCommand::Monitor),
"trace" | "ntptrace" => Ok(CliCommand::Trace),
"sysinfo" => Ok(CliCommand::SysInfo),
_ => Err(format!("unknown command: {command}")),
}
}
fn format_sysinfo(sys: &SystemVariables) -> String {
let mut out = String::new();
out.push_str("=== System Information ===\n");
let fields = [
("associd", "Association ID"),
("status", "Status"),
("stratum", "Stratum"),
("refid", "Reference ID"),
("reftime", "Reference Time"),
("offset", "Clock Offset"),
("delay", "Root Delay"),
("dispersion", "Root Dispersion"),
("frequency", "Frequency (ppm)"),
("leap", "Leap Indicator"),
("precision", "Precision"),
("rootdelay", "Root Delay"),
("rootdisp", "Root Dispersion"),
("peer", "System Peer"),
("syspeer", "System Peer ID"),
("version", "NTP Version"),
("processor", "Processor"),
("system", "OS"),
("uptime", "Uptime (seconds)"),
];
for (key, label) in &fields {
if let Some(val) = sys.get(key) {
out.push_str(&format!(" {:<20} = {}\n", label, val));
}
}
let stratum = sys.stratum();
let leap = sys.leap_str();
out.push_str(&format!("─────────────────────────────────\n"));
out.push_str(&format!(" stratum={} leap={}\n", stratum, leap));
out
}
fn main() {
let cli = Cli::parse();
let mut commands: Vec<String> = cli.command.clone();
if cli.peers {
commands.push("peers".to_string());
}
if commands.is_empty() {
commands.push("rv".to_string());
}
let mut client = ControlClient::new(cli.timeout, 1);
let mut had_error = false;
for cmd in &commands {
let result: Result<String, String> = match parse_cli_command(cmd) {
Err(e) => {
eprintln!("ntpq-rs: {e}");
had_error = true;
continue;
}
Ok(CliCommand::ReadVar { associd }) => {
if associd == 0 {
client
.read_system_vars(&cli.host, cli.port)
.map(|sys| format_readvar(&sys))
.map_err(|e| format!("{e}"))
} else {
client
.read_peer_vars(&cli.host, cli.port, associd)
.map(|pv| format_peer_readvar(&pv))
.map_err(|e| format!("{e}"))
}
}
Ok(CliCommand::Associations) => client
.read_associations(&cli.host, cli.port)
.map(|assocs| format_associations(&assocs))
.map_err(|e| format!("{e}")),
Ok(CliCommand::Peers) => {
let assoc_result = client.read_associations(&cli.host, cli.port);
match assoc_result {
Ok(assocs) => {
let mut rows = Vec::new();
for a in &assocs {
if !a.configured && !a.reachable {
continue;
}
match client.read_peer_vars(&cli.host, cli.port, a.associd) {
Ok(pv) => rows.push(PeerRow::from_association(&pv, a)),
Err(e) => {
rows.push(PeerRow {
tally: ' ',
remote: format!("? (error: {e})"),
refid: String::new(),
associd: a.associd,
stratum: 16,
peer_type: 'u',
when: None,
poll: 64,
reach: 0,
delay: 0.0,
offset: 0.0,
jitter: 0.0,
});
}
}
}
Ok(format_peers(&rows))
}
Err(e) => Err(format!("{e}")),
}
}
Ok(CliCommand::MruList) => client
.read_mru_list(&cli.host, cli.port)
.map(|entries| ntpsec_rs_core::control_client::MruEntry::format_list(&entries))
.map_err(|e| format!("{e}")),
Ok(CliCommand::Monitor) => match client.read_system_vars(&cli.host, cli.port) {
Ok(sys) => match client.read_associations(&cli.host, cli.port) {
Ok(assocs) => {
let mut output =
format!("=== System Variables ===\n{}", format_readvar(&sys));
output.push_str(&format!(
"\n=== Associations ({} total) ===\n",
assocs.len()
));
for a in &assocs {
output.push_str(&format!(
" associd={} status={:04x} configured={} reachable={}\n",
a.associd, a.status, a.configured, a.reachable
));
}
Ok(output)
}
Err(e) => Err(format!("{e}")),
},
Err(e) => Err(format!("{e}")),
},
Ok(CliCommand::Trace) => {
let sys_result = client.read_system_vars(&cli.host, cli.port);
match sys_result {
Ok(sys) => {
let stratum = sys.stratum();
let refid = sys.get("refid").unwrap_or("").to_string();
let offset_val = sys
.get("offset")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
let syspeer = sys.get("syspeer").unwrap_or("").to_string();
Ok(format!(
"{:>15} {:>15} {:>3} {:>10.6} syspeer={}\n",
cli.host, refid, stratum, offset_val, syspeer
))
}
Err(e) => Err(format!("{e}")),
}
}
Ok(CliCommand::SysInfo) => client
.read_system_vars(&cli.host, cli.port)
.map(|sys| format_sysinfo(&sys))
.map_err(|e| format!("{e}")),
};
match result {
Ok(output) => print!("{}", output),
Err(e) => {
eprintln!("ntpq-rs: {e}");
had_error = true;
}
}
}
if had_error {
process::exit(1);
}
}