Added rnstatus utility

This commit is contained in:
Mark Qvist 2021-09-24 20:10:04 +02:00
parent f5510f9777
commit 7991db5c74
11 changed files with 234 additions and 7 deletions

View File

@ -48,6 +48,9 @@ class AX25KISSInterface(Interface):
serial = None serial = None
def __init__(self, owner, name, callsign, ssid, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control): def __init__(self, owner, name, callsign, ssid, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control):
self.rxb = 0
self.txb = 0
self.serial = None self.serial = None
self.owner = owner self.owner = owner
self.name = name self.name = name
@ -188,10 +191,12 @@ class AX25KISSInterface(Interface):
def processIncoming(self, data): def processIncoming(self, data):
if (len(data) > AX25.HEADER_SIZE): if (len(data) > AX25.HEADER_SIZE):
self.rxb += len(data)
self.owner.inbound(data[AX25.HEADER_SIZE:], self) self.owner.inbound(data[AX25.HEADER_SIZE:], self)
def processOutgoing(self,data): def processOutgoing(self,data):
datalen = len(data)
if self.online: if self.online:
if self.interface_ready: if self.interface_ready:
if self.flow_control: if self.flow_control:
@ -224,6 +229,8 @@ class AX25KISSInterface(Interface):
kiss_frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND]) kiss_frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_frame) written = self.serial.write(kiss_frame)
self.txb += datalen
if written != len(kiss_frame): if written != len(kiss_frame):
if self.flow_control: if self.flow_control:
self.interface_ready = True self.interface_ready = True

View File

@ -8,7 +8,9 @@ class Interface:
name = None name = None
def __init__(self): def __init__(self):
pass self.rxb = 0
self.txb = 0
self.online = False
def get_hash(self): def get_hash(self):
return RNS.Identity.full_hash(str(self).encode("utf-8")) return RNS.Identity.full_hash(str(self).encode("utf-8"))

View File

@ -40,6 +40,9 @@ class KISSInterface(Interface):
serial = None serial = None
def __init__(self, owner, name, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control, beacon_interval, beacon_data): def __init__(self, owner, name, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control, beacon_interval, beacon_data):
self.rxb = 0
self.txb = 0
if beacon_data == None: if beacon_data == None:
beacon_data = "" beacon_data = ""
@ -174,10 +177,12 @@ class KISSInterface(Interface):
def processIncoming(self, data): def processIncoming(self, data):
self.rxb += len(data)
self.owner.inbound(data, self) self.owner.inbound(data, self)
def processOutgoing(self,data): def processOutgoing(self,data):
datalen = len(data)
if self.online: if self.online:
if self.interface_ready: if self.interface_ready:
if self.flow_control: if self.flow_control:
@ -189,6 +194,7 @@ class KISSInterface(Interface):
frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND]) frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND])
written = self.serial.write(frame) written = self.serial.write(frame)
self.txb += datalen
if data == self.beacon_d: if data == self.beacon_d:
self.first_tx = None self.first_tx = None

View File

