1use 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 if let Some(ref hash_str) = remote_hash {
52 remote_status(hash_str, config_path.as_deref());
53 return;
54 }
55
56 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 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 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 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 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 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 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 if let Some(interfaces) = response.get("interfaces").and_then(|v| v.as_list()) {
201 let mut iface_list: Vec<&PickleValue> = interfaces.iter().collect();
203
204 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 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 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}