1use std::path::Path;
6use std::process;
7use std::time::{Duration, Instant};
8
9use crate::args::Args;
10use crate::format::{prettyfrequency, prettyhexrep, prettytime, size_str, speed_str};
11use rns_net::config;
12use rns_net::pickle::PickleValue;
13use rns_net::rpc::derive_auth_key;
14use rns_net::storage;
15use rns_net::{RpcAddr, RpcClient};
16
17const MONITOR_MIN_SLEEP: Duration = Duration::from_millis(200);
18
19pub fn run(args: Args) {
20 if args.has("version") {
21 println!("rns-ctl {}", env!("FULL_VERSION"));
22 return;
23 }
24
25 if args.has("help") {
26 print_usage();
27 return;
28 }
29
30 env_logger::Builder::new()
31 .filter_level(match args.verbosity {
32 0 => log::LevelFilter::Warn,
33 1 => log::LevelFilter::Info,
34 2 => log::LevelFilter::Debug,
35 _ => log::LevelFilter::Trace,
36 })
37 .format_timestamp_secs()
38 .init();
39
40 let config_path = args.config_path().map(|s| s.to_string());
41 let json_output = args.has("j");
42 let show_all = args.has("a");
43 let sort_by = args.get("s").map(|s| s.to_string());
44 let reverse = args.has("r");
45 let show_totals = args.has("t");
46 let show_links = args.has("l");
47 let show_announces = args.has("A");
48 let monitor_mode = args.has("m");
49 let monitor_interval: f64 = args.get("I").and_then(|s| s.parse().ok()).unwrap_or(1.0);
50 let remote_timeout = args
51 .get("w")
52 .and_then(|s| s.parse::<f64>().ok())
53 .unwrap_or(rns_core::constants::PATH_REQUEST_TIMEOUT);
54 let management_identity = args.get("i").or_else(|| args.get("identity"));
55 let remote_hash = args.get("R").map(|s| s.to_string());
56 let filter = args.positional.first().cloned();
57
58 if let Some(ref hash_str) = remote_hash {
60 remote_status(
61 hash_str,
62 management_identity,
63 config_path.as_deref(),
64 remote_timeout,
65 show_links,
66 json_output,
67 monitor_mode,
68 monitor_interval,
69 show_all,
70 sort_by.as_deref(),
71 reverse,
72 filter.as_deref(),
73 show_totals,
74 show_announces,
75 );
76 return;
77 }
78
79 let config_dir =
81 storage::resolve_config_dir(config_path.as_ref().map(|s| Path::new(s.as_str())));
82 let config_file = config_dir.join("config");
83 let rns_config = if config_file.exists() {
84 match config::parse_file(&config_file) {
85 Ok(c) => c,
86 Err(e) => {
87 eprintln!("Error reading config: {}", e);
88 process::exit(1);
89 }
90 }
91 } else {
92 match config::parse("") {
93 Ok(c) => c,
94 Err(e) => {
95 eprintln!("Error: {}", e);
96 process::exit(1);
97 }
98 }
99 };
100
101 let paths = match storage::ensure_storage_dirs(&config_dir) {
103 Ok(p) => p,
104 Err(e) => {
105 eprintln!("Error: {}", e);
106 process::exit(1);
107 }
108 };
109
110 let identity = match storage::load_or_create_identity(&paths.identities) {
111 Ok(id) => id,
112 Err(e) => {
113 eprintln!("Error loading identity: {}", e);
114 process::exit(1);
115 }
116 };
117
118 let auth_key = derive_auth_key(&identity.get_private_key().unwrap_or([0u8; 64]));
119
120 let rpc_port = rns_config.reticulum.instance_control_port;
121 let rpc_addr = RpcAddr::Tcp("127.0.0.1".into(), rpc_port);
122
123 loop {
124 let monitor_started = Instant::now();
125
126 let mut client = match RpcClient::connect(&rpc_addr, &auth_key) {
128 Ok(c) => c,
129 Err(e) => {
130 if monitor_mode {
131 eprintln!("Could not connect to rnsd: {} — retrying...", e);
132 std::thread::sleep(monitor_sleep_duration(
133 monitor_interval,
134 monitor_started.elapsed(),
135 ));
136 continue;
137 }
138 eprintln!("Could not connect to rnsd: {}", e);
139 eprintln!("Is rnsd running?");
140 process::exit(1);
141 }
142 };
143
144 let response = match client.call(&PickleValue::Dict(vec![(
146 PickleValue::String("get".into()),
147 PickleValue::String("interface_stats".into()),
148 )])) {
149 Ok(r) => r,
150 Err(e) => {
151 eprintln!("RPC error: {}", e);
152 if monitor_mode {
153 std::thread::sleep(monitor_sleep_duration(
154 monitor_interval,
155 monitor_started.elapsed(),
156 ));
157 continue;
158 }
159 process::exit(1);
160 }
161 };
162
163 let link_count = if show_links {
165 match client.call(&PickleValue::Dict(vec![(
166 PickleValue::String("get".into()),
167 PickleValue::String("link_count".into()),
168 )])) {
169 Ok(r) => r.as_int(),
170 Err(_) => None,
171 }
172 } else {
173 None
174 };
175
176 if monitor_mode {
177 print!("\x1b[2J\x1b[H");
179 }
180
181 if json_output {
182 print_json(&response);
183 } else {
184 print_status(
185 &response,
186 show_all,
187 sort_by.as_deref(),
188 reverse,
189 filter.as_deref(),
190 show_totals,
191 show_announces,
192 );
193 }
194
195 if let Some(count) = link_count {
196 println!(" Active links : {}", count);
197 println!();
198 }
199
200 if !monitor_mode {
201 break;
202 }
203
204 std::thread::sleep(monitor_sleep_duration(
205 monitor_interval,
206 monitor_started.elapsed(),
207 ));
208 }
209}
210
211fn monitor_sleep_duration(interval_secs: f64, elapsed: Duration) -> Duration {
212 let interval = Duration::from_secs_f64(interval_secs);
213 interval
214 .checked_sub(elapsed)
215 .unwrap_or(MONITOR_MIN_SLEEP)
216 .max(MONITOR_MIN_SLEEP)
217}
218
219fn print_status(
220 response: &PickleValue,
221 _show_all: bool,
222 sort_by: Option<&str>,
223 reverse: bool,
224 filter: Option<&str>,
225 show_totals: bool,
226 show_announces: bool,
227) {
228 if let Some(PickleValue::Bool(true)) = response.get("transport_enabled").map(|v| v) {
230 print!(" Transport Instance ");
231 if let Some(tid) = response.get("transport_id").and_then(|v| v.as_bytes()) {
232 print!("{} ", prettyhexrep(&tid[..tid.len().min(8)]));
233 }
234 if let Some(PickleValue::Float(uptime)) = response.get("transport_uptime") {
235 print!("running for {}", prettytime(*uptime));
236 }
237 println!();
238 println!();
239 }
240
241 if let Some(interfaces) = response.get("interfaces").and_then(|v| v.as_list()) {
243 let mut iface_list: Vec<&PickleValue> = interfaces.iter().collect();
245
246 if let Some(f) = filter {
248 iface_list.retain(|iface| {
249 let name = iface.get("name").and_then(|v| v.as_str()).unwrap_or("");
250 name.to_lowercase().contains(&f.to_lowercase())
251 });
252 }
253
254 if let Some(sort_key) = sort_by {
256 iface_list.sort_by(|a, b| {
257 let cmp = match sort_key {
258 "rate" => {
259 let ra = a.get("bitrate").and_then(|v| v.as_int()).unwrap_or(0);
260 let rb = b.get("bitrate").and_then(|v| v.as_int()).unwrap_or(0);
261 ra.cmp(&rb)
262 }
263 "traffic" => {
264 let ta = a.get("rxb").and_then(|v| v.as_int()).unwrap_or(0)
265 + a.get("txb").and_then(|v| v.as_int()).unwrap_or(0);
266 let tb = b.get("rxb").and_then(|v| v.as_int()).unwrap_or(0)
267 + b.get("txb").and_then(|v| v.as_int()).unwrap_or(0);
268 ta.cmp(&tb)
269 }
270 "rx" => {
271 let ra = a.get("rxb").and_then(|v| v.as_int()).unwrap_or(0);
272 let rb = b.get("rxb").and_then(|v| v.as_int()).unwrap_or(0);
273 ra.cmp(&rb)
274 }
275 "tx" => {
276 let ta = a.get("txb").and_then(|v| v.as_int()).unwrap_or(0);
277 let tb = b.get("txb").and_then(|v| v.as_int()).unwrap_or(0);
278 ta.cmp(&tb)
279 }
280 _ => {
281 let na = a.get("name").and_then(|v| v.as_str()).unwrap_or("");
282 let nb = b.get("name").and_then(|v| v.as_str()).unwrap_or("");
283 na.cmp(nb)
284 }
285 };
286 if reverse {
287 cmp.reverse()
288 } else {
289 cmp
290 }
291 });
292 }
293
294 for iface in &iface_list {
295 let name = iface
296 .get("name")
297 .and_then(|v| v.as_str())
298 .unwrap_or("Unknown");
299 let status = iface
300 .get("status")
301 .and_then(|v| v.as_bool())
302 .unwrap_or(false);
303 let rxb = iface.get("rxb").and_then(|v| v.as_int()).unwrap_or(0) as u64;
304 let txb = iface.get("txb").and_then(|v| v.as_int()).unwrap_or(0) as u64;
305 let bitrate = iface
306 .get("bitrate")
307 .and_then(|v| v.as_int())
308 .map(|n| n as u64);
309 let mode = iface.get("mode").and_then(|v| v.as_int()).unwrap_or(0) as u8;
310 let started = iface
311 .get("started")
312 .and_then(|v| v.as_float())
313 .unwrap_or(0.0);
314
315 let mode_str = match mode {
316 rns_net::MODE_FULL => "Full",
317 rns_net::MODE_ACCESS_POINT => "Access Point",
318 rns_net::MODE_POINT_TO_POINT => "Point-to-Point",
319 rns_net::MODE_ROAMING => "Roaming",
320 rns_net::MODE_BOUNDARY => "Boundary",
321 rns_net::MODE_GATEWAY => "Gateway",
322 _ => "Unknown",
323 };
324
325 println!(" {}", name);
326 println!(" Status : {}", if status { "Up" } else { "Down" });
327 println!(" Mode : {}", mode_str);
328 if let Some(br) = bitrate {
329 println!(" Rate : {}", speed_str(br));
330 }
331 println!(
332 " Traffic : {} \u{2191} {} \u{2193}",
333 size_str(txb),
334 size_str(rxb),
335 );
336 if started > 0.0 {
337 let uptime = rns_net::time::now() - started;
338 if uptime > 0.0 {
339 println!(" Uptime : {}", prettytime(uptime));
340 }
341 }
342 if show_announces {
343 let ia_freq = iface
344 .get("ia_freq")
345 .and_then(|v| v.as_float())
346 .unwrap_or(0.0);
347 let oa_freq = iface
348 .get("oa_freq")
349 .and_then(|v| v.as_float())
350 .unwrap_or(0.0);
351 println!(
352 " Announces : {} in {} out",
353 prettyfrequency(ia_freq),
354 prettyfrequency(oa_freq),
355 );
356 }
357 println!();
358 }
359 }
360
361 if show_totals {
363 let total_rxb = response.get("rxb").and_then(|v| v.as_int()).unwrap_or(0) as u64;
364 let total_txb = response.get("txb").and_then(|v| v.as_int()).unwrap_or(0) as u64;
365 println!(
366 " Traffic totals: {} \u{2191} {} \u{2193}",
367 size_str(total_txb),
368 size_str(total_rxb),
369 );
370 println!();
371 }
372}
373
374fn print_json(response: &PickleValue) {
375 println!("{}", pickle_to_json(response));
376}
377
378fn pickle_to_json(value: &PickleValue) -> String {
379 match value {
380 PickleValue::None => "null".into(),
381 PickleValue::Bool(b) => if *b { "true" } else { "false" }.into(),
382 PickleValue::Int(n) => format!("{}", n),
383 PickleValue::Float(f) => format!("{}", f),
384 PickleValue::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
385 PickleValue::Bytes(b) => {
386 format!("\"{}\"", prettyhexrep(b))
387 }
388 PickleValue::List(items) => {
389 let inner: Vec<String> = items.iter().map(pickle_to_json).collect();
390 format!("[{}]", inner.join(", "))
391 }
392 PickleValue::Dict(pairs) => {
393 let inner: Vec<String> = pairs
394 .iter()
395 .map(|(k, v)| format!("{}: {}", pickle_to_json(k), pickle_to_json(v)))
396 .collect();
397 format!("{{{}}}", inner.join(", "))
398 }
399 }
400}
401
402#[allow(clippy::too_many_arguments)]
403fn remote_status(
404 hash_str: &str,
405 management_identity: Option<&str>,
406 config_path: Option<&str>,
407 remote_timeout: f64,
408 show_links: bool,
409 json_output: bool,
410 monitor_mode: bool,
411 monitor_interval: f64,
412 show_all: bool,
413 sort_by: Option<&str>,
414 reverse: bool,
415 filter: Option<&str>,
416 show_totals: bool,
417 show_announces: bool,
418) {
419 let transport_hash = match rns_net::remote_management::parse_transport_identity_hash(hash_str) {
420 Ok(h) => h,
421 Err(e) => {
422 eprintln!("{e}");
423 process::exit(1);
424 }
425 };
426 let Some(identity_path) = management_identity else {
427 eprintln!(
428 "{}",
429 rns_net::remote_management::RemoteManagementError::MissingIdentity
430 );
431 process::exit(1);
432 };
433 let timeout = Duration::from_secs_f64(remote_timeout.max(0.2));
434 let mut client = match rns_net::remote_management::RemoteManagementClient::connect(
435 config_path.map(Path::new),
436 Some(Path::new(identity_path)),
437 timeout,
438 ) {
439 Ok(client) => client,
440 Err(e) => {
441 eprintln!("{e}");
442 process::exit(1);
443 }
444 };
445
446 loop {
447 let monitor_started = Instant::now();
448 match client.status(transport_hash, show_links) {
449 Ok(remote) => {
450 if monitor_mode {
451 print!("\x1b[2J\x1b[H");
452 }
453 if json_output {
454 print_json(&remote.stats);
455 } else {
456 print_status(
457 &remote.stats,
458 show_all,
459 sort_by,
460 reverse,
461 filter,
462 show_totals,
463 show_announces,
464 );
465 }
466 if let Some(count) = remote.link_count {
467 println!(" Active links : {}", count);
468 println!();
469 }
470 }
471 Err(e) => {
472 eprintln!("Remote status error: {e}");
473 if !monitor_mode {
474 process::exit(1);
475 }
476 }
477 }
478
479 if !monitor_mode {
480 break;
481 }
482 std::thread::sleep(monitor_sleep_duration(
483 monitor_interval,
484 monitor_started.elapsed(),
485 ));
486 }
487}
488
489fn print_usage() {
490 println!("Usage: rns-ctl status [OPTIONS] [FILTER]");
491 println!();
492 println!("Options:");
493 println!(" --config PATH, -c PATH Path to config directory");
494 println!(" -a Show all interfaces");
495 println!(" -j JSON output");
496 println!(" -s SORT Sort by: rate, traffic, rx, tx");
497 println!(" -r Reverse sort order");
498 println!(" -t Show traffic totals");
499 println!(" -l Show link count");
500 println!(" -A Show announce statistics");
501 println!(" -m Monitor mode (loop)");
502 println!(" -I SECONDS Monitor interval (default: 1.0)");
503 println!(" -R HASH Query remote transport identity via management link");
504 println!(" -i PATH Identity file for remote management");
505 println!(" -w SECONDS Timeout for remote queries");
506 println!(" -v Increase verbosity");
507 println!(" --version Print version and exit");
508 println!(" --help, -h Print this help");
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514
515 #[test]
516 fn monitor_sleep_accounts_for_elapsed_iteration_time() {
517 assert_eq!(
518 monitor_sleep_duration(1.0, Duration::from_millis(250)),
519 Duration::from_millis(750)
520 );
521 assert_eq!(
522 monitor_sleep_duration(1.0, Duration::from_millis(950)),
523 MONITOR_MIN_SLEEP
524 );
525 assert_eq!(
526 monitor_sleep_duration(1.0, Duration::from_millis(1500)),
527 MONITOR_MIN_SLEEP
528 );
529 }
530}