use clap::Parser;
use ntpsec_rs_core::control_client::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
#[derive(Parser, Debug)]
#[command(name = "ntpmon-rs", about = "NTP real-time monitor", version)]
struct Cli {
#[arg(default_value = "127.0.0.1")]
host: String,
#[arg(short = 'p', long, default_value = "123")]
port: u16,
#[arg(short = 'r', long, default_value = "2")]
interval: u32,
#[arg(short = 'n', long, default_value = "0")]
count: u32,
}
fn fmt_uptime(uptime_str: Option<&str>) -> String {
let secs = match uptime_str.and_then(|s| s.parse::<f64>().ok()) {
Some(s) => s as u64,
None => return "N/A".to_string(),
};
let days = secs / 86400;
let hours = (secs % 86400) / 3600;
let minutes = (secs % 3600) / 60;
let seconds = secs % 60;
if days > 0 {
format!("{days}d {hours:02}:{minutes:02}:{seconds:02}")
} else {
format!("{hours:02}:{minutes:02}:{seconds:02}")
}
}
fn format_peer_line(pv: &PeerVariables, assoc: &AssociationStatus) -> String {
let peer_stratum = pv.stratum();
let offset = pv
.get("offset")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
let delay = pv
.get("delay")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
let jitter = pv
.get("jitter")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
let remote = pv.get("srcadr").unwrap_or("(unknown)").to_string();
let refid = pv.get("refid").unwrap_or("").to_string();
let tally = assoc.tally_char();
let reach = pv
.get("reach")
.and_then(|s| u8::from_str_radix(s, 16).ok())
.unwrap_or(0);
format!(
"{tally} {remote:>21} refid={refid:<4} st={peer_stratum:<2} offset={offset:>9.6} delay={delay:>9.6} jitter={jitter:>9.6} reach={reach:02x}"
)
}
fn main() {
let cli = Cli::parse();
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
let sigint_result = std::thread::spawn(move || {
loop {
std::thread::sleep(std::time::Duration::from_millis(500));
if !r.load(Ordering::Relaxed) {
break;
}
}
});
let signal_thread = sigint_result;
println!(
"ntpmon-rs v{} — NTP monitor (Rust)",
env!("CARGO_PKG_VERSION")
);
println!(
"Monitoring: {}:{} (every {}s)",
cli.host, cli.port, cli.interval
);
println!("Press Ctrl-C to stop.");
println!();
let mut client = ControlClient::new(5, 1);
let max_iterations = if cli.count == 0 { u32::MAX } else { cli.count };
let start_time = std::time::Instant::now();
for iter in 0..max_iterations {
if !running.load(Ordering::Relaxed) {
break;
}
let elapsed = start_time.elapsed().as_secs();
println!(
"--- iteration={} elapsed={} ---",
iter + 1,
fmt_uptime(Some(&elapsed.to_string()))
);
match client.read_system_vars(&cli.host, cli.port) {
Ok(sys) => {
let stratum = sys.stratum();
let leap = sys.leap_str();
let display = sys.get("display").unwrap_or("").to_string();
let offset = sys
.get("offset")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
let freq = sys
.get("frequency")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
let delay = sys
.get("delay")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
let rootdelay = sys
.get("rootdelay")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
let rootdisp = sys
.get("rootdisp")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
let uptime = fmt_uptime(sys.get("uptime"));
println!(
" stratum={} leap={} offset={:.6} freq={:.3} delay={:.6} rootdelay={:.6} rootdisp={:.6}",
stratum, leap, offset, freq, delay, rootdelay, rootdisp
);
println!(" uptime={} display={}", uptime, display);
}
Err(e) => {
eprintln!("ERROR reading system variables: {e}");
break;
}
}
match client.read_associations(&cli.host, cli.port) {
Ok(assocs) => {
let reachable = assocs.iter().filter(|a| a.reachable).count();
let configured = assocs.iter().filter(|a| a.configured).count();
println!(
" associations: {} configured, {} reachable",
configured, reachable
);
for a in &assocs {
if !a.configured && !a.reachable {
continue;
}
if let Ok(pv) = client.read_peer_vars(&cli.host, cli.port, a.associd) {
println!(" {}", format_peer_line(&pv, a));
} else {
println!(
" {} associd={} (query error)",
a.tally_char(),
a.associd
);
}
}
}
Err(e) => {
eprintln!("ERROR reading associations: {e}");
break;
}
}
if iter + 1 >= max_iterations {
break;
}
let sleep_total = cli.interval as u64;
let sleep_step = std::cmp::min(sleep_total, 1);
let steps = sleep_total / sleep_step;
for _ in 0..steps {
if !running.load(Ordering::Relaxed) {
break;
}
std::thread::sleep(std::time::Duration::from_secs(sleep_step));
}
}
println!("\nntpmon-rs: monitoring stopped.");
}