rnstatus.py
1 #!/usr/bin/env python3 2 3 # Reticulum License 4 # 5 # Copyright (c) 2016-2025 Mark Qvist 6 # 7 # Permission is hereby granted, free of charge, to any person obtaining a copy 8 # of this software and associated documentation files (the "Software"), to deal 9 # in the Software without restriction, including without limitation the rights 10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 # copies of the Software, and to permit persons to whom the Software is 12 # furnished to do so, subject to the following conditions: 13 # 14 # - The Software shall not be used in any kind of system which includes amongst 15 # its functions the ability to purposefully do harm to human beings. 16 # 17 # - The Software shall not be used, directly or indirectly, in the creation of 18 # an artificial intelligence, machine learning or language model training 19 # dataset, including but not limited to any use that contributes to the 20 # training or development of such a model or algorithm. 21 # 22 # - The above copyright notice and this permission notice shall be included in 23 # all copies or substantial portions of the Software. 24 # 25 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 # SOFTWARE. 32 33 import RNS 34 import os 35 import sys 36 import time 37 import argparse 38 import io 39 40 from RNS._version import __version__ 41 42 def size_str(num, suffix='B'): 43 units = ['','K','M','G','T','P','E','Z'] 44 last_unit = 'Y' 45 46 if suffix == 'b': 47 num *= 8 48 units = ['','K','M','G','T','P','E','Z'] 49 last_unit = 'Y' 50 51 for unit in units: 52 if abs(num) < 1000.0: 53 if unit == "": 54 return "%.0f %s%s" % (num, unit, suffix) 55 else: 56 return "%.2f %s%s" % (num, unit, suffix) 57 num /= 1000.0 58 59 return "%.2f%s%s" % (num, last_unit, suffix) 60 61 request_result = None 62 request_concluded = False 63 def get_remote_status(destination_hash, include_lstats, identity, no_output=False, timeout=RNS.Transport.PATH_REQUEST_TIMEOUT): 64 global request_result, request_concluded 65 link_count = None 66 67 if not RNS.Transport.has_path(destination_hash): 68 if not no_output: 69 print("Path to "+RNS.prettyhexrep(destination_hash)+" requested", end=" ") 70 sys.stdout.flush() 71 RNS.Transport.request_path(destination_hash) 72 pr_time = time.time() 73 while not RNS.Transport.has_path(destination_hash): 74 time.sleep(0.1) 75 if time.time() - pr_time > timeout: 76 if not no_output: 77 print("\r \r", end="") 78 print("Path request timed out") 79 exit(12) 80 81 remote_identity = RNS.Identity.recall(destination_hash) 82 83 def remote_link_closed(link): 84 if link.teardown_reason == RNS.Link.TIMEOUT: 85 if not no_output: 86 print("\r \r", end="") 87 print("The link timed out, exiting now") 88 elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED: 89 if not no_output: 90 print("\r \r", end="") 91 print("The link was closed by the server, exiting now") 92 else: 93 if not no_output: 94 print("\r \r", end="") 95 print("Link closed unexpectedly, exiting now") 96 exit(10) 97 98 def request_failed(request_receipt): 99 global request_result, request_concluded 100 if not no_output: 101 print("\r \r", end="") 102 print("The remote status request failed. Likely authentication failure.") 103 request_concluded = True 104 105 def got_response(request_receipt): 106 global request_result, request_concluded 107 response = request_receipt.response 108 if isinstance(response, list): 109 status = response[0] 110 if len(response) > 1: 111 link_count = response[1] 112 else: 113 link_count = None 114 115 request_result = (status, link_count) 116 117 request_concluded = True 118 119 def remote_link_established(link): 120 if not no_output: 121 print("\r \r", end="") 122 print("Sending request...", end=" ") 123 sys.stdout.flush() 124 link.identify(identity) 125 link.request("/status", data = [include_lstats], response_callback = got_response, failed_callback = request_failed) 126 127 if not no_output: 128 print("\r \r", end="") 129 print("Establishing link with remote transport instance...", end=" ") 130 sys.stdout.flush() 131 132 remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management") 133 link = RNS.Link(remote_destination) 134 link.set_link_established_callback(remote_link_established) 135 link.set_link_closed_callback(remote_link_closed) 136 137 while not request_concluded: 138 time.sleep(0.1) 139 140 if request_result != None: 141 print("\r \r", end="") 142 143 return request_result 144 145 def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=False, astats=False, lstats=False, sorting=None, sort_reverse=False, 146 remote=None, management_identity=None, remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT, must_exit=True, rns_instance=None, 147 traffic_totals=False, discovered_interfaces=False, config_entries=False): 148 149 if remote: require_shared = False 150 else: require_shared = True 151 152 try: 153 if rns_instance: 154 reticulum = rns_instance 155 must_exit = False 156 else: 157 reticulum = RNS.Reticulum(configdir=configdir, loglevel=3+verbosity, require_shared_instance=require_shared) 158 159 except Exception as e: 160 print("No shared RNS instance available to get status from") 161 if must_exit: exit(1) 162 else: return 163 164 link_count = None 165 stats = None 166 167 details = False 168 if config_entries: 169 discovered_interfaces = True 170 details = True 171 172 if discovered_interfaces: 173 if_discovery = RNS.Discovery.InterfaceDiscovery(discover_interfaces=False) 174 ifs = if_discovery.list_discovered_interfaces() 175 print("") 176 177 if json: 178 import json 179 for i in ifs: 180 for e in i: 181 if isinstance(i[e], bytes): i[e] = RNS.hexrep(i[e], delimit=False) 182 183 print(json.dumps(ifs)) 184 185 else: 186 filtered_ifs = [] 187 for i in ifs: 188 name = i["name"] 189 if not name_filter or name_filter.lower() in name.lower(): filtered_ifs.append(i) 190 191 if details: 192 for idx, i in enumerate(filtered_ifs): 193 try: 194 name = i["name"] 195 if_type = i["type"] 196 status = i["status"] 197 198 if status == "available": status_display = "Available" 199 elif status == "unknown": status_display = "Unknown" 200 elif status == "stale": status_display = "Stale" 201 else: status_display = status 202 203 now = time.time() 204 dago = now-i["discovered"] 205 hago = now-i["last_heard"] 206 discovered_display = f"{RNS.prettytime(dago, compact=True)} ago" 207 last_heard_display = f"{RNS.prettytime(hago, compact=True)} ago" 208 transport_str = "Enabled" if i["transport"] else "Disabled" 209 210 if i["latitude"] is not None and i["longitude"] is not None: 211 lat = round(i["latitude"], 4) 212 lon = round(i["longitude"], 4) 213 if i["height"] != None: height = ", "+str(i["height"])+"m h" 214 else: height = "" 215 location = f"{lat}, {lon}{height}" 216 else: location = "Unknown" 217 218 transport_id = None 219 network = None 220 if "transport_id" in i: transport_id = i["transport_id"] 221 if "transport_id" in i and "network_id" in i and i["transport_id"] != i["network_id"]: 222 network = i["network_id"] 223 224 if idx > 0: print("\n"+"="*32+"\n") 225 if network: print(f"Network ID : {network}") 226 if transport_id: print(f"Transport ID : {transport_id}") 227 228 print(f"Name : {name}") 229 print(f"Type : {if_type}") 230 print(f"Status : {status_display}") 231 print(f"Transport : {transport_str}") 232 print(f"Distance : {i['hops']} hop{'' if i['hops'] == 1 else 's'}") 233 print(f"Discovered : {discovered_display}") 234 print(f"Last Heard : {last_heard_display}") 235 print(f"Location : {location}") 236 237 if "frequency" in i: print(f"Frequency : {i['frequency']:,} Hz") 238 if "bandwidth" in i: print(f"Bandwidth : {i['bandwidth']:,} Hz") 239 if "sf" in i: print(f"Sprd. Factor : {i['sf']}") 240 if "cr" in i: print(f"Coding Rate : {i['cr']}") 241 if "modulation" in i: print(f"Modulation : {i['modulation']}") 242 if "reachable_on" in i: print(f"Address : {i['reachable_on']}") 243 if "reachable_on" in i: print(f"Port : {i['port']}") 244 245 print(f"Stamp Value : {i['value']}") 246 247 print(f"\nConfiguration Entry:") 248 config_lines = i["config_entry"].split('\n') 249 for line in config_lines: print(f" {line}") 250 251 except Exception as e: 252 pass 253 254 else: 255 print(f"{'Name':<25} {'Type':<12} {'Status':<12} {'Last Heard':<12} {'Value':<8} {'Location':<15}") 256 print("-" * 89) 257 258 for i in filtered_ifs: 259 try: 260 name = i["name"][:24] + "…" if len(i["name"]) > 24 else i["name"] 261 262 if_type = i["type"].replace("Interface", "") 263 264 status = i["status"] 265 if status == "available": status_display = "✓ Available" 266 elif status == "unknown": status_display = "? Unknown" 267 elif status == "stale": status_display = "× Stale" 268 else: status_display = status 269 270 now = time.time() 271 last_heard = i["last_heard"] 272 diff = now - last_heard 273 274 if diff < 60: last_heard_display = "Just now" 275 elif diff < 3600: 276 mins = int(diff / 60) 277 last_heard_display = f"{mins}m ago" 278 elif diff < 86400: 279 hours = int(diff / 3600) 280 last_heard_display = f"{hours}h ago" 281 else: 282 days = int(diff / 86400) 283 last_heard_display = f"{days}d ago" 284 285 value = str(i["value"]) 286 287 if i["latitude"] is not None and i["longitude"] is not None: 288 lat = round(i["latitude"], 4) 289 lon = round(i["longitude"], 4) 290 location = f"{lat}, {lon}" 291 else: location = "N/A" 292 293 print(f"{name:<25} {if_type:<12} {status_display:<12} {last_heard_display:<12} {value:<8} {location:<15}") 294 295 except Exception as e: 296 pass 297 298 if must_exit: exit(0) 299 else: return 300 301 if remote: 302 try: 303 if management_identity is None: 304 raise ValueError("Remote management requires an identity file. Use -i to specify the path to a management identity.") 305 306 dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 307 if len(remote) != dest_len: 308 raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)) 309 try: 310 identity_hash = bytes.fromhex(remote) 311 destination_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.remote.management", identity_hash) 312 except Exception as e: 313 raise ValueError("Invalid destination entered. Check your input.") 314 315 identity = RNS.Identity.from_file(os.path.expanduser(management_identity)) 316 if identity == None: 317 raise ValueError("Could not load management identity from "+str(management_identity)) 318 319 try: 320 remote_status = get_remote_status(destination_hash, lstats, identity, no_output=json, timeout=remote_timeout) 321 if remote_status != None: 322 stats, link_count = remote_status 323 except Exception as e: 324 raise e 325 326 except Exception as e: 327 print(str(e)) 328 if must_exit: exit(20) 329 else: return 330 331 else: 332 if lstats: 333 try: link_count = reticulum.get_link_count() 334 except Exception as e: pass 335 336 try: stats = reticulum.get_interface_stats() 337 except Exception as e: pass 338 339 if stats != None: 340 if json: 341 import json 342 for s in stats: 343 if isinstance(stats[s], bytes): 344 stats[s] = RNS.hexrep(stats[s], delimit=False) 345 346 if isinstance(stats[s], dict) or isinstance(stats[s], list): 347 for i in stats[s]: 348 if isinstance(i, dict): 349 for k in i: 350 if isinstance(i[k], bytes): 351 i[k] = RNS.hexrep(i[k], delimit=False) 352 353 print(json.dumps(stats)) 354 if must_exit: exit() 355 else: return 356 357 interfaces = stats["interfaces"] 358 if sorting != None and isinstance(sorting, str): 359 sorting = sorting.lower() 360 if sorting == "rate" or sorting == "bitrate": 361 interfaces.sort(key=lambda i: i["bitrate"], reverse=not sort_reverse) 362 if sorting == "rx": 363 interfaces.sort(key=lambda i: i["rxb"], reverse=not sort_reverse) 364 if sorting == "tx": 365 interfaces.sort(key=lambda i: i["txb"], reverse=not sort_reverse) 366 if sorting == "rxs": 367 interfaces.sort(key=lambda i: i["rxs"], reverse=not sort_reverse) 368 if sorting == "txs": 369 interfaces.sort(key=lambda i: i["txs"], reverse=not sort_reverse) 370 if sorting == "traffic": 371 interfaces.sort(key=lambda i: i["rxb"]+i["txb"], reverse=not sort_reverse) 372 if sorting == "announces" or sorting == "announce": 373 interfaces.sort(key=lambda i: i["incoming_announce_frequency"]+i["outgoing_announce_frequency"], reverse=not sort_reverse) 374 if sorting == "arx": 375 interfaces.sort(key=lambda i: i["incoming_announce_frequency"], reverse=not sort_reverse) 376 if sorting == "atx": 377 interfaces.sort(key=lambda i: i["outgoing_announce_frequency"], reverse=not sort_reverse) 378 if sorting == "held": 379 interfaces.sort(key=lambda i: i["held_announces"], reverse=not sort_reverse) 380 381 382 for ifstat in interfaces: 383 name = ifstat["name"] 384 385 if dispall or not ( 386 name.startswith("LocalInterface[") or 387 name.startswith("TCPInterface[Client") or 388 name.startswith("BackboneInterface[Client on") or 389 name.startswith("AutoInterfacePeer[") or 390 name.startswith("WeaveInterfacePeer[") or 391 name.startswith("I2PInterfacePeer[Connected peer") or 392 (name.startswith("I2PInterface[") and ("i2p_connectable" in ifstat and ifstat["i2p_connectable"] == False)) 393 ): 394 395 if not (name.startswith("I2PInterface[") and ("i2p_connectable" in ifstat and ifstat["i2p_connectable"] == False)): 396 if name_filter == None or name_filter.lower() in name.lower(): 397 print("") 398 399 if ifstat["status"]: ss = "Up" 400 else: ss = "Down" 401 402 if ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_ACCESS_POINT: modestr = "Access Point" 403 elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_POINT_TO_POINT: modestr = "Point-to-Point" 404 elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_ROAMING: modestr = "Roaming" 405 elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_BOUNDARY: modestr = "Boundary" 406 elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_GATEWAY: modestr = "Gateway" 407 else: modestr = "Full" 408 409 410 if ifstat["clients"] != None: 411 clients = ifstat["clients"] 412 if name.startswith("Shared Instance["): 413 cnum = max(clients-1,0) 414 if cnum == 1: 415 spec_str = " program" 416 else: 417 spec_str = " programs" 418 419 clients_string = "Serving : "+str(cnum)+spec_str 420 elif name.startswith("I2PInterface["): 421 if "i2p_connectable" in ifstat and ifstat["i2p_connectable"] == True: 422 cnum = clients 423 if cnum == 1: 424 spec_str = " connected I2P endpoint" 425 else: 426 spec_str = " connected I2P endpoints" 427 428 clients_string = "Peers : "+str(cnum)+spec_str 429 else: 430 clients_string = "" 431 else: 432 clients_string = "Clients : "+str(clients) 433 434 else: 435 clients = None 436 437 print(" {n}".format(n=ifstat["name"])) 438 439 if "autoconnect_source" in ifstat and ifstat["autoconnect_source"] != None: 440 print(" Source : Auto-connect via <{ns}>".format(ns=ifstat["autoconnect_source"])) 441 442 if "ifac_netname" in ifstat and ifstat["ifac_netname"] != None: 443 print(" Network : {nn}".format(nn=ifstat["ifac_netname"])) 444 445 print(" Status : {ss}".format(ss=ss)) 446 447 if clients != None and clients_string != "": 448 print(" "+clients_string) 449 450 if not (name.startswith("Shared Instance[") or name.startswith("TCPInterface[Client") or name.startswith("LocalInterface[")): 451 print(" Mode : {mode}".format(mode=modestr)) 452 453 if "bitrate" in ifstat and ifstat["bitrate"] != None: 454 print(" Rate : {ss}".format(ss=speed_str(ifstat["bitrate"]))) 455 456 if "noise_floor" in ifstat: 457 if not "interference" in ifstat: nstr = "" 458 else: 459 nf = ifstat["interference"] 460 lstr = ", no interference" 461 if "interference_last_ts" in ifstat and "interference_last_dbm" in ifstat: 462 lago = time.time()-ifstat["interference_last_ts"] 463 ldbm = ifstat["interference_last_dbm"] 464 lstr = f"\n Intrfrnc. : {ldbm} dBm {RNS.prettytime(lago, compact=True)} ago" 465 466 467 nstr = f"\n Intrfrnc. : {nf} dBm" if nf else lstr 468 469 if ifstat["noise_floor"] != None: print(" Noise Fl. : {nfl} dBm{ntr}".format(nfl=str(ifstat["noise_floor"]), ntr=nstr)) 470 else: print(" Noise Fl. : Unknown") 471 472 if "cpu_load" in ifstat: 473 if ifstat["cpu_load"] != None: print(" CPU load : {v} %".format(v=str(ifstat["cpu_load"]))) 474 else: print(" CPU load : Unknown") 475 476 if "cpu_temp" in ifstat: 477 if ifstat["cpu_temp"] != None: print(" CPU temp : {v}°C".format(v=str(ifstat["cpu_temp"]))) 478 else: print(" CPU load : Unknown") 479 480 if "mem_load" in ifstat: 481 if ifstat["cpu_load"] != None: print(" Mem usage : {v} %".format(v=str(ifstat["mem_load"]))) 482 else: print(" Mem usage : Unknown") 483 484 if "battery_percent" in ifstat and ifstat["battery_percent"] != None: 485 try: 486 bpi = int(ifstat["battery_percent"]) 487 bss = ifstat["battery_state"] 488 print(f" Battery : {bpi}% ({bss})") 489 except: 490 pass 491 492 if "airtime_short" in ifstat and "airtime_long" in ifstat: 493 print(" Airtime : {ats}% (15s), {atl}% (1h)".format(ats=str(ifstat["airtime_short"]),atl=str(ifstat["airtime_long"]))) 494 495 if "channel_load_short" in ifstat and "channel_load_long" in ifstat: 496 print(" Ch. Load : {ats}% (15s), {atl}% (1h)".format(ats=str(ifstat["channel_load_short"]),atl=str(ifstat["channel_load_long"]))) 497 498 if "switch_id" in ifstat: 499 if ifstat["switch_id"] != None: print(" Switch ID : {v}".format(v=str(ifstat["switch_id"]))) 500 else: print(" Switch ID : Unknown") 501 502 if "endpoint_id" in ifstat: 503 if ifstat["endpoint_id"] != None: print(" Endpoint : {v}".format(v=str(ifstat["endpoint_id"]))) 504 else: print(" Endpoint : Unknown") 505 506 if "via_switch_id" in ifstat: 507 if ifstat["via_switch_id"] != None: print(" Via : {v}".format(v=str(ifstat["via_switch_id"]))) 508 else: print(" Via : Unknown") 509 510 if "peers" in ifstat and ifstat["peers"] != None: 511 print(" Peers : {np} reachable".format(np=ifstat["peers"])) 512 513 if "tunnelstate" in ifstat and ifstat["tunnelstate"] != None: 514 print(" I2P : {ts}".format(ts=ifstat["tunnelstate"])) 515 516 if "ifac_signature" in ifstat and ifstat["ifac_signature"] != None: 517 sigstr = "<…"+RNS.hexrep(ifstat["ifac_signature"][-5:], delimit=False)+">" 518 print(" Access : {nb}-bit IFAC by {sig}".format(nb=ifstat["ifac_size"]*8, sig=sigstr)) 519 520 if "i2p_b32" in ifstat and ifstat["i2p_b32"] != None: 521 print(" I2P B32 : {ep}".format(ep=str(ifstat["i2p_b32"]))) 522 523 if astats and "announce_queue" in ifstat and ifstat["announce_queue"] != None and ifstat["announce_queue"] > 0: 524 aqn = ifstat["announce_queue"] 525 if aqn == 1: 526 print(" Queued : {np} announce".format(np=aqn)) 527 else: 528 print(" Queued : {np} announces".format(np=aqn)) 529 530 if astats and "held_announces" in ifstat and ifstat["held_announces"] != None and ifstat["held_announces"] > 0: 531 aqn = ifstat["held_announces"] 532 if aqn == 1: 533 print(" Held : {np} announce".format(np=aqn)) 534 else: 535 print(" Held : {np} announces".format(np=aqn)) 536 537 if astats and "incoming_announce_frequency" in ifstat and ifstat["incoming_announce_frequency"] != None: 538 print(" Announces : {iaf}↑".format(iaf=RNS.prettyfrequency(ifstat["outgoing_announce_frequency"]))) 539 print(" {iaf}↓".format(iaf=RNS.prettyfrequency(ifstat["incoming_announce_frequency"]))) 540 541 rxb_str = "↓"+RNS.prettysize(ifstat["rxb"]) 542 txb_str = "↑"+RNS.prettysize(ifstat["txb"]) 543 strdiff = len(rxb_str)-len(txb_str) 544 if strdiff > 0: 545 txb_str += " "*strdiff 546 elif strdiff < 0: 547 rxb_str += " "*-strdiff 548 549 rxstat = rxb_str 550 txstat = txb_str 551 if "rxs" in ifstat and "txs" in ifstat: 552 rxstat += " "+RNS.prettyspeed(ifstat["rxs"]) 553 txstat += " "+RNS.prettyspeed(ifstat["txs"]) 554 555 print(f" Traffic : {txstat}\n {rxstat}") 556 557 lstr = "" 558 if link_count != None and lstats: 559 ms = "y" if link_count == 1 else "ies" 560 if "transport_id" in stats and stats["transport_id"] != None: 561 lstr = f", {link_count} entr{ms} in link table" 562 else: 563 lstr = f" {link_count} entr{ms} in link table" 564 565 if traffic_totals: 566 rxb_str = "↓"+RNS.prettysize(stats["rxb"]) 567 txb_str = "↑"+RNS.prettysize(stats["txb"]) 568 strdiff = len(rxb_str)-len(txb_str) 569 if strdiff > 0: 570 txb_str += " "*strdiff 571 elif strdiff < 0: 572 rxb_str += " "*-strdiff 573 574 rxstat = rxb_str+" "+RNS.prettyspeed(stats["rxs"]) 575 txstat = txb_str+" "+RNS.prettyspeed(stats["txs"]) 576 print(f"\n Totals : {txstat}\n {rxstat}") 577 578 if "transport_id" in stats and stats["transport_id"] != None: 579 print("\n Transport Instance "+RNS.prettyhexrep(stats["transport_id"])+" running") 580 if "network_id" in stats and stats["network_id"] != None: 581 print(" Network Identity "+RNS.prettyhexrep(stats["network_id"])) 582 if "probe_responder" in stats and stats["probe_responder"] != None: 583 print(" Probe responder at "+RNS.prettyhexrep(stats["probe_responder"])+ " active") 584 if "transport_uptime" in stats and stats["transport_uptime"] != None: 585 print(" Uptime is "+RNS.prettytime(stats["transport_uptime"])+lstr) 586 else: 587 if lstr != "": 588 print(f"\n{lstr}") 589 590 print("") 591 592 else: 593 if not remote: 594 print("Could not get RNS status") 595 else: 596 print("Could not get RNS status from remote transport instance "+RNS.prettyhexrep(identity_hash)) 597 if must_exit: 598 exit(2) 599 else: 600 return 601 602 def main(must_exit=True, rns_instance=None): 603 try: 604 parser = argparse.ArgumentParser(description="Reticulum Network Stack Status") 605 parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str) 606 parser.add_argument("--version", action="version", version="rnstatus {version}".format(version=__version__)) 607 608 parser.add_argument("-a", "--all", action="store_true", help="show all interfaces", default=False) 609 parser.add_argument("-A", "--announce-stats", action="store_true", help="show announce stats", default=False) 610 parser.add_argument("-l", "--link-stats", action="store_true", help="show link stats", default=False) 611 parser.add_argument("-t", "--totals", action="store_true", help="display traffic totals", default=False) 612 parser.add_argument("-s", "--sort", action="store", help="sort interfaces by [rate, traffic, rx, tx, rxs, txs, announces, arx, atx, held]", default=None, type=str) 613 parser.add_argument("-r", "--reverse", action="store_true", help="reverse sorting", default=False) 614 parser.add_argument("-j", "--json", action="store_true", help="output in JSON format", default=False) 615 parser.add_argument("-R", action="store", metavar="hash", help="transport identity hash of remote instance to get status from", default=None, type=str) 616 parser.add_argument("-i", action="store", metavar="path", help="path to identity used for remote management", default=None, type=str) 617 parser.add_argument("-w", action="store", metavar="seconds", type=float, help="timeout before giving up on remote queries", default=RNS.Transport.PATH_REQUEST_TIMEOUT) 618 parser.add_argument("-d", "--discovered", action="store_true", help="list discovered interfaces", default=False) 619 parser.add_argument("-D", action="store_true", help="show details and config entries for discovered interfaces", default=False) 620 parser.add_argument("-m", "--monitor", action="store_true", help="continuously monitor status", default=False) 621 parser.add_argument("-I", "--monitor-interval", action="store", metavar="seconds", type=float, help="refresh interval for monitor mode (default: 1)", default=1.0) 622 parser.add_argument('-v', '--verbose', action='count', default=0) 623 parser.add_argument("filter", nargs="?", default=None, help="only display interfaces with names including filter", type=str) 624 625 args = parser.parse_args() 626 627 if args.config: configarg = args.config 628 else: configarg = None 629 630 if args.monitor: 631 if args.R: require_shared = False 632 else: require_shared = True 633 634 try: reticulum = RNS.Reticulum(configdir=configarg, loglevel=3+args.verbose, require_shared_instance=require_shared) 635 except Exception as e: 636 print("No shared RNS instance available to get status from") 637 exit(1) 638 639 while True: 640 buffer = io.StringIO() 641 old_stdout = sys.stdout 642 sys.stdout = buffer 643 644 try: 645 program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose, name_filter=args.filter, json=args.json, 646 astats=args.announce_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse, remote=args.R, 647 management_identity=args.i, remote_timeout=args.w, must_exit=False, rns_instance=reticulum, traffic_totals=args.totals, 648 discovered_interfaces=args.discovered, config_entries=args.D) 649 650 finally: 651 sys.stdout = old_stdout 652 653 output = buffer.getvalue() 654 print("\033[H\033[2J", end="") 655 print(output, end="", flush=True) 656 657 time.sleep(args.monitor_interval) 658 659 else: 660 program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose, name_filter=args.filter, json=args.json, 661 astats=args.announce_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse, remote=args.R, 662 management_identity=args.i, remote_timeout=args.w, must_exit=must_exit, rns_instance=rns_instance, traffic_totals=args.totals, 663 discovered_interfaces=args.discovered, config_entries=args.D) 664 665 except KeyboardInterrupt: 666 print("") 667 if must_exit: exit() 668 else: return 669 670 def speed_str(num, suffix='bps'): 671 units = ['','k','M','G','T','P','E','Z'] 672 last_unit = 'Y' 673 674 if suffix == 'Bps': 675 num /= 8 676 units = ['','K','M','G','T','P','E','Z'] 677 last_unit = 'Y' 678 679 for unit in units: 680 if abs(num) < 1000.0: 681 return "%3.2f %s%s" % (num, unit, suffix) 682 num /= 1000.0 683 684 return "%.2f %s%s" % (num, last_unit, suffix) 685 686 if __name__ == "__main__": 687 main()