use std::path::Path;
use std::process;
use std::time::{Duration, Instant};
use crate::args::Args;
use crate::format::{prettyfrequency, prettyhexrep, prettytime, size_str, speed_str};
use rns_net::config;
use rns_net::pickle::PickleValue;
use rns_net::rpc::derive_auth_key;
use rns_net::storage;
use rns_net::{RpcAddr, RpcClient};
const MONITOR_MIN_SLEEP: Duration = Duration::from_millis(200);
pub fn run(args: Args) {
if args.has("version") {
println!("rns-ctl {}", env!("FULL_VERSION"));
return;
}
if args.has("help") {
print_usage();
return;
}
env_logger::Builder::new()
.filter_level(match args.verbosity {
0 => log::LevelFilter::Warn,
1 => log::LevelFilter::Info,
2 => log::LevelFilter::Debug,
_ => log::LevelFilter::Trace,
})
.format_timestamp_secs()
.init();
let config_path = args.config_path().map(|s| s.to_string());
let json_output = args.has("j");
let show_all = args.has("a");
let sort_by = args.get("s").map(|s| s.to_string());
let reverse = args.has("r");
let show_totals = args.has("t");
let show_links = args.has("l");
let show_announces = args.has("A");
let monitor_mode = args.has("m");
let monitor_interval: f64 = args.get("I").and_then(|s| s.parse().ok()).unwrap_or(1.0);
let remote_timeout = args
.get("w")
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(rns_core::constants::PATH_REQUEST_TIMEOUT);
let management_identity = args.get("i").or_else(|| args.get("identity"));
let remote_hash = args.get("R").map(|s| s.to_string());
let filter = args.positional.first().cloned();
if let Some(ref hash_str) = remote_hash {
remote_status(
hash_str,
management_identity,
config_path.as_deref(),
remote_timeout,
show_links,
json_output,
monitor_mode,
monitor_interval,
show_all,
sort_by.as_deref(),
reverse,
filter.as_deref(),
show_totals,
show_announces,
);
return;
}
let config_dir =
storage::resolve_config_dir(config_path.as_ref().map(|s| Path::new(s.as_str())));
let config_file = config_dir.join("config");
let rns_config = if config_file.exists() {
match config::parse_file(&config_file) {
Ok(c) => c,
Err(e) => {
eprintln!("Error reading config: {}", e);
process::exit(1);
}
}
} else {
match config::parse("") {
Ok(c) => c,
Err(e) => {
eprintln!("Error: {}", e);
process::exit(1);
}
}
};
let paths = match storage::ensure_storage_dirs(&config_dir) {
Ok(p) => p,
Err(e) => {
eprintln!("Error: {}", e);
process::exit(1);
}
};
let identity = match storage::load_or_create_identity(&paths.identities) {
Ok(id) => id,
Err(e) => {
eprintln!("Error loading identity: {}", e);
process::exit(1);
}
};
let auth_key = derive_auth_key(&identity.get_private_key().unwrap_or([0u8; 64]));
let rpc_port = rns_config.reticulum.instance_control_port;
let rpc_addr = RpcAddr::Tcp("127.0.0.1".into(), rpc_port);
loop {
let monitor_started = Instant::now();
let mut client = match RpcClient::connect(&rpc_addr, &auth_key) {
Ok(c) => c,
Err(e) => {
if monitor_mode {
eprintln!("Could not connect to rnsd: {} — retrying...", e);
std::thread::sleep(monitor_sleep_duration(
monitor_interval,
monitor_started.elapsed(),
));
continue;
}
eprintln!("Could not connect to rnsd: {}", e);
eprintln!("Is rnsd running?");
process::exit(1);
}
};
let response = match client.call(&PickleValue::Dict(vec![(
PickleValue::String("get".into()),
PickleValue::String("interface_stats".into()),
)])) {
Ok(r) => r,
Err(e) => {
eprintln!("RPC error: {}", e);
if monitor_mode {
std::thread::sleep(monitor_sleep_duration(
monitor_interval,
monitor_started.elapsed(),
));
continue;
}
process::exit(1);
}
};
let link_count = if show_links {
match client.call(&PickleValue::Dict(vec![(
PickleValue::String("get".into()),
PickleValue::String("link_count".into()),
)])) {
Ok(r) => r.as_int(),
Err(_) => None,
}
} else {
None
};
if monitor_mode {
print!("\x1b[2J\x1b[H");
}
if json_output {
print_json(&response);
} else {
print_status(
&response,
show_all,
sort_by.as_deref(),
reverse,
filter.as_deref(),
show_totals,
show_announces,
);
}
if let Some(count) = link_count {
println!(" Active links : {}", count);
println!();
}
if !monitor_mode {
break;
}
std::thread::sleep(monitor_sleep_duration(
monitor_interval,
monitor_started.elapsed(),
));
}
}
fn monitor_sleep_duration(interval_secs: f64, elapsed: Duration) -> Duration {
let interval = Duration::from_secs_f64(interval_secs);
interval
.checked_sub(elapsed)
.unwrap_or(MONITOR_MIN_SLEEP)
.max(MONITOR_MIN_SLEEP)
}
fn print_status(
response: &PickleValue,
_show_all: bool,
sort_by: Option<&str>,
reverse: bool,
filter: Option<&str>,
show_totals: bool,
show_announces: bool,
) {
if let Some(PickleValue::Bool(true)) = response.get("transport_enabled").map(|v| v) {
print!(" Transport Instance ");
if let Some(tid) = response.get("transport_id").and_then(|v| v.as_bytes()) {
print!("{} ", prettyhexrep(&tid[..tid.len().min(8)]));
}
if let Some(PickleValue::Float(uptime)) = response.get("transport_uptime") {
print!("running for {}", prettytime(*uptime));
}
println!();
println!();
}
if let Some(interfaces) = response.get("interfaces").and_then(|v| v.as_list()) {
let mut iface_list: Vec<&PickleValue> = interfaces.iter().collect();
if let Some(f) = filter {
iface_list.retain(|iface| {
let name = iface.get("name").and_then(|v| v.as_str()).unwrap_or("");
name.to_lowercase().contains(&f.to_lowercase())
});
}
if let Some(sort_key) = sort_by {
iface_list.sort_by(|a, b| {
let cmp = match sort_key {
"rate" => {
let ra = a.get("bitrate").and_then(|v| v.as_int()).unwrap_or(0);
let rb = b.get("bitrate").and_then(|v| v.as_int()).unwrap_or(0);
ra.cmp(&rb)
}
"traffic" => {
let ta = a.get("rxb").and_then(|v| v.as_int()).unwrap_or(0)
+ a.get("txb").and_then(|v| v.as_int()).unwrap_or(0);
let tb = b.get("rxb").and_then(|v| v.as_int()).unwrap_or(0)
+ b.get("txb").and_then(|v| v.as_int()).unwrap_or(0);
ta.cmp(&tb)
}
"rx" => {
let ra = a.get("rxb").and_then(|v| v.as_int()).unwrap_or(0);
let rb = b.get("rxb").and_then(|v| v.as_int()).unwrap_or(0);
ra.cmp(&rb)
}
"tx" => {
let ta = a.get("txb").and_then(|v| v.as_int()).unwrap_or(0);
let tb = b.get("txb").and_then(|v| v.as_int()).unwrap_or(0);
ta.cmp(&tb)
}
_ => {
let na = a.get("name").and_then(|v| v.as_str()).unwrap_or("");
let nb = b.get("name").and_then(|v| v.as_str()).unwrap_or("");
na.cmp(nb)
}
};
if reverse {
cmp.reverse()
} else {
cmp
}
});
}
for iface in &iface_list {
let name = iface
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("Unknown");
let status = iface
.get("status")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let rxb = iface.get("rxb").and_then(|v| v.as_int()).unwrap_or(0) as u64;
let txb = iface.get("txb").and_then(|v| v.as_int()).unwrap_or(0) as u64;
let bitrate = iface
.get("bitrate")
.and_then(|v| v.as_int())
.map(|n| n as u64);
let mode = iface.get("mode").and_then(|v| v.as_int()).unwrap_or(0) as u8;
let started = iface
.get("started")
.and_then(|v| v.as_float())
.unwrap_or(0.0);
let mode_str = match mode {
rns_net::MODE_FULL => "Full",
rns_net::MODE_ACCESS_POINT => "Access Point",
rns_net::MODE_POINT_TO_POINT => "Point-to-Point",
rns_net::MODE_ROAMING => "Roaming",
rns_net::MODE_BOUNDARY => "Boundary",
rns_net::MODE_GATEWAY => "Gateway",
_ => "Unknown",
};
println!(" {}", name);
println!(" Status : {}", if status { "Up" } else { "Down" });
println!(" Mode : {}", mode_str);
if let Some(br) = bitrate {
println!(" Rate : {}", speed_str(br));
}
println!(
" Traffic : {} \u{2191} {} \u{2193}",
size_str(txb),
size_str(rxb),
);
if started > 0.0 {
let uptime = rns_net::time::now() - started;
if uptime > 0.0 {
println!(" Uptime : {}", prettytime(uptime));
}
}
if show_announces {
let ia_freq = iface
.get("ia_freq")
.and_then(|v| v.as_float())
.unwrap_or(0.0);
let oa_freq = iface
.get("oa_freq")
.and_then(|v| v.as_float())
.unwrap_or(0.0);
println!(
" Announces : {} in {} out",
prettyfrequency(ia_freq),
prettyfrequency(oa_freq),
);
}
println!();
}
}
if show_totals {
let total_rxb = response.get("rxb").and_then(|v| v.as_int()).unwrap_or(0) as u64;
let total_txb = response.get("txb").and_then(|v| v.as_int()).unwrap_or(0) as u64;
println!(
" Traffic totals: {} \u{2191} {} \u{2193}",
size_str(total_txb),
size_str(total_rxb),
);
println!();
}
}
fn print_json(response: &PickleValue) {
println!("{}", pickle_to_json(response));
}
fn pickle_to_json(value: &PickleValue) -> String {
match value {
PickleValue::None => "null".into(),
PickleValue::Bool(b) => if *b { "true" } else { "false" }.into(),
PickleValue::Int(n) => format!("{}", n),
PickleValue::Float(f) => format!("{}", f),
PickleValue::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
PickleValue::Bytes(b) => {
format!("\"{}\"", prettyhexrep(b))
}
PickleValue::List(items) => {
let inner: Vec<String> = items.iter().map(pickle_to_json).collect();
format!("[{}]", inner.join(", "))
}
PickleValue::Dict(pairs) => {
let inner: Vec<String> = pairs
.iter()
.map(|(k, v)| format!("{}: {}", pickle_to_json(k), pickle_to_json(v)))
.collect();
format!("{{{}}}", inner.join(", "))
}
}
}
#[allow(clippy::too_many_arguments)]
fn remote_status(
hash_str: &str,
management_identity: Option<&str>,
config_path: Option<&str>,
remote_timeout: f64,
show_links: bool,
json_output: bool,
monitor_mode: bool,
monitor_interval: f64,
show_all: bool,
sort_by: Option<&str>,
reverse: bool,
filter: Option<&str>,
show_totals: bool,
show_announces: bool,
) {
let transport_hash = match rns_net::remote_management::parse_transport_identity_hash(hash_str) {
Ok(h) => h,
Err(e) => {
eprintln!("{e}");
process::exit(1);
}
};
let Some(identity_path) = management_identity else {
eprintln!(
"{}",
rns_net::remote_management::RemoteManagementError::MissingIdentity
);
process::exit(1);
};
let timeout = Duration::from_secs_f64(remote_timeout.max(0.2));
let mut client = match rns_net::remote_management::RemoteManagementClient::connect(
config_path.map(Path::new),
Some(Path::new(identity_path)),
timeout,
) {
Ok(client) => client,
Err(e) => {
eprintln!("{e}");
process::exit(1);
}
};
loop {
let monitor_started = Instant::now();
match client.status(transport_hash, show_links) {
Ok(remote) => {
if monitor_mode {
print!("\x1b[2J\x1b[H");
}
if json_output {
print_json(&remote.stats);
} else {
print_status(
&remote.stats,
show_all,
sort_by,
reverse,
filter,
show_totals,
show_announces,
);
}
if let Some(count) = remote.link_count {
println!(" Active links : {}", count);
println!();
}
}
Err(e) => {
eprintln!("Remote status error: {e}");
if !monitor_mode {
process::exit(1);
}
}
}
if !monitor_mode {
break;
}
std::thread::sleep(monitor_sleep_duration(
monitor_interval,
monitor_started.elapsed(),
));
}
}
fn print_usage() {
println!("Usage: rns-ctl status [OPTIONS] [FILTER]");
println!();
println!("Options:");
println!(" --config PATH, -c PATH Path to config directory");
println!(" -a Show all interfaces");
println!(" -j JSON output");
println!(" -s SORT Sort by: rate, traffic, rx, tx");
println!(" -r Reverse sort order");
println!(" -t Show traffic totals");
println!(" -l Show link count");
println!(" -A Show announce statistics");
println!(" -m Monitor mode (loop)");
println!(" -I SECONDS Monitor interval (default: 1.0)");
println!(" -R HASH Query remote transport identity via management link");
println!(" -i PATH Identity file for remote management");
println!(" -w SECONDS Timeout for remote queries");
println!(" -v Increase verbosity");
println!(" --version Print version and exit");
println!(" --help, -h Print this help");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn monitor_sleep_accounts_for_elapsed_iteration_time() {
assert_eq!(
monitor_sleep_duration(1.0, Duration::from_millis(250)),
Duration::from_millis(750)
);
assert_eq!(
monitor_sleep_duration(1.0, Duration::from_millis(950)),
MONITOR_MIN_SLEEP
);
assert_eq!(
monitor_sleep_duration(1.0, Duration::from_millis(1500)),
MONITOR_MIN_SLEEP
);
}
}