Skip to main content

rns_ctl/cmd/
status.rs

1//! Display Reticulum network interface status.
2//!
3//! Connects to a running rnsd via RPC and displays interface statistics.
4
5use std::path::Path;
6use std::process;
7
8use crate::args::Args;
9use crate::format::{prettyfrequency, prettyhexrep, prettytime, size_str, speed_str};
10use rns_net::config;
11use rns_net::pickle::PickleValue;
12use rns_net::rpc::derive_auth_key;
13use rns_net::storage;
14use rns_net::{RpcAddr, RpcClient};
15
16pub fn run(args: Args) {
17    if args.has("version") {
18        println!("rns-ctl {}", env!("FULL_VERSION"));
19        return;
20    }
21
22    if args.has("help") {
23        print_usage();
24        return;
25    }
26
27    env_logger::Builder::new()
28        .filter_level(match args.verbosity {
29            0 => log::LevelFilter::Warn,
30            1 => log::LevelFilter::Info,
31            2 => log::LevelFilter::Debug,
32            _ => log::LevelFilter::Trace,
33        })
34        .format_timestamp_secs()
35        .init();
36
37    let config_path = args.config_path().map(|s| s.to_string());
38    let json_output = args.has("j");
39    let show_all = args.has("a");
40    let sort_by = args.get("s").map(|s| s.to_string());
41    let reverse = args.has("r");
42    let show_totals = args.has("t");
43    let show_links = args.has("l");
44    let show_announces = args.has("A");
45    let monitor_mode = args.has("m");
46    let monitor_interval: f64 = args.get("I").and_then(|s| s.parse().ok()).unwrap_or(1.0);
47    let remote_hash = args.get("R").map(|s| s.to_string());
48    let filter = args.positional.first().cloned();
49
50    // Remote management query via -R flag
51    if let Some(ref hash_str) = remote_hash {
52        remote_status(hash_str, config_path.as_deref());
53        return;
54    }
55
56    // Load config to get RPC address and auth key
57    let config_dir =
58        storage::resolve_config_dir(config_path.as_ref().map(|s| Path::new(s.as_str())));
59    let config_file = config_dir.join("config");
60    let rns_config = if config_file.exists() {
61        match config::parse_file(&config_file) {
62            Ok(c) => c,
63            Err(e) => {
64                eprintln!("Error reading config: {}", e);
65                process::exit(1);
66            }
67        }
68    } else {
69        match config::parse("") {
70            Ok(c) => c,
71            Err(e) => {
72                eprintln!("Error: {}", e);
73                process::exit(1);
74            }
75        }
76    };
77
78    // Load identity to derive auth key
79    let paths = match storage::ensure_storage_dirs(&config_dir) {
80        Ok(p) => p,
81        Err(e) => {
82            eprintln!("Error: {}", e);
83            process::exit(1);
84        }
85    };
86
87    let identity = match storage::load_or_create_identity(&paths.identities) {
88        Ok(id) => id,
89        Err(e) => {
90            eprintln!("Error loading identity: {}", e);
91            process::exit(1);
92        }
93    };
94
95    let auth_key = derive_auth_key(&identity.get_private_key().unwrap_or([0u8; 64]));
96
97    let rpc_port = rns_config.reticulum.instance_control_port;
98    let rpc_addr = RpcAddr::Tcp("127.0.0.1".into(), rpc_port);
99
100    loop {
101        // Connect to RPC server
102        let mut client = match RpcClient::connect(&rpc_addr, &auth_key) {
103            Ok(c) => c,
104            Err(e) => {
105                if monitor_mode {
106                    eprintln!("Could not connect to rnsd: {} — retrying...", e);
107                    std::thread::sleep(std::time::Duration::from_secs_f64(monitor_interval));
108                    continue;
109                }
110                eprintln!("Could not connect to rnsd: {}", e);
111                eprintln!("Is rnsd running?");
112                process::exit(1);
113            }
114        };
115
116        // Request interface stats
117        let response = match client.call(&PickleValue::Dict(vec![(
118            PickleValue::String("get".into()),
119            PickleValue::String("interface_stats".into()),
120        )])) {
121            Ok(r) => r,
122            Err(e) => {
123                eprintln!("RPC error: {}", e);
124                if monitor_mode {
125                    std::thread::sleep(std::time::Duration::from_secs_f64(monitor_interval));
126                    continue;
127                }
128                process::exit(1);
129            }
130        };
131
132        // Query link count if requested
133        let link_count = if show_links {
134            match client.call(&PickleValue::Dict(vec![(
135                PickleValue::String("get".into()),
136                PickleValue::String("link_count".into()),
137            )])) {
138                Ok(r) => r.as_int(),
139                Err(_) => None,
140            }
141        } else {
142            None
143        };
144
145        if monitor_mode {
146            // Clear screen
147            print!("\x1b[2J\x1b[H");
148        }
149
150        if json_output {
151            print_json(&response);
152        } else {
153            print_status(
154                &response,
155                show_all,
156                sort_by.as_deref(),
157                reverse,
158                filter.as_deref(),
159                show_totals,
160                show_announces,
161            );
162        }
163
164        if let Some(count) = link_count {
165            println!(" Active links  : {}", count);
166            println!();
167        }
168
169        if !monitor_mode {
170            break;
171        }
172
173        std::thread::sleep(std::time::Duration::from_secs_f64(monitor_interval));
174    }
175}
176
177fn print_status(
178    response: &PickleValue,
179    _show_all: bool,
180    sort_by: Option<&str>,
181    reverse: bool,
182    filter: Option<&str>,
183    show_totals: bool,
184    show_announces: bool,
185) {
186    // Print transport info
187    if let Some(PickleValue::Bool(true)) = response.get("transport_enabled").map(|v| v) {
188        print!(" Transport Instance ");
189        if let Some(tid) = response.get("transport_id").and_then(|v| v.as_bytes()) {
190            print!("{} ", prettyhexrep(&tid[..tid.len().min(8)]));
191        }
192        if let Some(PickleValue::Float(uptime)) = response.get("transport_uptime") {
193            print!("running for {}", prettytime(*uptime));
194        }
195        println!();
196        println!();
197    }
198
199    // Print interfaces
200    if let Some(interfaces) = response.get("interfaces").and_then(|v| v.as_list()) {
201        // Collect into a sortable vec of references
202        let mut iface_list: Vec<&PickleValue> = interfaces.iter().collect();
203
204        // Apply filter
205        if let Some(f) = filter {
206            iface_list.retain(|iface| {
207                let name = iface.get("name").and_then(|v| v.as_str()).unwrap_or("");
208                name.to_lowercase().contains(&f.to_lowercase())
209            });
210        }
211
212        // Sort if requested
213        if let Some(sort_key) = sort_by {
214            iface_list.sort_by(|a, b| {
215                let cmp = match sort_key {
216                    "rate" => {
217                        let ra = a.get("bitrate").and_then(|v| v.as_int()).unwrap_or(0);
218                        let rb = b.get("bitrate").and_then(|v| v.as_int()).unwrap_or(0);
219                        ra.cmp(&rb)
220                    }
221                    "traffic" => {
222                        let ta = a.get("rxb").and_then(|v| v.as_int()).unwrap_or(0)
223                            + a.get("txb").and_then(|v| v.as_int()).unwrap_or(0);
224                        let tb = b.get("rxb").and_then(|v| v.as_int()).unwrap_or(0)
225                            + b.get("txb").and_then(|v| v.as_int()).unwrap_or(0);
226                        ta.cmp(&tb)
227                    }
228                    "rx" => {
229                        let ra = a.get("rxb").and_then(|v| v.as_int()).unwrap_or(0);
230                        let rb = b.get("rxb").and_then(|v| v.as_int()).unwrap_or(0);
231                        ra.cmp(&rb)
232                    }
233                    "tx" => {
234                        let ta = a.get("txb").and_then(|v| v.as_int()).unwrap_or(0);
235                        let tb = b.get("txb").and_then(|v| v.as_int()).unwrap_or(0);
236                        ta.cmp(&tb)
237                    }
238                    _ => {
239                        let na = a.get("name").and_then(|v| v.as_str()).unwrap_or("");
240                        let nb = b.get("name").and_then(|v| v.as_str()).unwrap_or("");
241                        na.cmp(nb)
242                    }
243                };
244                if reverse {
245                    cmp.reverse()
246                } else {
247                    cmp
248                }
249            });
250        }
251
252        for iface in &iface_list {
253            let name = iface
254                .get("name")
255                .and_then(|v| v.as_str())
256                .unwrap_or("Unknown");
257            let status = iface
258                .get("status")
259                .and_then(|v| v.as_bool())
260                .unwrap_or(false);
261            let rxb = iface.get("rxb").and_then(|v| v.as_int()).unwrap_or(0) as u64;
262            let txb = iface.get("txb").and_then(|v| v.as_int()).unwrap_or(0) as u64;
263            let bitrate = iface
264                .get("bitrate")
265                .and_then(|v| v.as_int())
266                .map(|n| n as u64);
267            let mode = iface.get("mode").and_then(|v| v.as_int()).unwrap_or(0) as u8;
268            let started = iface
269                .get("started")
270                .and_then(|v| v.as_float())
271                .unwrap_or(0.0);
272
273            let mode_str = match mode {
274                rns_net::MODE_FULL => "Full",
275                rns_net::MODE_ACCESS_POINT => "Access Point",
276                rns_net::MODE_POINT_TO_POINT => "Point-to-Point",
277                rns_net::MODE_ROAMING => "Roaming",
278                rns_net::MODE_BOUNDARY => "Boundary",
279                rns_net::MODE_GATEWAY => "Gateway",
280                _ => "Unknown",
281            };
282
283            println!(" {}", name);
284            println!("    Status    : {}", if status { "Up" } else { "Down" });
285            println!("    Mode      : {}", mode_str);
286            if let Some(br) = bitrate {
287                println!("    Rate      : {}", speed_str(br));
288            }
289            println!(
290                "    Traffic   : {} \u{2191}  {} \u{2193}",
291                size_str(txb),
292                size_str(rxb),
293            );
294            if started > 0.0 {
295                let uptime = rns_net::time::now() - started;
296                if uptime > 0.0 {
297                    println!("    Uptime    : {}", prettytime(uptime));
298                }
299            }
300            if show_announces {
301                let ia_freq = iface
302                    .get("ia_freq")
303                    .and_then(|v| v.as_float())
304                    .unwrap_or(0.0);
305                let oa_freq = iface
306                    .get("oa_freq")
307                    .and_then(|v| v.as_float())
308                    .unwrap_or(0.0);
309                println!(
310                    "    Announces : {} in  {} out",
311                    prettyfrequency(ia_freq),
312                    prettyfrequency(oa_freq),
313                );
314            }
315            println!();
316        }
317    }
318
319    // Show traffic totals
320    if show_totals {
321        let total_rxb = response.get("rxb").and_then(|v| v.as_int()).unwrap_or(0) as u64;
322        let total_txb = response.get("txb").and_then(|v| v.as_int()).unwrap_or(0) as u64;
323        println!(
324            " Traffic totals: {} \u{2191}  {} \u{2193}",
325            size_str(total_txb),
326            size_str(total_rxb),
327        );
328        println!();
329    }
330}
331
332fn print_json(response: &PickleValue) {
333    println!("{}", pickle_to_json(response));
334}
335
336fn pickle_to_json(value: &PickleValue) -> String {
337    match value {
338        PickleValue::None => "null".into(),
339        PickleValue::Bool(b) => if *b { "true" } else { "false" }.into(),
340        PickleValue::Int(n) => format!("{}", n),
341        PickleValue::Float(f) => format!("{}", f),
342        PickleValue::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
343        PickleValue::Bytes(b) => {
344            format!("\"{}\"", prettyhexrep(b))
345        }
346        PickleValue::List(items) => {
347            let inner: Vec<String> = items.iter().map(pickle_to_json).collect();
348            format!("[{}]", inner.join(", "))
349        }
350        PickleValue::Dict(pairs) => {
351            let inner: Vec<String> = pairs
352                .iter()
353                .map(|(k, v)| format!("{}: {}", pickle_to_json(k), pickle_to_json(v)))
354                .collect();
355            format!("{{{}}}", inner.join(", "))
356        }
357    }
358}
359
360fn remote_status(hash_str: &str, config_path: Option<&str>) {
361    let dest_hash = match crate::remote::parse_hex_hash(hash_str) {
362        Some(h) => h,
363        None => {
364            eprintln!(
365                "Invalid destination hash: {} (expected 32 hex chars)",
366                hash_str
367            );
368            process::exit(1);
369        }
370    };
371
372    eprintln!(
373        "Remote management query to {} (not yet fully implemented)",
374        prettyhexrep(&dest_hash),
375    );
376    eprintln!("Requires an active link to the remote management destination.");
377    eprintln!("This feature will work once rnsd is running and the remote node is reachable.");
378
379    let _ = (dest_hash, config_path);
380}
381
382fn print_usage() {
383    println!("Usage: rns-ctl status [OPTIONS] [FILTER]");
384    println!();
385    println!("Options:");
386    println!("  --config PATH, -c PATH  Path to config directory");
387    println!("  -a                      Show all interfaces");
388    println!("  -j                      JSON output");
389    println!("  -s SORT                 Sort by: rate, traffic, rx, tx");
390    println!("  -r                      Reverse sort order");
391    println!("  -t                      Show traffic totals");
392    println!("  -l                      Show link count");
393    println!("  -A                      Show announce statistics");
394    println!("  -m                      Monitor mode (loop)");
395    println!("  -I SECONDS              Monitor interval (default: 1.0)");
396    println!("  -R HASH                 Query remote node via management link");
397    println!("  -v                      Increase verbosity");
398    println!("  --version               Print version and exit");
399    println!("  --help, -h              Print this help");
400}