@ -24,6 +24,10 @@ class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
class LocalClientInterface(Interface): class LocalClientInterface(Interface):
def __init__(self, owner, name, target_port = None, connected_socket=None): def __init__(self, owner, name, target_port = None, connected_socket=None):
self.rxb = 0
self.txb = 0
self.online = False
self.IN = True self.IN = True
self.OUT = False self.OUT = False
self.socket = None self.socket = None
@ -58,6 +62,10 @@ class LocalClientInterface(Interface):
thread.start() thread.start()
def processIncoming(self, data): def processIncoming(self, data):
self.rxb += len(data)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.rxb += len(data)
self.owner.inbound(data, self) self.owner.inbound(data, self)
def processOutgoing(self, data): def processOutgoing(self, data):
@ -70,6 +78,10 @@ class LocalClientInterface(Interface):
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG]) data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
self.socket.sendall(data) self.socket.sendall(data)
self.writing = False self.writing = False
self.txb += len(data)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.txb += len(data)
except Exception as e: except Exception as e:
RNS.log("Exception occurred while transmitting via "+str(self)+", tearing down interface", RNS.LOG_ERROR) RNS.log("Exception occurred while transmitting via "+str(self)+", tearing down interface", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
@ -147,6 +159,8 @@ class LocalClientInterface(Interface):
if self in RNS.Transport.local_client_interfaces: if self in RNS.Transport.local_client_interfaces:
RNS.Transport.local_client_interfaces.remove(self) RNS.Transport.local_client_interfaces.remove(self)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.clients -= 1
if nowarning == False: if nowarning == False:
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR) RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR)
@ -170,6 +184,11 @@ class LocalClientInterface(Interface):
class LocalServerInterface(Interface): class LocalServerInterface(Interface):
def __init__(self, owner, bindport=None): def __init__(self, owner, bindport=None):
self.rxb = 0
self.txb = 0
self.online = False
self.clients = 0
self.IN = True self.IN = True
self.OUT = False self.OUT = False
self.name = "Reticulum" self.name = "Reticulum"
@ -196,6 +215,9 @@ class LocalServerInterface(Interface):
thread.setDaemon(True) thread.setDaemon(True)
thread.start() thread.start()
self.online = True
def incoming_connection(self, handler): def incoming_connection(self, handler):
interface_name = str(str(handler.client_address[1])) interface_name = str(str(handler.client_address[1]))
@ -208,13 +230,14 @@ class LocalServerInterface(Interface):
RNS.log("Accepting new connection to shared instance: "+str(spawned_interface), RNS.LOG_VERBOSE) RNS.log("Accepting new connection to shared instance: "+str(spawned_interface), RNS.LOG_VERBOSE)
RNS.Transport.interfaces.append(spawned_interface) RNS.Transport.interfaces.append(spawned_interface)
RNS.Transport.local_client_interfaces.append(spawned_interface) RNS.Transport.local_client_interfaces.append(spawned_interface)
self.clients += 1
spawned_interface.read_loop() spawned_interface.read_loop()
def processOutgoing(self, data): def processOutgoing(self, data):
pass pass
def __str__(self): def __str__(self):
return "Shared Instance ["+str(self.bind_port)+"]" return "Shared Instance["+str(self.bind_port)+"]"
class LocalInterfaceHandler(socketserver.BaseRequestHandler): class LocalInterfaceHandler(socketserver.BaseRequestHandler):
def __init__(self, callback, *args, **keys): def __init__(self, callback, *args, **keys):

View File

@ -72,6 +72,9 @@ class RNodeInterface(Interface):
CALLSIGN_MAX_LEN = 32 CALLSIGN_MAX_LEN = 32
def __init__(self, owner, name, port, frequency = None, bandwidth = None, txpower = None, sf = None, cr = None, flow_control = False, id_interval = None, id_callsign = None): def __init__(self, owner, name, port, frequency = None, bandwidth = None, txpower = None, sf = None, cr = None, flow_control = False, id_interval = None, id_callsign = None):
self.rxb = 0
self.txb = 0
self.serial = None self.serial = None
self.owner = owner self.owner = owner
self.name = name self.name = name
@ -278,10 +281,12 @@ class RNodeInterface(Interface):
self.bitrate = 0 self.bitrate = 0
def processIncoming(self, data): def processIncoming(self, data):
self.rxb += len(data)
self.owner.inbound(data, self) self.owner.inbound(data, self)
def processOutgoing(self,data): def processOutgoing(self,data):
datalen = len(data)
if self.online: if self.online:
if self.interface_ready: if self.interface_ready:
if self.flow_control: if self.flow_control:
@ -297,6 +302,7 @@ class RNodeInterface(Interface):
frame = bytes([0xc0])+bytes([0x00])+data+bytes([0xc0]) frame = bytes([0xc0])+bytes([0x00])+data+bytes([0xc0])
written = self.serial.write(frame) written = self.serial.write(frame)
self.txb += datalen
if written != len(frame): if written != len(frame):
raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))

View File

@ -31,6 +31,9 @@ class SerialInterface(Interface):
serial = None serial = None
def __init__(self, owner, name, port, speed, databits, parity, stopbits): def __init__(self, owner, name, port, speed, databits, parity, stopbits):
self.rxb = 0
self.txb = 0
self.serial = None self.serial = None
self.owner = owner self.owner = owner
self.name = name self.name = name
@ -79,6 +82,7 @@ class SerialInterface(Interface):
def processIncoming(self, data): def processIncoming(self, data):
self.rxb += len(data)
self.owner.inbound(data, self) self.owner.inbound(data, self)
@ -86,6 +90,7 @@ class SerialInterface(Interface):
if self.online: if self.online:
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG]) data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
written = self.serial.write(data) written = self.serial.write(data)
self.txb += len(data)
if written != len(data): if written != len(data):
raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))

View File

@ -34,6 +34,9 @@ class TCPClientInterface(Interface):
TCP_PROBES = 5 TCP_PROBES = 5
def __init__(self, owner, name, target_ip=None, target_port=None, connected_socket=None, max_reconnect_tries=None): def __init__(self, owner, name, target_ip=None, target_port=None, connected_socket=None, max_reconnect_tries=None):
self.rxb = 0
self.txb = 0
self.IN = True self.IN = True
self.OUT = False self.OUT = False
self.socket = None self.socket = None
@ -177,6 +180,10 @@ class TCPClientInterface(Interface):
raise IOError("Attempt to reconnect on a non-initiator TCP interface") raise IOError("Attempt to reconnect on a non-initiator TCP interface")
def processIncoming(self, data): def processIncoming(self, data):
self.rxb += len(data)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.rxb += len(data)
self.owner.inbound(data, self) self.owner.inbound(data, self)
def processOutgoing(self, data): def processOutgoing(self, data):
@ -189,6 +196,10 @@ class TCPClientInterface(Interface):
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG]) data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
self.socket.sendall(data) self.socket.sendall(data)
self.writing = False self.writing = False
self.txb += len(data)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.txb += len(data)
except Exception as e: except Exception as e:
RNS.log("Exception occurred while transmitting via "+str(self)+", tearing down interface", RNS.LOG_ERROR) RNS.log("Exception occurred while transmitting via "+str(self)+", tearing down interface", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
@ -248,7 +259,7 @@ class TCPClientInterface(Interface):
self.teardown() self.teardown()
def teardown(self): def teardown(self):
if self.initiator: if self.initiator and not self.detached:
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR) RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR)
if RNS.Reticulum.panic_on_interface_error: if RNS.Reticulum.panic_on_interface_error:
RNS.panic() RNS.panic()
@ -260,6 +271,9 @@ class TCPClientInterface(Interface):
self.OUT = False self.OUT = False
self.IN = False self.IN = False
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.clients -= 1
if self in RNS.Transport.interfaces: if self in RNS.Transport.interfaces:
RNS.Transport.interfaces.remove(self) RNS.Transport.interfaces.remove(self)
@ -277,6 +291,11 @@ class TCPServerInterface(Interface):
return netifaces.ifaddresses(name)[netifaces.AF_INET][0]['broadcast'] return netifaces.ifaddresses(name)[netifaces.AF_INET][0]['broadcast']
def __init__(self, owner, name, device=None, bindip=None, bindport=None): def __init__(self, owner, name, device=None, bindip=None, bindport=None):
self.rxb = 0
self.txb = 0
self.online = False
self.clients = 0
self.IN = True self.IN = True
self.OUT = False self.OUT = False
self.name = name self.name = name
@ -304,6 +323,8 @@ class TCPServerInterface(Interface):
thread.setDaemon(True) thread.setDaemon(True)
thread.start() thread.start()
self.online = True
def incoming_connection(self, handler): def incoming_connection(self, handler):
RNS.log("Accepting incoming TCP connection", RNS.LOG_VERBOSE) RNS.log("Accepting incoming TCP connection", RNS.LOG_VERBOSE)
@ -317,6 +338,7 @@ class TCPServerInterface(Interface):
spawned_interface.online = True spawned_interface.online = True
RNS.log("Spawned new TCPClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE) RNS.log("Spawned new TCPClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE)
RNS.Transport.interfaces.append(spawned_interface) RNS.Transport.interfaces.append(spawned_interface)
self.clients += 1
spawned_interface.read_loop() spawned_interface.read_loop()
def processOutgoing(self, data): def processOutgoing(self, data):

View File

@ -18,9 +18,12 @@ class UDPInterface(Interface):
return netifaces.ifaddresses(name)[netifaces.AF_INET][0]['broadcast'] return netifaces.ifaddresses(name)[netifaces.AF_INET][0]['broadcast']
def __init__(self, owner, name, device=None, bindip=None, bindport=None, forwardip=None, forwardport=None): def __init__(self, owner, name, device=None, bindip=None, bindport=None, forwardip=None, forwardport=None):
self.rxb = 0
self.txb = 0
self.IN = True self.IN = True
self.OUT = False self.OUT = False
self.name = name self.name = name
self.online = False
if device != None: if device != None:
if bindip == None: if bindip == None:
@ -47,6 +50,8 @@ class UDPInterface(Interface):
thread.setDaemon(True) thread.setDaemon(True)
thread.start() thread.start()
self.online = True
if (forwardip != None and forwardport != None): if (forwardip != None and forwardport != None):
self.forwards = True self.forwards = True
self.forward_ip = forwardip self.forward_ip = forwardip
@ -54,6 +59,7 @@ class UDPInterface(Interface):
def processIncoming(self, data): def processIncoming(self, data):
self.rxb += len(data)
self.owner.inbound(data, self) self.owner.inbound(data, self)
def processOutgoing(self,data): def processOutgoing(self,data):
@ -61,6 +67,8 @@ class UDPInterface(Interface):
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
udp_socket.sendto(data, (self.forward_ip, self.forward_port)) udp_socket.sendto(data, (self.forward_ip, self.forward_port))
self.txb += len(data)
except Exception as e: except Exception as e:
RNS.log("Could not transmit on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("Could not transmit on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)

View File

@ -1,8 +1,10 @@
from .Interfaces import * from .Interfaces import *
import configparser
from .vendor.configobj import ConfigObj from .vendor.configobj import ConfigObj
import configparser
import multiprocessing.connection
import RNS import RNS
import signal import signal
import threading
import atexit import atexit
import struct import struct
import array import array
@ -108,7 +110,9 @@ class Reticulum:
Reticulum.panic_on_interface_error = False Reticulum.panic_on_interface_error = False
self.local_interface_port = 37428 self.local_interface_port = 37428
self.share_instance = True self.local_control_port = 37429
self.share_instance = True
self.rpc_listener = None
self.requested_loglevel = loglevel self.requested_loglevel = loglevel
if self.requested_loglevel != None: if self.requested_loglevel != None:
@ -153,6 +157,15 @@ class Reticulum:
RNS.Transport.start(self) RNS.Transport.start(self)
self.rpc_addr = ("127.0.0.1", self.local_control_port)
self.rpc_key = RNS.Identity.full_hash(RNS.Transport.identity.get_private_key())
if self.is_shared_instance:
self.rpc_listener = multiprocessing.connection.Listener(self.rpc_addr, authkey=self.rpc_key)
thread = threading.Thread(target=self.rpc_loop)
thread.setDaemon(True)
thread.start()
atexit.register(Reticulum.exit_handler) atexit.register(Reticulum.exit_handler)
signal.signal(signal.SIGINT, Reticulum.sigint_handler) signal.signal(signal.SIGINT, Reticulum.sigint_handler)
@ -165,6 +178,7 @@ class Reticulum:
) )
interface.OUT = True interface.OUT = True
RNS.Transport.interfaces.append(interface) RNS.Transport.interfaces.append(interface)
self.is_shared_instance = True self.is_shared_instance = True
RNS.log("Started shared instance interface: "+str(interface), RNS.LOG_DEBUG) RNS.log("Started shared instance interface: "+str(interface), RNS.LOG_DEBUG)
except Exception as e: except Exception as e:
@ -211,6 +225,9 @@ class Reticulum:
if option == "shared_instance_port": if option == "shared_instance_port":
value = int(self.config["reticulum"][option]) value = int(self.config["reticulum"][option])
self.local_interface_port = value self.local_interface_port = value
if option == "instance_control_port":
value = int(self.config["reticulum"][option])
self.local_control_port = value
if option == "enable_transport": if option == "enable_transport":
v = self.config["reticulum"].as_bool(option) v = self.config["reticulum"].as_bool(option)
if v == True: if v == True:
@ -455,7 +472,7 @@ class Reticulum:
RNS.Transport.interfaces.append(interface) RNS.Transport.interfaces.append(interface)
else: else:
RNS.log("Skipping disabled interface \""+name+"\"", RNS.LOG_NOTICE) RNS.log("Skipping disabled interface \""+name+"\"", RNS.LOG_INFO)
except Exception as e: except Exception as e:
RNS.log("The interface \""+name+"\" could not be created. Check your configuration file for errors!", RNS.LOG_ERROR) RNS.log("The interface \""+name+"\" could not be created. Check your configuration file for errors!", RNS.LOG_ERROR)
@ -475,6 +492,47 @@ class Reticulum:
self.config.write() self.config.write()
self.__apply_config() self.__apply_config()
def rpc_loop(self):
while True:
try:
rpc_connection = self.rpc_listener.accept()
call = rpc_connection.recv()
if "get" in call:
path = call["get"]
if path == "interface_stats":
rpc_connection.send(self.get_interface_stats())
rpc_connection.close()
except Exception as e:
RNS.log("An error ocurred while handling RPC call from local client: "+str(e), RNS.LOG_ERROR)
def get_interface_stats(self):
if self.is_connected_to_shared_instance:
rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
rpc_connection.send({"get": "interface_stats"})
response = rpc_connection.recv()
return response
else:
stats = []
for interface in RNS.Transport.interfaces:
ifstats = {}
if hasattr(interface, "clients"):
ifstats["clients"] = interface.clients
else:
ifstats["clients"] = None
ifstats["name"] = str(interface)
ifstats["rxb"] = interface.rxb
ifstats["txb"] = interface.txb
ifstats["status"] = interface.online
stats.append(ifstats)
return stats
@staticmethod @staticmethod
def should_use_implicit_proof(): def should_use_implicit_proof():
""" """

View File

@ -124,7 +124,7 @@ def main():
parser.add_argument( parser.add_argument(
"--version", "--version",
action="version", action="version",
version="rnpath {version}".format(version=__version__) version="rnprobe {version}".format(version=__version__)
) )
parser.add_argument( parser.add_argument(

90
RNS/Utilities/rnstatus.py Normal file
View File

@ -0,0 +1,90 @@
#!/usr/bin/env python3
import RNS
import argparse
from RNS._version import __version__
def size_str(num, suffix='B'):
units = ['','K','M','G','T','P','E','Z']
last_unit = 'Y'
if suffix == 'b':
num *= 8
units = ['','K','M','G','T','P','E','Z']
last_unit = 'Y'
for unit in units:
if abs(num) < 1000.0:
if unit == "":
return "%.0f %s%s" % (num, unit, suffix)
else:
return "%.2f %s%s" % (num, unit, suffix)
num /= 1000.0
return "%.2f%s%s" % (num, last_unit, suffix)
def program_setup(configdir, dispall=False, verbosity = 0):
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
ifstats = reticulum.get_interface_stats()
if ifstats != None:
for ifstat in ifstats:
name = ifstat["name"]
if dispall or not (name.startswith("LocalInterface[") or name.startswith("TCPInterface[Client")):
if ifstat["status"]:
ss = "Up"
else:
ss = "Down"
if ifstat["clients"] != None:
clients = ifstat["clients"]
if name.startswith("Shared Instance["):
clients_string = "Connected applications: "+str(clients-1)
else:
clients_string = "Connected clients: "+str(clients)
else:
clients = None
print(" {n}".format(n=ifstat["name"]))
print("\tStatus: {ss}".format(ss=ss))
if clients != None:
print("\t"+clients_string)
print("\tRX: {rxb}\n\tTX: {txb}".format(rxb=size_str(ifstat["rxb"]), txb=size_str(ifstat["txb"])))
print("")
else:
print("Could not get RNS status")
def main():
try:
parser = argparse.ArgumentParser(description="Reticulum Network Stack Status")
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
parser.add_argument("--version", action="version", version="rnstatus {version}".format(version=__version__))
parser.add_argument(
"-a",
"--all",
action="store_true",
help="show all interfaces",
default=False
)
parser.add_argument('-v', '--verbose', action='count', default=0)
args = parser.parse_args()
if args.config:
configarg = args.config
else:
configarg = None
program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose)
except KeyboardInterrupt:
print("")
exit()
if __name__ == "__main__":
main()