mirror of
				https://github.com/craigerl/aprsd.git
				synced 2025-10-31 04:40:22 -04:00 
			
		
		
		
	Merge pull request #159 from craigerl/stats-rework
Reworked the stats making the rpc server obsolete.
This commit is contained in:
		
						commit
						1267a53ec8
					
				
							
								
								
									
										111
									
								
								aprsd/client.py
									
									
									
									
									
								
							
							
						
						
									
										111
									
								
								aprsd/client.py
									
									
									
									
									
								
							| @ -1,15 +1,18 @@ | ||||
| import abc | ||||
| import datetime | ||||
| import logging | ||||
| import threading | ||||
| import time | ||||
| 
 | ||||
| import aprslib | ||||
| from aprslib.exceptions import LoginError | ||||
| from oslo_config import cfg | ||||
| import wrapt | ||||
| 
 | ||||
| from aprsd import exception | ||||
| from aprsd.clients import aprsis, fake, kiss | ||||
| from aprsd.packets import core, packet_list | ||||
| from aprsd.utils import trace | ||||
| from aprsd.utils import singleton, trace | ||||
| 
 | ||||
| 
 | ||||
| CONF = cfg.CONF | ||||
| @ -25,6 +28,34 @@ TRANSPORT_FAKE = "fake" | ||||
| factory = None | ||||
| 
 | ||||
| 
 | ||||
| @singleton | ||||
| class APRSClientStats: | ||||
| 
 | ||||
|     lock = threading.Lock() | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def stats(self, serializable=False): | ||||
|         client = factory.create() | ||||
|         stats = { | ||||
|             "transport": client.transport(), | ||||
|             "filter": client.filter, | ||||
|             "connected": client.connected, | ||||
|         } | ||||
| 
 | ||||
|         if client.transport() == TRANSPORT_APRSIS: | ||||
|             stats["server_string"] = client.client.server_string | ||||
|             keepalive = client.client.aprsd_keepalive | ||||
|             if serializable: | ||||
|                 keepalive = keepalive.isoformat() | ||||
|             stats["server_keepalive"] = keepalive | ||||
|         elif client.transport() == TRANSPORT_TCPKISS: | ||||
|             stats["host"] = CONF.kiss_tcp.host | ||||
|             stats["port"] = CONF.kiss_tcp.port | ||||
|         elif client.transport() == TRANSPORT_SERIALKISS: | ||||
|             stats["device"] = CONF.kiss_serial.device | ||||
|         return stats | ||||
| 
 | ||||
| 
 | ||||
| class Client: | ||||
|     """Singleton client class that constructs the aprslib connection.""" | ||||
| 
 | ||||
| @ -32,8 +63,8 @@ class Client: | ||||
|     _client = None | ||||
| 
 | ||||
|     connected = False | ||||
|     server_string = None | ||||
|     filter = None | ||||
|     lock = threading.Lock() | ||||
| 
 | ||||
|     def __new__(cls, *args, **kwargs): | ||||
|         """This magic turns this into a singleton.""" | ||||
| @ -43,6 +74,10 @@ class Client: | ||||
|             cls._instance._create_client() | ||||
|         return cls._instance | ||||
| 
 | ||||
|     @abc.abstractmethod | ||||
|     def stats(self) -> dict: | ||||
|         pass | ||||
| 
 | ||||
|     def set_filter(self, filter): | ||||
|         self.filter = filter | ||||
|         if self._client: | ||||
| @ -69,9 +104,12 @@ class Client: | ||||
|         packet_list.PacketList().tx(packet) | ||||
|         self.client.send(packet) | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def reset(self): | ||||
|         """Call this to force a rebuild/reconnect.""" | ||||
|         LOG.info("Resetting client connection.") | ||||
|         if self._client: | ||||
|             self._client.close() | ||||
|             del self._client | ||||
|             self._create_client() | ||||
|         else: | ||||
| @ -102,11 +140,34 @@ class Client: | ||||
|     def consumer(self, callback, blocking=False, immortal=False, raw=False): | ||||
|         pass | ||||
| 
 | ||||
|     @abc.abstractmethod | ||||
|     def is_alive(self): | ||||
|         pass | ||||
| 
 | ||||
|     @abc.abstractmethod | ||||
|     def close(self): | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
| class APRSISClient(Client): | ||||
| 
 | ||||
|     _client = None | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0} | ||||
|         self.max_delta = datetime.timedelta(**max_timeout) | ||||
| 
 | ||||
|     def stats(self) -> dict: | ||||
|         stats = {} | ||||
|         if self.is_configured(): | ||||
|             stats = { | ||||
|                 "server_string": self._client.server_string, | ||||
|                 "sever_keepalive": self._client.aprsd_keepalive, | ||||
|                 "filter": self.filter, | ||||
|             } | ||||
| 
 | ||||
|         return stats | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def is_enabled(): | ||||
|         # Defaults to True if the enabled flag is non existent | ||||
| @ -138,14 +199,24 @@ class APRSISClient(Client): | ||||
|             return True | ||||
|         return True | ||||
| 
 | ||||
|     def _is_stale_connection(self): | ||||
|         delta = datetime.datetime.now() - self._client.aprsd_keepalive | ||||
|         if delta > self.max_delta: | ||||
|             LOG.error(f"Connection is stale, last heard {delta} ago.") | ||||
|             return True | ||||
| 
 | ||||
|     def is_alive(self): | ||||
|         if self._client: | ||||
|             LOG.warning(f"APRS_CLIENT {self._client} alive? {self._client.is_alive()}") | ||||
|             return self._client.is_alive() | ||||
|             return self._client.is_alive() and not self._is_stale_connection() | ||||
|         else: | ||||
|             LOG.warning(f"APRS_CLIENT {self._client} alive? NO!!!") | ||||
|             return False | ||||
| 
 | ||||
|     def close(self): | ||||
|         if self._client: | ||||
|             self._client.stop() | ||||
|             self._client.close() | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def transport(): | ||||
|         return TRANSPORT_APRSIS | ||||
| @ -159,25 +230,25 @@ class APRSISClient(Client): | ||||
|         password = CONF.aprs_network.password | ||||
|         host = CONF.aprs_network.host | ||||
|         port = CONF.aprs_network.port | ||||
|         connected = False | ||||
|         self.connected = False | ||||
|         backoff = 1 | ||||
|         aprs_client = None | ||||
|         while not connected: | ||||
|         while not self.connected: | ||||
|             try: | ||||
|                 LOG.info(f"Creating aprslib client({host}:{port}) and logging in {user}.") | ||||
|                 aprs_client = aprsis.Aprsdis(user, passwd=password, host=host, port=port) | ||||
|                 # Force the log to be the same | ||||
|                 aprs_client.logger = LOG | ||||
|                 aprs_client.connect() | ||||
|                 connected = True | ||||
|                 self.connected = True | ||||
|                 backoff = 1 | ||||
|             except LoginError as e: | ||||
|                 LOG.error(f"Failed to login to APRS-IS Server '{e}'") | ||||
|                 connected = False | ||||
|                 self.connected = False | ||||
|                 time.sleep(backoff) | ||||
|             except Exception as e: | ||||
|                 LOG.error(f"Unable to connect to APRS-IS server. '{e}' ") | ||||
|                 connected = False | ||||
|                 self.connected = False | ||||
|                 time.sleep(backoff) | ||||
|                 # Don't allow the backoff to go to inifinity. | ||||
|                 if backoff > 5: | ||||
| @ -190,7 +261,6 @@ class APRSISClient(Client): | ||||
|         return aprs_client | ||||
| 
 | ||||
|     def consumer(self, callback, blocking=False, immortal=False, raw=False): | ||||
|         if self.is_alive(): | ||||
|         self._client.consumer( | ||||
|             callback, blocking=blocking, | ||||
|             immortal=immortal, raw=raw, | ||||
| @ -201,6 +271,14 @@ class KISSClient(Client): | ||||
| 
 | ||||
|     _client = None | ||||
| 
 | ||||
|     def stats(self) -> dict: | ||||
|         stats = {} | ||||
|         if self.is_configured(): | ||||
|             return { | ||||
|                 "transport": self.transport(), | ||||
|             } | ||||
|         return stats | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def is_enabled(): | ||||
|         """Return if tcp or serial KISS is enabled.""" | ||||
| @ -239,6 +317,10 @@ class KISSClient(Client): | ||||
|         else: | ||||
|             return False | ||||
| 
 | ||||
|     def close(self): | ||||
|         if self._client: | ||||
|             self._client.stop() | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def transport(): | ||||
|         if CONF.kiss_serial.enabled: | ||||
| @ -268,6 +350,7 @@ class KISSClient(Client): | ||||
| 
 | ||||
|     def setup_connection(self): | ||||
|         self._client = kiss.KISS3Client() | ||||
|         self.connected = True | ||||
|         return self._client | ||||
| 
 | ||||
|     def consumer(self, callback, blocking=False, immortal=False, raw=False): | ||||
| @ -276,6 +359,9 @@ class KISSClient(Client): | ||||
| 
 | ||||
| class APRSDFakeClient(Client, metaclass=trace.TraceWrapperMetaclass): | ||||
| 
 | ||||
|     def stats(self) -> dict: | ||||
|         return {} | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def is_enabled(): | ||||
|         if CONF.fake_client.enabled: | ||||
| @ -289,7 +375,11 @@ class APRSDFakeClient(Client, metaclass=trace.TraceWrapperMetaclass): | ||||
|     def is_alive(self): | ||||
|         return True | ||||
| 
 | ||||
|     def close(self): | ||||
|         pass | ||||
| 
 | ||||
|     def setup_connection(self): | ||||
|         self.connected = True | ||||
|         return fake.APRSDFakeClient() | ||||
| 
 | ||||
|     @staticmethod | ||||
| @ -329,7 +419,6 @@ class ClientFactory: | ||||
|                 key = TRANSPORT_FAKE | ||||
| 
 | ||||
|         builder = self._builders.get(key) | ||||
|         LOG.debug(f"ClientFactory Creating client of type '{key}'") | ||||
|         if not builder: | ||||
|             raise ValueError(key) | ||||
|         return builder() | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import datetime | ||||
| import logging | ||||
| import select | ||||
| import threading | ||||
| @ -11,7 +12,6 @@ from aprslib.exceptions import ( | ||||
| import wrapt | ||||
| 
 | ||||
| import aprsd | ||||
| from aprsd import stats | ||||
| from aprsd.packets import core | ||||
| 
 | ||||
| 
 | ||||
| @ -24,6 +24,9 @@ class Aprsdis(aprslib.IS): | ||||
|     # flag to tell us to stop | ||||
|     thread_stop = False | ||||
| 
 | ||||
|     # date for last time we heard from the server | ||||
|     aprsd_keepalive = datetime.datetime.now() | ||||
| 
 | ||||
|     # timeout in seconds | ||||
|     select_timeout = 1 | ||||
|     lock = threading.Lock() | ||||
| @ -142,7 +145,6 @@ class Aprsdis(aprslib.IS): | ||||
| 
 | ||||
|             self.logger.info(f"Connected to {server_string}") | ||||
|             self.server_string = server_string | ||||
|             stats.APRSDStats().set_aprsis_server(server_string) | ||||
| 
 | ||||
|         except LoginError as e: | ||||
|             self.logger.error(str(e)) | ||||
| @ -176,13 +178,14 @@ class Aprsdis(aprslib.IS): | ||||
|             try: | ||||
|                 for line in self._socket_readlines(blocking): | ||||
|                     if line[0:1] != b"#": | ||||
|                         self.aprsd_keepalive = datetime.datetime.now() | ||||
|                         if raw: | ||||
|                             callback(line) | ||||
|                         else: | ||||
|                             callback(self._parse(line)) | ||||
|                     else: | ||||
|                         self.logger.debug("Server: %s", line.decode("utf8")) | ||||
|                         stats.APRSDStats().set_aprsis_keepalive() | ||||
|                         self.aprsd_keepalive = datetime.datetime.now() | ||||
|             except ParseError as exp: | ||||
|                 self.logger.log( | ||||
|                     11, | ||||
|  | ||||
| @ -1,10 +1,9 @@ | ||||
| # Fetch active stats from a remote running instance of aprsd server | ||||
| # This uses the RPC server to fetch the stats from the remote server. | ||||
| 
 | ||||
| # Fetch active stats from a remote running instance of aprsd admin web interface. | ||||
| import logging | ||||
| 
 | ||||
| import click | ||||
| from oslo_config import cfg | ||||
| import requests | ||||
| from rich.console import Console | ||||
| from rich.table import Table | ||||
| 
 | ||||
| @ -12,7 +11,6 @@ from rich.table import Table | ||||
| import aprsd | ||||
| from aprsd import cli_helper | ||||
| from aprsd.main import cli | ||||
| from aprsd.rpc import client as rpc_client | ||||
| 
 | ||||
| 
 | ||||
| # setup the global logger | ||||
| @ -26,87 +24,80 @@ CONF = cfg.CONF | ||||
| @click.option( | ||||
|     "--host", type=str, | ||||
|     default=None, | ||||
|     help="IP address of the remote aprsd server to fetch stats from.", | ||||
|     help="IP address of the remote aprsd admin web ui fetch stats from.", | ||||
| ) | ||||
| @click.option( | ||||
|     "--port", type=int, | ||||
|     default=None, | ||||
|     help="Port of the remote aprsd server rpc port to fetch stats from.", | ||||
| ) | ||||
| @click.option( | ||||
|     "--magic-word", type=str, | ||||
|     default=None, | ||||
|     help="Magic word of the remote aprsd server rpc port to fetch stats from.", | ||||
|     help="Port of the remote aprsd web admin interface to fetch stats from.", | ||||
| ) | ||||
| @click.pass_context | ||||
| @cli_helper.process_standard_options | ||||
| def fetch_stats(ctx, host, port, magic_word): | ||||
|     """Fetch stats from a remote running instance of aprsd server.""" | ||||
|     LOG.info(f"APRSD Fetch-Stats started version: {aprsd.__version__}") | ||||
| def fetch_stats(ctx, host, port): | ||||
|     """Fetch stats from a APRSD admin web interface.""" | ||||
|     console = Console() | ||||
|     console.print(f"APRSD Fetch-Stats started version: {aprsd.__version__}") | ||||
| 
 | ||||
|     CONF.log_opt_values(LOG, logging.DEBUG) | ||||
|     if not host: | ||||
|         host = CONF.rpc_settings.ip | ||||
|         host = CONF.admin.web_ip | ||||
|     if not port: | ||||
|         port = CONF.rpc_settings.port | ||||
|     if not magic_word: | ||||
|         magic_word = CONF.rpc_settings.magic_word | ||||
|         port = CONF.admin.web_port | ||||
| 
 | ||||
|     msg = f"Fetching stats from {host}:{port} with magic word '{magic_word}'" | ||||
|     console = Console() | ||||
|     msg = f"Fetching stats from {host}:{port}" | ||||
|     console.print(msg) | ||||
|     with console.status(msg): | ||||
|         client = rpc_client.RPCClient(host, port, magic_word) | ||||
|         stats = client.get_stats_dict() | ||||
|         if stats: | ||||
|             console.print_json(data=stats) | ||||
|         else: | ||||
|             LOG.error(f"Failed to fetch stats via RPC aprsd server at {host}:{port}") | ||||
|         response = requests.get(f"http://{host}:{port}/stats", timeout=120) | ||||
|         if not response: | ||||
|             console.print( | ||||
|                 f"Failed to fetch stats from {host}:{port}?", | ||||
|                 style="bold red", | ||||
|             ) | ||||
|             return | ||||
| 
 | ||||
|         stats = response.json() | ||||
|         if not stats: | ||||
|             console.print( | ||||
|                 f"Failed to fetch stats from aprsd admin ui at {host}:{port}", | ||||
|                 style="bold red", | ||||
|             ) | ||||
|             return | ||||
| 
 | ||||
|     aprsd_title = ( | ||||
|         "APRSD " | ||||
|         f"[bold cyan]v{stats['aprsd']['version']}[/] " | ||||
|         f"Callsign [bold green]{stats['aprsd']['callsign']}[/] " | ||||
|         f"Uptime [bold yellow]{stats['aprsd']['uptime']}[/]" | ||||
|         f"[bold cyan]v{stats['APRSDStats']['version']}[/] " | ||||
|         f"Callsign [bold green]{stats['APRSDStats']['callsign']}[/] " | ||||
|         f"Uptime [bold yellow]{stats['APRSDStats']['uptime']}[/]" | ||||
|     ) | ||||
| 
 | ||||
|     console.rule(f"Stats from {host}:{port} with magic word '{magic_word}'") | ||||
|     console.rule(f"Stats from {host}:{port}") | ||||
|     console.print("\n\n") | ||||
|     console.rule(aprsd_title) | ||||
| 
 | ||||
|     # Show the connection to APRS | ||||
|     # It can be a connection to an APRS-IS server or a local TNC via KISS or KISSTCP | ||||
|     if "aprs-is" in stats: | ||||
|         title = f"APRS-IS Connection {stats['aprs-is']['server']}" | ||||
|         title = f"APRS-IS Connection {stats['APRSClientStats']['server_string']}" | ||||
|         table = Table(title=title) | ||||
|         table.add_column("Key") | ||||
|         table.add_column("Value") | ||||
|         for key, value in stats["aprs-is"].items(): | ||||
|         for key, value in stats["APRSClientStats"].items(): | ||||
|             table.add_row(key, value) | ||||
|         console.print(table) | ||||
| 
 | ||||
|     threads_table = Table(title="Threads") | ||||
|     threads_table.add_column("Name") | ||||
|     threads_table.add_column("Alive?") | ||||
|     for name, alive in stats["aprsd"]["threads"].items(): | ||||
|     for name, alive in stats["APRSDThreadList"].items(): | ||||
|         threads_table.add_row(name, str(alive)) | ||||
| 
 | ||||
|     console.print(threads_table) | ||||
| 
 | ||||
|     msgs_table = Table(title="Messages") | ||||
|     msgs_table.add_column("Key") | ||||
|     msgs_table.add_column("Value") | ||||
|     for key, value in stats["messages"].items(): | ||||
|         msgs_table.add_row(key, str(value)) | ||||
| 
 | ||||
|     console.print(msgs_table) | ||||
| 
 | ||||
|     packet_totals = Table(title="Packet Totals") | ||||
|     packet_totals.add_column("Key") | ||||
|     packet_totals.add_column("Value") | ||||
|     packet_totals.add_row("Total Received", str(stats["packets"]["total_received"])) | ||||
|     packet_totals.add_row("Total Sent", str(stats["packets"]["total_sent"])) | ||||
|     packet_totals.add_row("Total Tracked", str(stats["packets"]["total_tracked"])) | ||||
|     packet_totals.add_row("Total Received", str(stats["PacketList"]["rx"])) | ||||
|     packet_totals.add_row("Total Sent", str(stats["PacketList"]["tx"])) | ||||
|     console.print(packet_totals) | ||||
| 
 | ||||
|     # Show each of the packet types | ||||
| @ -114,47 +105,52 @@ def fetch_stats(ctx, host, port, magic_word): | ||||
|     packets_table.add_column("Packet Type") | ||||
|     packets_table.add_column("TX") | ||||
|     packets_table.add_column("RX") | ||||
|     for key, value in stats["packets"]["by_type"].items(): | ||||
|     for key, value in stats["PacketList"]["packets"].items(): | ||||
|         packets_table.add_row(key, str(value["tx"]), str(value["rx"])) | ||||
| 
 | ||||
|     console.print(packets_table) | ||||
| 
 | ||||
|     if "plugins" in stats: | ||||
|         count = len(stats["plugins"]) | ||||
|         count = len(stats["PluginManager"]) | ||||
|         plugins_table = Table(title=f"Plugins ({count})") | ||||
|         plugins_table.add_column("Plugin") | ||||
|         plugins_table.add_column("Enabled") | ||||
|         plugins_table.add_column("Version") | ||||
|         plugins_table.add_column("TX") | ||||
|         plugins_table.add_column("RX") | ||||
|         for key, value in stats["plugins"].items(): | ||||
|         plugins = stats["PluginManager"] | ||||
|         for key, value in plugins.items(): | ||||
|             plugins_table.add_row( | ||||
|                 key, | ||||
|                 str(stats["plugins"][key]["enabled"]), | ||||
|                 stats["plugins"][key]["version"], | ||||
|                 str(stats["plugins"][key]["tx"]), | ||||
|                 str(stats["plugins"][key]["rx"]), | ||||
|                 str(plugins[key]["enabled"]), | ||||
|                 plugins[key]["version"], | ||||
|                 str(plugins[key]["tx"]), | ||||
|                 str(plugins[key]["rx"]), | ||||
|             ) | ||||
| 
 | ||||
|         console.print(plugins_table) | ||||
| 
 | ||||
|     if "seen_list" in stats["aprsd"]: | ||||
|         count = len(stats["aprsd"]["seen_list"]) | ||||
|     seen_list = stats.get("SeenList") | ||||
| 
 | ||||
|     if seen_list: | ||||
|         count = len(seen_list) | ||||
|         seen_table = Table(title=f"Seen List ({count})") | ||||
|         seen_table.add_column("Callsign") | ||||
|         seen_table.add_column("Message Count") | ||||
|         seen_table.add_column("Last Heard") | ||||
|         for key, value in stats["aprsd"]["seen_list"].items(): | ||||
|         for key, value in seen_list.items(): | ||||
|             seen_table.add_row(key, str(value["count"]), value["last"]) | ||||
| 
 | ||||
|         console.print(seen_table) | ||||
| 
 | ||||
|     if "watch_list" in stats["aprsd"]: | ||||
|         count = len(stats["aprsd"]["watch_list"]) | ||||
|     watch_list = stats.get("WatchList") | ||||
| 
 | ||||
|     if watch_list: | ||||
|         count = len(watch_list) | ||||
|         watch_table = Table(title=f"Watch List ({count})") | ||||
|         watch_table.add_column("Callsign") | ||||
|         watch_table.add_column("Last Heard") | ||||
|         for key, value in stats["aprsd"]["watch_list"].items(): | ||||
|         for key, value in watch_list.items(): | ||||
|             watch_table.add_row(key, value["last"]) | ||||
| 
 | ||||
|         console.print(watch_table) | ||||
|  | ||||
| @ -13,11 +13,11 @@ from oslo_config import cfg | ||||
| from rich.console import Console | ||||
| 
 | ||||
| import aprsd | ||||
| from aprsd import cli_helper, utils | ||||
| from aprsd import cli_helper | ||||
| from aprsd import conf  # noqa | ||||
| # local imports here | ||||
| from aprsd.main import cli | ||||
| from aprsd.rpc import client as aprsd_rpc_client | ||||
| from aprsd.threads import stats as stats_threads | ||||
| 
 | ||||
| 
 | ||||
| # setup the global logger | ||||
| @ -39,46 +39,48 @@ console = Console() | ||||
| @cli_helper.process_standard_options | ||||
| def healthcheck(ctx, timeout): | ||||
|     """Check the health of the running aprsd server.""" | ||||
|     console.log(f"APRSD HealthCheck version: {aprsd.__version__}") | ||||
|     if not CONF.rpc_settings.enabled: | ||||
|         LOG.error("Must enable rpc_settings.enabled to use healthcheck") | ||||
|         sys.exit(-1) | ||||
|     if not CONF.rpc_settings.ip: | ||||
|         LOG.error("Must enable rpc_settings.ip to use healthcheck") | ||||
|         sys.exit(-1) | ||||
|     if not CONF.rpc_settings.magic_word: | ||||
|         LOG.error("Must enable rpc_settings.magic_word to use healthcheck") | ||||
|         sys.exit(-1) | ||||
|     ver_str = f"APRSD HealthCheck version: {aprsd.__version__}" | ||||
|     console.log(ver_str) | ||||
| 
 | ||||
|     with console.status(f"APRSD HealthCheck version: {aprsd.__version__}") as status: | ||||
|     with console.status(ver_str): | ||||
|         try: | ||||
|             status.update(f"Contacting APRSD via RPC {CONF.rpc_settings.ip}") | ||||
|             stats = aprsd_rpc_client.RPCClient().get_stats_dict() | ||||
|             stats_obj = stats_threads.StatsStore() | ||||
|             stats_obj.load() | ||||
|             stats = stats_obj.data | ||||
|             # console.print(stats) | ||||
|         except Exception as ex: | ||||
|             console.log(f"Failed to fetch healthcheck : '{ex}'") | ||||
|             console.log(f"Failed to load stats: '{ex}'") | ||||
|             sys.exit(-1) | ||||
|         else: | ||||
|             now = datetime.datetime.now() | ||||
|             if not stats: | ||||
|                 console.log("No stats from aprsd") | ||||
|                 sys.exit(-1) | ||||
|             email_thread_last_update = stats["email"]["thread_last_update"] | ||||
| 
 | ||||
|             email_stats = stats.get("EmailStats") | ||||
|             if email_stats: | ||||
|                 email_thread_last_update = email_stats["last_check_time"] | ||||
| 
 | ||||
|                 if email_thread_last_update != "never": | ||||
|                 delta = utils.parse_delta_str(email_thread_last_update) | ||||
|                 d = datetime.timedelta(**delta) | ||||
|                     d = now - email_thread_last_update | ||||
|                     max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0} | ||||
|                     max_delta = datetime.timedelta(**max_timeout) | ||||
|                     if d > max_delta: | ||||
|                         console.log(f"Email thread is very old! {d}") | ||||
|                         sys.exit(-1) | ||||
| 
 | ||||
|             aprsis_last_update = stats["aprs-is"]["last_update"] | ||||
|             delta = utils.parse_delta_str(aprsis_last_update) | ||||
|             d = datetime.timedelta(**delta) | ||||
|             client_stats = stats.get("APRSClientStats") | ||||
|             if not client_stats: | ||||
|                 console.log("No APRSClientStats") | ||||
|                 sys.exit(-1) | ||||
|             else: | ||||
|                 aprsis_last_update = client_stats["server_keepalive"] | ||||
|                 d = now - aprsis_last_update | ||||
|                 max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0} | ||||
|                 max_delta = datetime.timedelta(**max_timeout) | ||||
|                 if d > max_delta: | ||||
|                     LOG.error(f"APRS-IS last update is very old! {d}") | ||||
|                     sys.exit(-1) | ||||
| 
 | ||||
|             console.log("OK") | ||||
|             sys.exit(0) | ||||
|  | ||||
| @ -21,7 +21,7 @@ from aprsd import cli_helper | ||||
| from aprsd import plugin as aprsd_plugin | ||||
| from aprsd.main import cli | ||||
| from aprsd.plugins import ( | ||||
|     email, fortune, location, notify, ping, query, time, version, weather, | ||||
|     email, fortune, location, notify, ping, time, version, weather, | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| @ -122,7 +122,7 @@ def get_installed_extensions(): | ||||
| 
 | ||||
| 
 | ||||
| def show_built_in_plugins(console): | ||||
|     modules = [email, fortune, location, notify, ping, query, time, version, weather] | ||||
|     modules = [email, fortune, location, notify, ping, time, version, weather] | ||||
|     plugins = [] | ||||
| 
 | ||||
|     for module in modules: | ||||
|  | ||||
| @ -15,10 +15,10 @@ from rich.console import Console | ||||
| 
 | ||||
| # local imports here | ||||
| import aprsd | ||||
| from aprsd import cli_helper, client, packets, plugin, stats, threads | ||||
| from aprsd import cli_helper, client, packets, plugin, threads | ||||
| from aprsd.main import cli | ||||
| from aprsd.packets import log as packet_log | ||||
| from aprsd.rpc import server as rpc_server | ||||
| from aprsd.stats import collector | ||||
| from aprsd.threads import rx | ||||
| 
 | ||||
| 
 | ||||
| @ -38,7 +38,7 @@ def signal_handler(sig, frame): | ||||
|             ), | ||||
|         ) | ||||
|         time.sleep(5) | ||||
|         LOG.info(stats.APRSDStats()) | ||||
|         LOG.info(collector.Collector().collect()) | ||||
| 
 | ||||
| 
 | ||||
| class APRSDListenThread(rx.APRSDRXThread): | ||||
| @ -169,6 +169,7 @@ def listen( | ||||
|     LOG.info(f"APRSD Listen Started version: {aprsd.__version__}") | ||||
| 
 | ||||
|     CONF.log_opt_values(LOG, logging.DEBUG) | ||||
|     collector.Collector() | ||||
| 
 | ||||
|     # Try and load saved MsgTrack list | ||||
|     LOG.debug("Loading saved MsgTrack object.") | ||||
| @ -192,10 +193,6 @@ def listen( | ||||
|     keepalive = threads.KeepAliveThread() | ||||
|     # keepalive.start() | ||||
| 
 | ||||
|     if CONF.rpc_settings.enabled: | ||||
|         rpc = rpc_server.APRSDRPCThread() | ||||
|         rpc.start() | ||||
| 
 | ||||
|     pm = None | ||||
|     pm = plugin.PluginManager() | ||||
|     if load_plugins: | ||||
| @ -206,6 +203,8 @@ def listen( | ||||
|             "Not Loading any plugins use --load-plugins to load what's " | ||||
|             "defined in the config file.", | ||||
|         ) | ||||
|     stats_thread = threads.APRSDStatsStoreThread() | ||||
|     stats_thread.start() | ||||
| 
 | ||||
|     LOG.debug("Create APRSDListenThread") | ||||
|     listen_thread = APRSDListenThread( | ||||
| @ -221,6 +220,4 @@ def listen( | ||||
|     keepalive.join() | ||||
|     LOG.debug("listen_thread Join") | ||||
|     listen_thread.join() | ||||
| 
 | ||||
|     if CONF.rpc_settings.enabled: | ||||
|         rpc.join() | ||||
|     stats_thread.join() | ||||
|  | ||||
| @ -76,7 +76,6 @@ def send_message( | ||||
|             aprs_login = CONF.aprs_network.login | ||||
| 
 | ||||
|     if not aprs_password: | ||||
|         LOG.warning(CONF.aprs_network.password) | ||||
|         if not CONF.aprs_network.password: | ||||
|             click.echo("Must set --aprs-password or APRS_PASSWORD") | ||||
|             ctx.exit(-1) | ||||
|  | ||||
| @ -10,8 +10,9 @@ from aprsd import cli_helper, client | ||||
| from aprsd import main as aprsd_main | ||||
| from aprsd import packets, plugin, threads, utils | ||||
| from aprsd.main import cli | ||||
| from aprsd.rpc import server as rpc_server | ||||
| from aprsd.threads import registry, rx, tx | ||||
| from aprsd.threads import keep_alive, log_monitor, registry, rx | ||||
| from aprsd.threads import stats as stats_thread | ||||
| from aprsd.threads import tx | ||||
| 
 | ||||
| 
 | ||||
| CONF = cfg.CONF | ||||
| @ -47,6 +48,14 @@ def server(ctx, flush): | ||||
|     # Initialize the client factory and create | ||||
|     # The correct client object ready for use | ||||
|     client.ClientFactory.setup() | ||||
|     if not client.factory.is_client_enabled(): | ||||
|         LOG.error("No Clients are enabled in config.") | ||||
|         sys.exit(-1) | ||||
| 
 | ||||
|     # Creates the client object | ||||
|     LOG.info("Creating client connection") | ||||
|     aprs_client = client.factory.create() | ||||
|     LOG.info(aprs_client) | ||||
| 
 | ||||
|     # Create the initial PM singleton and Register plugins | ||||
|     # We register plugins first here so we can register each | ||||
| @ -87,16 +96,21 @@ def server(ctx, flush): | ||||
|         packets.PacketTrack().flush() | ||||
|         packets.WatchList().flush() | ||||
|         packets.SeenList().flush() | ||||
|         packets.PacketList().flush() | ||||
|     else: | ||||
|         # Try and load saved MsgTrack list | ||||
|         LOG.debug("Loading saved MsgTrack object.") | ||||
|         packets.PacketTrack().load() | ||||
|         packets.WatchList().load() | ||||
|         packets.SeenList().load() | ||||
|         packets.PacketList().load() | ||||
| 
 | ||||
|     keepalive = threads.KeepAliveThread() | ||||
|     keepalive = keep_alive.KeepAliveThread() | ||||
|     keepalive.start() | ||||
| 
 | ||||
|     stats_store_thread = stats_thread.APRSDStatsStoreThread() | ||||
|     stats_store_thread.start() | ||||
| 
 | ||||
|     rx_thread = rx.APRSDPluginRXThread( | ||||
|         packet_queue=threads.packet_queue, | ||||
|     ) | ||||
| @ -106,7 +120,6 @@ def server(ctx, flush): | ||||
|     rx_thread.start() | ||||
|     process_thread.start() | ||||
| 
 | ||||
|     packets.PacketTrack().restart() | ||||
|     if CONF.enable_beacon: | ||||
|         LOG.info("Beacon Enabled.  Starting Beacon thread.") | ||||
|         bcn_thread = tx.BeaconSendThread() | ||||
| @ -117,11 +130,9 @@ def server(ctx, flush): | ||||
|         registry_thread = registry.APRSRegistryThread() | ||||
|         registry_thread.start() | ||||
| 
 | ||||
|     if CONF.rpc_settings.enabled: | ||||
|         rpc = rpc_server.APRSDRPCThread() | ||||
|         rpc.start() | ||||
|         log_monitor = threads.log_monitor.LogMonitorThread() | ||||
|         log_monitor.start() | ||||
|     if CONF.admin.web_enabled: | ||||
|         log_monitor_thread = log_monitor.LogMonitorThread() | ||||
|         log_monitor_thread.start() | ||||
| 
 | ||||
|     rx_thread.join() | ||||
|     process_thread.join() | ||||
|  | ||||
| @ -23,7 +23,7 @@ from aprsd import ( | ||||
| ) | ||||
| from aprsd.main import cli | ||||
| from aprsd.threads import aprsd as aprsd_threads | ||||
| from aprsd.threads import rx, tx | ||||
| from aprsd.threads import keep_alive, rx, tx | ||||
| from aprsd.utils import trace | ||||
| 
 | ||||
| 
 | ||||
| @ -63,7 +63,7 @@ def signal_handler(sig, frame): | ||||
|         time.sleep(1.5) | ||||
|         # packets.WatchList().save() | ||||
|         # packets.SeenList().save() | ||||
|         LOG.info(stats.APRSDStats()) | ||||
|         LOG.info(stats.stats_collector.collect()) | ||||
|         LOG.info("Telling flask to bail.") | ||||
|         signal.signal(signal.SIGTERM, sys.exit(0)) | ||||
| 
 | ||||
| @ -378,7 +378,7 @@ def _get_transport(stats): | ||||
|         transport = "aprs-is" | ||||
|         aprs_connection = ( | ||||
|             "APRS-IS Server: <a href='http://status.aprs2.net' >" | ||||
|             "{}</a>".format(stats["stats"]["aprs-is"]["server"]) | ||||
|             "{}</a>".format(stats["APRSClientStats"]["server_string"]) | ||||
|         ) | ||||
|     elif client.KISSClient.is_enabled(): | ||||
|         transport = client.KISSClient.transport() | ||||
| @ -414,12 +414,13 @@ def location(callsign): | ||||
| @flask_app.route("/") | ||||
| def index(): | ||||
|     stats = _stats() | ||||
|     LOG.error(stats) | ||||
| 
 | ||||
|     # For development | ||||
|     html_template = "index.html" | ||||
|     LOG.debug(f"Template {html_template}") | ||||
| 
 | ||||
|     transport, aprs_connection = _get_transport(stats) | ||||
|     transport, aprs_connection = _get_transport(stats["stats"]) | ||||
|     LOG.debug(f"transport {transport} aprs_connection {aprs_connection}") | ||||
| 
 | ||||
|     stats["transport"] = transport | ||||
| @ -454,27 +455,28 @@ def send_message_status(): | ||||
| 
 | ||||
| 
 | ||||
| def _stats(): | ||||
|     stats_obj = stats.APRSDStats() | ||||
|     now = datetime.datetime.now() | ||||
| 
 | ||||
|     time_format = "%m-%d-%Y %H:%M:%S" | ||||
|     stats_dict = stats_obj.stats() | ||||
|     stats_dict = stats.stats_collector.collect(serializable=True) | ||||
|     # Webchat doesnt need these | ||||
|     if "watch_list" in stats_dict["aprsd"]: | ||||
|         del stats_dict["aprsd"]["watch_list"] | ||||
|     if "seen_list" in stats_dict["aprsd"]: | ||||
|         del stats_dict["aprsd"]["seen_list"] | ||||
|     if "threads" in stats_dict["aprsd"]: | ||||
|         del stats_dict["aprsd"]["threads"] | ||||
|     # del stats_dict["email"] | ||||
|     # del stats_dict["plugins"] | ||||
|     # del stats_dict["messages"] | ||||
|     if "WatchList" in stats_dict: | ||||
|         del stats_dict["WatchList"] | ||||
|     if "SeenList" in stats_dict: | ||||
|         del stats_dict["SeenList"] | ||||
|     if "APRSDThreadList" in stats_dict: | ||||
|         del stats_dict["APRSDThreadList"] | ||||
|     if "PacketList" in stats_dict: | ||||
|         del stats_dict["PacketList"] | ||||
|     if "EmailStats" in stats_dict: | ||||
|         del stats_dict["EmailStats"] | ||||
|     if "PluginManager" in stats_dict: | ||||
|         del stats_dict["PluginManager"] | ||||
| 
 | ||||
|     result = { | ||||
|         "time": now.strftime(time_format), | ||||
|         "stats": stats_dict, | ||||
|     } | ||||
| 
 | ||||
|     return result | ||||
| 
 | ||||
| 
 | ||||
| @ -544,7 +546,7 @@ class SendMessageNamespace(Namespace): | ||||
|         LOG.debug(f"Long {long}") | ||||
| 
 | ||||
|         tx.send( | ||||
|             packets.GPSPacket( | ||||
|             packets.BeaconPacket( | ||||
|                 from_call=CONF.callsign, | ||||
|                 to_call="APDW16", | ||||
|                 latitude=lat, | ||||
| @ -642,7 +644,7 @@ def webchat(ctx, flush, port): | ||||
|     packets.WatchList() | ||||
|     packets.SeenList() | ||||
| 
 | ||||
|     keepalive = threads.KeepAliveThread() | ||||
|     keepalive = keep_alive.KeepAliveThread() | ||||
|     LOG.info("Start KeepAliveThread") | ||||
|     keepalive.start() | ||||
| 
 | ||||
|  | ||||
| @ -15,10 +15,6 @@ watch_list_group = cfg.OptGroup( | ||||
|     name="watch_list", | ||||
|     title="Watch List settings", | ||||
| ) | ||||
| rpc_group = cfg.OptGroup( | ||||
|     name="rpc_settings", | ||||
|     title="RPC Settings for admin <--> web", | ||||
| ) | ||||
| webchat_group = cfg.OptGroup( | ||||
|     name="webchat", | ||||
|     title="Settings specific to the webchat command", | ||||
| @ -146,7 +142,7 @@ admin_opts = [ | ||||
|         default=False, | ||||
|         help="Enable the Admin Web Interface", | ||||
|     ), | ||||
|     cfg.IPOpt( | ||||
|     cfg.StrOpt( | ||||
|         "web_ip", | ||||
|         default="0.0.0.0", | ||||
|         help="The ip address to listen on", | ||||
| @ -169,28 +165,6 @@ admin_opts = [ | ||||
|     ), | ||||
| ] | ||||
| 
 | ||||
| rpc_opts = [ | ||||
|     cfg.BoolOpt( | ||||
|         "enabled", | ||||
|         default=True, | ||||
|         help="Enable RPC calls", | ||||
|     ), | ||||
|     cfg.StrOpt( | ||||
|         "ip", | ||||
|         default="localhost", | ||||
|         help="The ip address to listen on", | ||||
|     ), | ||||
|     cfg.PortOpt( | ||||
|         "port", | ||||
|         default=18861, | ||||
|         help="The port to listen on", | ||||
|     ), | ||||
|     cfg.StrOpt( | ||||
|         "magic_word", | ||||
|         default=APRSD_DEFAULT_MAGIC_WORD, | ||||
|         help="Magic word to authenticate requests between client/server", | ||||
|     ), | ||||
| ] | ||||
| 
 | ||||
| enabled_plugins_opts = [ | ||||
|     cfg.ListOpt( | ||||
| @ -213,7 +187,7 @@ enabled_plugins_opts = [ | ||||
| ] | ||||
| 
 | ||||
| webchat_opts = [ | ||||
|     cfg.IPOpt( | ||||
|     cfg.StrOpt( | ||||
|         "web_ip", | ||||
|         default="0.0.0.0", | ||||
|         help="The ip address to listen on", | ||||
| @ -281,8 +255,6 @@ def register_opts(config): | ||||
|     config.register_opts(admin_opts, group=admin_group) | ||||
|     config.register_group(watch_list_group) | ||||
|     config.register_opts(watch_list_opts, group=watch_list_group) | ||||
|     config.register_group(rpc_group) | ||||
|     config.register_opts(rpc_opts, group=rpc_group) | ||||
|     config.register_group(webchat_group) | ||||
|     config.register_opts(webchat_opts, group=webchat_group) | ||||
|     config.register_group(registry_group) | ||||
| @ -294,7 +266,6 @@ def list_opts(): | ||||
|         "DEFAULT": (aprsd_opts + enabled_plugins_opts), | ||||
|         admin_group.name: admin_opts, | ||||
|         watch_list_group.name: watch_list_opts, | ||||
|         rpc_group.name: rpc_opts, | ||||
|         webchat_group.name: webchat_opts, | ||||
|         registry_group.name: registry_opts, | ||||
|     } | ||||
|  | ||||
| @ -36,7 +36,6 @@ class InterceptHandler(logging.Handler): | ||||
| # to disable log to stdout, but still log to file | ||||
| # use the --quiet option on the cmdln | ||||
| def setup_logging(loglevel=None, quiet=False): | ||||
|     print(f"setup_logging: loglevel={loglevel}, quiet={quiet}") | ||||
|     if not loglevel: | ||||
|         log_level = CONF.logging.log_level | ||||
|     else: | ||||
| @ -58,6 +57,8 @@ def setup_logging(loglevel=None, quiet=False): | ||||
|     webserver_list = [ | ||||
|         "werkzeug", | ||||
|         "werkzeug._internal", | ||||
|         "socketio", | ||||
|         "urllib3.connectionpool", | ||||
|     ] | ||||
| 
 | ||||
|     # We don't really want to see the aprslib parsing debug output. | ||||
|  | ||||
| @ -35,7 +35,8 @@ from oslo_config import cfg, generator | ||||
| 
 | ||||
| # local imports here | ||||
| import aprsd | ||||
| from aprsd import cli_helper, packets, stats, threads, utils | ||||
| from aprsd import cli_helper, packets, threads, utils | ||||
| from aprsd.stats import collector | ||||
| 
 | ||||
| 
 | ||||
| # setup the global logger | ||||
| @ -44,7 +45,6 @@ CONF = cfg.CONF | ||||
| LOG = logging.getLogger("APRSD") | ||||
| CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) | ||||
| flask_enabled = False | ||||
| rpc_serv = None | ||||
| 
 | ||||
| 
 | ||||
| def custom_startswith(string, incomplete): | ||||
| @ -96,7 +96,8 @@ def signal_handler(sig, frame): | ||||
|         packets.PacketTrack().save() | ||||
|         packets.WatchList().save() | ||||
|         packets.SeenList().save() | ||||
|         LOG.info(stats.APRSDStats()) | ||||
|         packets.PacketList().save() | ||||
|         LOG.info(collector.Collector().collect()) | ||||
|         # signal.signal(signal.SIGTERM, sys.exit(0)) | ||||
|         # sys.exit(0) | ||||
| 
 | ||||
|  | ||||
| @ -109,14 +109,6 @@ class Packet: | ||||
|     path: List[str] = field(default_factory=list, compare=False, hash=False) | ||||
|     via: Optional[str] = field(default=None, compare=False, hash=False) | ||||
| 
 | ||||
|     @property | ||||
|     def json(self): | ||||
|         """get the json formated string. | ||||
| 
 | ||||
|         This is used soley by the rpc server to return json over the wire. | ||||
|         """ | ||||
|         return self.to_json() | ||||
| 
 | ||||
|     def get(self, key: str, default: Optional[str] = None): | ||||
|         """Emulate a getter on a dict.""" | ||||
|         if hasattr(self, key): | ||||
| @ -218,6 +210,11 @@ class BulletinPacket(Packet): | ||||
|     bid: Optional[str] = field(default="1") | ||||
|     message_text: Optional[str] = field(default=None) | ||||
| 
 | ||||
|     @property | ||||
|     def key(self) -> str: | ||||
|         """Build a key for finding this packet in a dict.""" | ||||
|         return f"{self.from_call}:BLN{self.bid}" | ||||
| 
 | ||||
|     @property | ||||
|     def human_info(self) -> str: | ||||
|         return f"BLN{self.bid} {self.message_text}" | ||||
| @ -385,6 +382,14 @@ class BeaconPacket(GPSPacket): | ||||
|             f"{self.payload}" | ||||
|         ) | ||||
| 
 | ||||
|     @property | ||||
|     def key(self) -> str: | ||||
|         """Build a key for finding this packet in a dict.""" | ||||
|         if self.raw_timestamp: | ||||
|             return f"{self.from_call}:{self.raw_timestamp}" | ||||
|         else: | ||||
|             return f"{self.from_call}:{self.human_info.replace(' ','')}" | ||||
| 
 | ||||
|     @property | ||||
|     def human_info(self) -> str: | ||||
|         h_str = [] | ||||
| @ -407,6 +412,11 @@ class MicEPacket(GPSPacket): | ||||
|     # 0 to 360 | ||||
|     course: int = 0 | ||||
| 
 | ||||
|     @property | ||||
|     def key(self) -> str: | ||||
|         """Build a key for finding this packet in a dict.""" | ||||
|         return f"{self.from_call}:{self.human_info.replace(' ', '')}" | ||||
| 
 | ||||
|     @property | ||||
|     def human_info(self) -> str: | ||||
|         h_info = super().human_info | ||||
| @ -428,6 +438,14 @@ class TelemetryPacket(GPSPacket): | ||||
|     # 0 to 360 | ||||
|     course: int = 0 | ||||
| 
 | ||||
|     @property | ||||
|     def key(self) -> str: | ||||
|         """Build a key for finding this packet in a dict.""" | ||||
|         if self.raw_timestamp: | ||||
|             return f"{self.from_call}:{self.raw_timestamp}" | ||||
|         else: | ||||
|             return f"{self.from_call}:{self.human_info.replace(' ','')}" | ||||
| 
 | ||||
|     @property | ||||
|     def human_info(self) -> str: | ||||
|         h_info = super().human_info | ||||
| @ -548,6 +566,14 @@ class WeatherPacket(GPSPacket, DataClassJsonMixin): | ||||
|         raw = cls._translate(cls, kvs)  # type: ignore | ||||
|         return super().from_dict(raw) | ||||
| 
 | ||||
|     @property | ||||
|     def key(self) -> str: | ||||
|         """Build a key for finding this packet in a dict.""" | ||||
|         if self.raw_timestamp: | ||||
|             return f"{self.from_call}:{self.raw_timestamp}" | ||||
|         elif self.wx_raw_timestamp: | ||||
|             return f"{self.from_call}:{self.wx_raw_timestamp}" | ||||
| 
 | ||||
|     @property | ||||
|     def human_info(self) -> str: | ||||
|         h_str = [] | ||||
| @ -643,6 +669,11 @@ class ThirdPartyPacket(Packet, DataClassJsonMixin): | ||||
|         obj.subpacket = factory(obj.subpacket)  # type: ignore | ||||
|         return obj | ||||
| 
 | ||||
|     @property | ||||
|     def key(self) -> str: | ||||
|         """Build a key for finding this packet in a dict.""" | ||||
|         return f"{self.from_call}:{self.subpacket.key}" | ||||
| 
 | ||||
|     @property | ||||
|     def human_info(self) -> str: | ||||
|         sub_info = self.subpacket.human_info | ||||
| @ -772,8 +803,7 @@ def factory(raw_packet: dict[Any, Any]) -> type[Packet]: | ||||
|         if "latitude" in raw: | ||||
|             packet_class = GPSPacket | ||||
|         else: | ||||
|             LOG.warning(f"Unknown packet type {packet_type}") | ||||
|             LOG.warning(raw) | ||||
|             # LOG.warning(raw) | ||||
|             packet_class = UnknownPacket | ||||
| 
 | ||||
|     raw.get("addresse", raw.get("to_call")) | ||||
|  | ||||
| @ -6,26 +6,28 @@ import threading | ||||
| from oslo_config import cfg | ||||
| import wrapt | ||||
| 
 | ||||
| from aprsd import stats | ||||
| from aprsd.packets import seen_list | ||||
| from aprsd.utils import objectstore | ||||
| 
 | ||||
| 
 | ||||
| CONF = cfg.CONF | ||||
| LOG = logging.getLogger("APRSD") | ||||
| 
 | ||||
| 
 | ||||
| class PacketList(MutableMapping): | ||||
| class PacketList(MutableMapping, objectstore.ObjectStoreMixin): | ||||
|     _instance = None | ||||
|     lock = threading.Lock() | ||||
|     _total_rx: int = 0 | ||||
|     _total_tx: int = 0 | ||||
|     types = {} | ||||
| 
 | ||||
|     def __new__(cls, *args, **kwargs): | ||||
|         if cls._instance is None: | ||||
|             cls._instance = super().__new__(cls) | ||||
|             cls._maxlen = 100 | ||||
|             cls.d = OrderedDict() | ||||
|             cls.data = { | ||||
|                 "types": {}, | ||||
|                 "packets": OrderedDict(), | ||||
|             } | ||||
|         return cls._instance | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
| @ -34,11 +36,10 @@ class PacketList(MutableMapping): | ||||
|         self._total_rx += 1 | ||||
|         self._add(packet) | ||||
|         ptype = packet.__class__.__name__ | ||||
|         if not ptype in self.types: | ||||
|             self.types[ptype] = {"tx": 0, "rx": 0} | ||||
|         self.types[ptype]["rx"] += 1 | ||||
|         if not ptype in self.data["types"]: | ||||
|             self.data["types"][ptype] = {"tx": 0, "rx": 0} | ||||
|         self.data["types"][ptype]["rx"] += 1 | ||||
|         seen_list.SeenList().update_seen(packet) | ||||
|         stats.APRSDStats().rx(packet) | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def tx(self, packet): | ||||
| @ -46,18 +47,17 @@ class PacketList(MutableMapping): | ||||
|         self._total_tx += 1 | ||||
|         self._add(packet) | ||||
|         ptype = packet.__class__.__name__ | ||||
|         if not ptype in self.types: | ||||
|             self.types[ptype] = {"tx": 0, "rx": 0} | ||||
|         self.types[ptype]["tx"] += 1 | ||||
|         if not ptype in self.data["types"]: | ||||
|             self.data["types"][ptype] = {"tx": 0, "rx": 0} | ||||
|         self.data["types"][ptype]["tx"] += 1 | ||||
|         seen_list.SeenList().update_seen(packet) | ||||
|         stats.APRSDStats().tx(packet) | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def add(self, packet): | ||||
|         self._add(packet) | ||||
| 
 | ||||
|     def _add(self, packet): | ||||
|         self[packet.key] = packet | ||||
|         self.data["packets"][packet.key] = packet | ||||
| 
 | ||||
|     def copy(self): | ||||
|         return self.d.copy() | ||||
| @ -72,23 +72,23 @@ class PacketList(MutableMapping): | ||||
| 
 | ||||
|     def __getitem__(self, key): | ||||
|         # self.d.move_to_end(key) | ||||
|         return self.d[key] | ||||
|         return self.data["packets"][key] | ||||
| 
 | ||||
|     def __setitem__(self, key, value): | ||||
|         if key in self.d: | ||||
|             self.d.move_to_end(key) | ||||
|         elif len(self.d) == self.maxlen: | ||||
|             self.d.popitem(last=False) | ||||
|         self.d[key] = value | ||||
|         if key in self.data["packets"]: | ||||
|             self.data["packets"].move_to_end(key) | ||||
|         elif len(self.data["packets"]) == self.maxlen: | ||||
|             self.data["packets"].popitem(last=False) | ||||
|         self.data["packets"][key] = value | ||||
| 
 | ||||
|     def __delitem__(self, key): | ||||
|         del self.d[key] | ||||
|         del self.data["packets"][key] | ||||
| 
 | ||||
|     def __iter__(self): | ||||
|         return self.d.__iter__() | ||||
|         return self.data["packets"].__iter__() | ||||
| 
 | ||||
|     def __len__(self): | ||||
|         return len(self.d) | ||||
|         return len(self.data["packets"]) | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def total_rx(self): | ||||
| @ -97,3 +97,14 @@ class PacketList(MutableMapping): | ||||
|     @wrapt.synchronized(lock) | ||||
|     def total_tx(self): | ||||
|         return self._total_tx | ||||
| 
 | ||||
|     def stats(self, serializable=False) -> dict: | ||||
|         stats = { | ||||
|             "total_tracked": self.total_tx() + self.total_rx(), | ||||
|             "rx": self.total_rx(), | ||||
|             "tx": self.total_tx(), | ||||
|             "types": self.data["types"], | ||||
|             "packets": self.data["packets"], | ||||
|         } | ||||
| 
 | ||||
|         return stats | ||||
|  | ||||
| @ -26,6 +26,10 @@ class SeenList(objectstore.ObjectStoreMixin): | ||||
|             cls._instance.data = {} | ||||
|         return cls._instance | ||||
| 
 | ||||
|     def stats(self, serializable=False): | ||||
|         """Return the stats for the PacketTrack class.""" | ||||
|         return self.data | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def update_seen(self, packet): | ||||
|         callsign = None | ||||
| @ -39,5 +43,5 @@ class SeenList(objectstore.ObjectStoreMixin): | ||||
|                 "last": None, | ||||
|                 "count": 0, | ||||
|             } | ||||
|         self.data[callsign]["last"] = str(datetime.datetime.now()) | ||||
|         self.data[callsign]["last"] = datetime.datetime.now() | ||||
|         self.data[callsign]["count"] += 1 | ||||
|  | ||||
| @ -4,7 +4,6 @@ import threading | ||||
| from oslo_config import cfg | ||||
| import wrapt | ||||
| 
 | ||||
| from aprsd.threads import tx | ||||
| from aprsd.utils import objectstore | ||||
| 
 | ||||
| 
 | ||||
| @ -58,6 +57,24 @@ class PacketTrack(objectstore.ObjectStoreMixin): | ||||
|     def values(self): | ||||
|         return self.data.values() | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def stats(self, serializable=False): | ||||
|         stats = { | ||||
|             "total_tracked": self.total_tracked, | ||||
|         } | ||||
|         pkts = {} | ||||
|         for key in self.data: | ||||
|             last_send_time = self.data[key].last_send_time | ||||
|             last_send_attempt = self.data[key]._last_send_attempt | ||||
|             pkts[key] = { | ||||
|                 "last_send_time": last_send_time, | ||||
|                 "last_send_attempt": last_send_attempt, | ||||
|                 "retry_count": self.data[key].retry_count, | ||||
|                 "message": self.data[key].raw, | ||||
|             } | ||||
|         stats["packets"] = pkts | ||||
|         return stats | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def __len__(self): | ||||
|         return len(self.data) | ||||
| @ -79,33 +96,3 @@ class PacketTrack(objectstore.ObjectStoreMixin): | ||||
|             del self.data[key] | ||||
|         except KeyError: | ||||
|             pass | ||||
| 
 | ||||
|     def restart(self): | ||||
|         """Walk the list of messages and restart them if any.""" | ||||
|         for key in self.data.keys(): | ||||
|             pkt = self.data[key] | ||||
|             if pkt._last_send_attempt < pkt.retry_count: | ||||
|                 tx.send(pkt) | ||||
| 
 | ||||
|     def _resend(self, packet): | ||||
|         packet._last_send_attempt = 0 | ||||
|         tx.send(packet) | ||||
| 
 | ||||
|     def restart_delayed(self, count=None, most_recent=True): | ||||
|         """Walk the list of delayed messages and restart them if any.""" | ||||
|         if not count: | ||||
|             # Send all the delayed messages | ||||
|             for key in self.data.keys(): | ||||
|                 pkt = self.data[key] | ||||
|                 if pkt._last_send_attempt == pkt._retry_count: | ||||
|                     self._resend(pkt) | ||||
|         else: | ||||
|             # They want to resend <count> delayed messages | ||||
|             tmp = sorted( | ||||
|                 self.data.items(), | ||||
|                 reverse=most_recent, | ||||
|                 key=lambda x: x[1].last_send_time, | ||||
|             ) | ||||
|             pkt_list = tmp[:count] | ||||
|             for (_key, pkt) in pkt_list: | ||||
|                 self._resend(pkt) | ||||
|  | ||||
| @ -28,7 +28,7 @@ class WatchList(objectstore.ObjectStoreMixin): | ||||
|         return cls._instance | ||||
| 
 | ||||
|     def __init__(self, config=None): | ||||
|         ring_size = CONF.watch_list.packet_keep_count | ||||
|         CONF.watch_list.packet_keep_count | ||||
| 
 | ||||
|         if CONF.watch_list.callsigns: | ||||
|             for callsign in CONF.watch_list.callsigns: | ||||
| @ -38,12 +38,22 @@ class WatchList(objectstore.ObjectStoreMixin): | ||||
|                 # last time a message was seen by aprs-is.  For now this | ||||
|                 # is all we can do. | ||||
|                 self.data[call] = { | ||||
|                     "last": datetime.datetime.now(), | ||||
|                     "packets": utils.RingBuffer( | ||||
|                         ring_size, | ||||
|                     ), | ||||
|                     "last": None, | ||||
|                     "packet": None, | ||||
|                 } | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def stats(self, serializable=False) -> dict: | ||||
|         stats = {} | ||||
|         for callsign in self.data: | ||||
|             stats[callsign] = { | ||||
|                 "last": self.data[callsign]["last"], | ||||
|                 "packet": self.data[callsign]["packet"], | ||||
|                 "age": self.age(callsign), | ||||
|                 "old": self.is_old(callsign), | ||||
|             } | ||||
|         return stats | ||||
| 
 | ||||
|     def is_enabled(self): | ||||
|         return CONF.watch_list.enabled | ||||
| 
 | ||||
| @ -58,7 +68,7 @@ class WatchList(objectstore.ObjectStoreMixin): | ||||
|             callsign = packet.from_call | ||||
|         if self.callsign_in_watchlist(callsign): | ||||
|             self.data[callsign]["last"] = datetime.datetime.now() | ||||
|             self.data[callsign]["packets"].append(packet) | ||||
|             self.data[callsign]["packet"] = packet | ||||
| 
 | ||||
|     def last_seen(self, callsign): | ||||
|         if self.callsign_in_watchlist(callsign): | ||||
| @ -66,7 +76,11 @@ class WatchList(objectstore.ObjectStoreMixin): | ||||
| 
 | ||||
|     def age(self, callsign): | ||||
|         now = datetime.datetime.now() | ||||
|         return str(now - self.last_seen(callsign)) | ||||
|         last_seen_time = self.last_seen(callsign) | ||||
|         if last_seen_time: | ||||
|             return str(now - last_seen_time) | ||||
|         else: | ||||
|             return None | ||||
| 
 | ||||
|     def max_delta(self, seconds=None): | ||||
|         if not seconds: | ||||
| @ -83,8 +97,11 @@ class WatchList(objectstore.ObjectStoreMixin): | ||||
|         We put this here so any notification plugin can use this | ||||
|         same test. | ||||
|         """ | ||||
|         age = self.age(callsign) | ||||
|         if not self.callsign_in_watchlist(callsign): | ||||
|             return False | ||||
| 
 | ||||
|         age = self.age(callsign) | ||||
|         if age: | ||||
|             delta = utils.parse_delta_str(age) | ||||
|             d = datetime.timedelta(**delta) | ||||
| 
 | ||||
| @ -94,3 +111,5 @@ class WatchList(objectstore.ObjectStoreMixin): | ||||
|                 return True | ||||
|             else: | ||||
|                 return False | ||||
|         else: | ||||
|             return False | ||||
|  | ||||
| @ -344,6 +344,28 @@ class PluginManager: | ||||
|         self._watchlist_pm = pluggy.PluginManager("aprsd") | ||||
|         self._watchlist_pm.add_hookspecs(APRSDPluginSpec) | ||||
| 
 | ||||
|     def stats(self, serializable=False) -> dict: | ||||
|         """Collect and return stats for all plugins.""" | ||||
|         def full_name_with_qualname(obj): | ||||
|             return "{}.{}".format( | ||||
|                 obj.__class__.__module__, | ||||
|                 obj.__class__.__qualname__, | ||||
|             ) | ||||
| 
 | ||||
|         plugin_stats = {} | ||||
|         plugins = self.get_plugins() | ||||
|         if plugins: | ||||
| 
 | ||||
|             for p in plugins: | ||||
|                 plugin_stats[full_name_with_qualname(p)] = { | ||||
|                     "enabled": p.enabled, | ||||
|                     "rx": p.rx_count, | ||||
|                     "tx": p.tx_count, | ||||
|                     "version": p.version, | ||||
|                 } | ||||
| 
 | ||||
|         return plugin_stats | ||||
| 
 | ||||
|     def is_plugin(self, obj): | ||||
|         for c in inspect.getmro(obj): | ||||
|             if issubclass(c, APRSDPluginBase): | ||||
| @ -369,7 +391,9 @@ class PluginManager: | ||||
|         try: | ||||
|             module_name, class_name = module_class_string.rsplit(".", 1) | ||||
|             module = importlib.import_module(module_name) | ||||
|             module = importlib.reload(module) | ||||
|             # Commented out because the email thread starts in a different context | ||||
|             # and hence gives a different singleton for the EmailStats | ||||
|             # module = importlib.reload(module) | ||||
|         except Exception as ex: | ||||
|             if not module_name: | ||||
|                 LOG.error(f"Failed to load Plugin {module_class_string}") | ||||
|  | ||||
| @ -11,7 +11,7 @@ import time | ||||
| import imapclient | ||||
| from oslo_config import cfg | ||||
| 
 | ||||
| from aprsd import packets, plugin, stats, threads | ||||
| from aprsd import packets, plugin, threads, utils | ||||
| from aprsd.threads import tx | ||||
| from aprsd.utils import trace | ||||
| 
 | ||||
| @ -60,6 +60,38 @@ class EmailInfo: | ||||
|             self._delay = val | ||||
| 
 | ||||
| 
 | ||||
| @utils.singleton | ||||
| class EmailStats: | ||||
|     """Singleton object to store stats related to email.""" | ||||
|     _instance = None | ||||
|     tx = 0 | ||||
|     rx = 0 | ||||
|     email_thread_last_time = None | ||||
| 
 | ||||
|     def stats(self, serializable=False): | ||||
|         if CONF.email_plugin.enabled: | ||||
|             last_check_time = self.email_thread_last_time | ||||
|             if serializable and last_check_time: | ||||
|                 last_check_time = last_check_time.isoformat() | ||||
|             stats = { | ||||
|                 "tx": self.tx, | ||||
|                 "rx": self.rx, | ||||
|                 "last_check_time": last_check_time, | ||||
|             } | ||||
|         else: | ||||
|             stats = {} | ||||
|         return stats | ||||
| 
 | ||||
|     def tx_inc(self): | ||||
|         self.tx += 1 | ||||
| 
 | ||||
|     def rx_inc(self): | ||||
|         self.rx += 1 | ||||
| 
 | ||||
|     def email_thread_update(self): | ||||
|         self.email_thread_last_time = datetime.datetime.now() | ||||
| 
 | ||||
| 
 | ||||
| class EmailPlugin(plugin.APRSDRegexCommandPluginBase): | ||||
|     """Email Plugin.""" | ||||
| 
 | ||||
| @ -190,10 +222,6 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase): | ||||
| def _imap_connect(): | ||||
|     imap_port = CONF.email_plugin.imap_port | ||||
|     use_ssl = CONF.email_plugin.imap_use_ssl | ||||
|     # host = CONFIG["aprsd"]["email"]["imap"]["host"] | ||||
|     # msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port) | ||||
|     #    LOG.debug("Connect to IMAP host {} with user '{}'". | ||||
|     #              format(msg, CONFIG['imap']['login'])) | ||||
| 
 | ||||
|     try: | ||||
|         server = imapclient.IMAPClient( | ||||
| @ -440,7 +468,7 @@ def send_email(to_addr, content): | ||||
|                 [to_addr], | ||||
|                 msg.as_string(), | ||||
|             ) | ||||
|             stats.APRSDStats().email_tx_inc() | ||||
|             EmailStats().tx_inc() | ||||
|         except Exception: | ||||
|             LOG.exception("Sendmail Error!!!!") | ||||
|             server.quit() | ||||
| @ -545,7 +573,7 @@ class APRSDEmailThread(threads.APRSDThread): | ||||
| 
 | ||||
|     def loop(self): | ||||
|         time.sleep(5) | ||||
|         stats.APRSDStats().email_thread_update() | ||||
|         EmailStats().email_thread_update() | ||||
|         # always sleep for 5 seconds and see if we need to check email | ||||
|         # This allows CTRL-C to stop the execution of this loop sooner | ||||
|         # than check_email_delay time | ||||
|  | ||||
| @ -1,81 +0,0 @@ | ||||
| import datetime | ||||
| import logging | ||||
| import re | ||||
| 
 | ||||
| from oslo_config import cfg | ||||
| 
 | ||||
| from aprsd import packets, plugin | ||||
| from aprsd.packets import tracker | ||||
| from aprsd.utils import trace | ||||
| 
 | ||||
| 
 | ||||
| CONF = cfg.CONF | ||||
| LOG = logging.getLogger("APRSD") | ||||
| 
 | ||||
| 
 | ||||
| class QueryPlugin(plugin.APRSDRegexCommandPluginBase): | ||||
|     """Query command.""" | ||||
| 
 | ||||
|     command_regex = r"^\!.*" | ||||
|     command_name = "query" | ||||
|     short_description = "APRSD Owner command to query messages in the MsgTrack" | ||||
| 
 | ||||
|     def setup(self): | ||||
|         """Do any plugin setup here.""" | ||||
|         if not CONF.query_plugin.callsign: | ||||
|             LOG.error("Config query_plugin.callsign not set.  Disabling plugin") | ||||
|             self.enabled = False | ||||
|         self.enabled = True | ||||
| 
 | ||||
|     @trace.trace | ||||
|     def process(self, packet: packets.MessagePacket): | ||||
|         LOG.info("Query COMMAND") | ||||
| 
 | ||||
|         fromcall = packet.from_call | ||||
|         message = packet.get("message_text", None) | ||||
| 
 | ||||
|         pkt_tracker = tracker.PacketTrack() | ||||
|         now = datetime.datetime.now() | ||||
|         reply = "Pending messages ({}) {}".format( | ||||
|             len(pkt_tracker), | ||||
|             now.strftime("%H:%M:%S"), | ||||
|         ) | ||||
| 
 | ||||
|         searchstring = "^" + CONF.query_plugin.callsign + ".*" | ||||
|         # only I can do admin commands | ||||
|         if re.search(searchstring, fromcall): | ||||
| 
 | ||||
|             # resend last N most recent:  "!3" | ||||
|             r = re.search(r"^\!([0-9]).*", message) | ||||
|             if r is not None: | ||||
|                 if len(pkt_tracker) > 0: | ||||
|                     last_n = r.group(1) | ||||
|                     reply = packets.NULL_MESSAGE | ||||
|                     LOG.debug(reply) | ||||
|                     pkt_tracker.restart_delayed(count=int(last_n)) | ||||
|                 else: | ||||
|                     reply = "No pending msgs to resend" | ||||
|                     LOG.debug(reply) | ||||
|                 return reply | ||||
| 
 | ||||
|             # resend all:   "!a" | ||||
|             r = re.search(r"^\![aA].*", message) | ||||
|             if r is not None: | ||||
|                 if len(pkt_tracker) > 0: | ||||
|                     reply = packets.NULL_MESSAGE | ||||
|                     LOG.debug(reply) | ||||
|                     pkt_tracker.restart_delayed() | ||||
|                 else: | ||||
|                     reply = "No pending msgs" | ||||
|                     LOG.debug(reply) | ||||
|                 return reply | ||||
| 
 | ||||
|             # delete all:   "!d" | ||||
|             r = re.search(r"^\![dD].*", message) | ||||
|             if r is not None: | ||||
|                 reply = "Deleted ALL pending msgs." | ||||
|                 LOG.debug(reply) | ||||
|                 pkt_tracker.flush() | ||||
|                 return reply | ||||
| 
 | ||||
|         return reply | ||||
| @ -1,7 +1,8 @@ | ||||
| import logging | ||||
| 
 | ||||
| import aprsd | ||||
| from aprsd import plugin, stats | ||||
| from aprsd import plugin | ||||
| from aprsd.stats import collector | ||||
| 
 | ||||
| 
 | ||||
| LOG = logging.getLogger("APRSD") | ||||
| @ -23,8 +24,8 @@ class VersionPlugin(plugin.APRSDRegexCommandPluginBase): | ||||
|         # fromcall = packet.get("from") | ||||
|         # message = packet.get("message_text", None) | ||||
|         # ack = packet.get("msgNo", "0") | ||||
|         s = stats.APRSDStats().stats() | ||||
|         s = collector.Collector().collect() | ||||
|         return "APRSD ver:{} uptime:{}".format( | ||||
|             aprsd.__version__, | ||||
|             s["aprsd"]["uptime"], | ||||
|             s["APRSDStats"]["uptime"], | ||||
|         ) | ||||
|  | ||||
| @ -1,14 +0,0 @@ | ||||
| import rpyc | ||||
| 
 | ||||
| 
 | ||||
| class AuthSocketStream(rpyc.SocketStream): | ||||
|     """Used to authenitcate the RPC stream to remote.""" | ||||
| 
 | ||||
|     @classmethod | ||||
|     def connect(cls, *args, authorizer=None, **kwargs): | ||||
|         stream_obj = super().connect(*args, **kwargs) | ||||
| 
 | ||||
|         if callable(authorizer): | ||||
|             authorizer(stream_obj.sock) | ||||
| 
 | ||||
|         return stream_obj | ||||
| @ -1,165 +0,0 @@ | ||||
| import json | ||||
| import logging | ||||
| 
 | ||||
| from oslo_config import cfg | ||||
| import rpyc | ||||
| 
 | ||||
| from aprsd import conf  # noqa | ||||
| from aprsd import rpc | ||||
| 
 | ||||
| 
 | ||||
| CONF = cfg.CONF | ||||
| LOG = logging.getLogger("APRSD") | ||||
| 
 | ||||
| 
 | ||||
| class RPCClient: | ||||
|     _instance = None | ||||
|     _rpc_client = None | ||||
| 
 | ||||
|     ip = None | ||||
|     port = None | ||||
|     magic_word = None | ||||
| 
 | ||||
|     def __new__(cls, *args, **kwargs): | ||||
|         if cls._instance is None: | ||||
|             cls._instance = super().__new__(cls) | ||||
|         return cls._instance | ||||
| 
 | ||||
|     def __init__(self, ip=None, port=None, magic_word=None): | ||||
|         if ip: | ||||
|             self.ip = ip | ||||
|         else: | ||||
|             self.ip = CONF.rpc_settings.ip | ||||
|         if port: | ||||
|             self.port = int(port) | ||||
|         else: | ||||
|             self.port = CONF.rpc_settings.port | ||||
|         if magic_word: | ||||
|             self.magic_word = magic_word | ||||
|         else: | ||||
|             self.magic_word = CONF.rpc_settings.magic_word | ||||
|         self._check_settings() | ||||
|         self.get_rpc_client() | ||||
| 
 | ||||
|     def _check_settings(self): | ||||
|         if not CONF.rpc_settings.enabled: | ||||
|             LOG.warning("RPC is not enabled, no way to get stats!!") | ||||
| 
 | ||||
|         if self.magic_word == conf.common.APRSD_DEFAULT_MAGIC_WORD: | ||||
|             LOG.warning("You are using the default RPC magic word!!!") | ||||
|             LOG.warning("edit aprsd.conf and change rpc_settings.magic_word") | ||||
| 
 | ||||
|         LOG.debug(f"RPC Client: {self.ip}:{self.port} {self.magic_word}") | ||||
| 
 | ||||
|     def _rpyc_connect( | ||||
|             self, host, port, service=rpyc.VoidService, | ||||
|             config={}, ipv6=False, | ||||
|             keepalive=False, authorizer=None, ): | ||||
| 
 | ||||
|         LOG.info(f"Connecting to RPC host '{host}:{port}'") | ||||
|         try: | ||||
|             s = rpc.AuthSocketStream.connect( | ||||
|                 host, port, ipv6=ipv6, keepalive=keepalive, | ||||
|                 authorizer=authorizer, | ||||
|             ) | ||||
|             return rpyc.utils.factory.connect_stream(s, service, config=config) | ||||
|         except ConnectionRefusedError: | ||||
|             LOG.error(f"Failed to connect to RPC host '{host}:{port}'") | ||||
|             return None | ||||
| 
 | ||||
|     def get_rpc_client(self): | ||||
|         if not self._rpc_client: | ||||
|             self._rpc_client = self._rpyc_connect( | ||||
|                 self.ip, | ||||
|                 self.port, | ||||
|                 authorizer=lambda sock: sock.send(self.magic_word.encode()), | ||||
|             ) | ||||
|         return self._rpc_client | ||||
| 
 | ||||
|     def get_stats_dict(self): | ||||
|         cl = self.get_rpc_client() | ||||
|         result = {} | ||||
|         if not cl: | ||||
|             return result | ||||
| 
 | ||||
|         try: | ||||
|             rpc_stats_dict = cl.root.get_stats() | ||||
|             result = json.loads(rpc_stats_dict) | ||||
|         except EOFError: | ||||
|             LOG.error("Lost connection to RPC Host") | ||||
|             self._rpc_client = None | ||||
|         return result | ||||
| 
 | ||||
|     def get_stats(self): | ||||
|         cl = self.get_rpc_client() | ||||
|         result = {} | ||||
|         if not cl: | ||||
|             return result | ||||
| 
 | ||||
|         try: | ||||
|             result = cl.root.get_stats_obj() | ||||
|         except EOFError: | ||||
|             LOG.error("Lost connection to RPC Host") | ||||
|             self._rpc_client = None | ||||
|         return result | ||||
| 
 | ||||
|     def get_packet_track(self): | ||||
|         cl = self.get_rpc_client() | ||||
|         result = None | ||||
|         if not cl: | ||||
|             return result | ||||
|         try: | ||||
|             result = cl.root.get_packet_track() | ||||
|         except EOFError: | ||||
|             LOG.error("Lost connection to RPC Host") | ||||
|             self._rpc_client = None | ||||
|         return result | ||||
| 
 | ||||
|     def get_packet_list(self): | ||||
|         cl = self.get_rpc_client() | ||||
|         result = None | ||||
|         if not cl: | ||||
|             return result | ||||
|         try: | ||||
|             result = cl.root.get_packet_list() | ||||
|         except EOFError: | ||||
|             LOG.error("Lost connection to RPC Host") | ||||
|             self._rpc_client = None | ||||
|         return result | ||||
| 
 | ||||
|     def get_watch_list(self): | ||||
|         cl = self.get_rpc_client() | ||||
|         result = None | ||||
|         if not cl: | ||||
|             return result | ||||
|         try: | ||||
|             result = cl.root.get_watch_list() | ||||
|         except EOFError: | ||||
|             LOG.error("Lost connection to RPC Host") | ||||
|             self._rpc_client = None | ||||
|         return result | ||||
| 
 | ||||
|     def get_seen_list(self): | ||||
|         cl = self.get_rpc_client() | ||||
|         result = None | ||||
|         if not cl: | ||||
|             return result | ||||
|         try: | ||||
|             result = cl.root.get_seen_list() | ||||
|         except EOFError: | ||||
|             LOG.error("Lost connection to RPC Host") | ||||
|             self._rpc_client = None | ||||
|         return result | ||||
| 
 | ||||
|     def get_log_entries(self): | ||||
|         cl = self.get_rpc_client() | ||||
|         result = None | ||||
|         if not cl: | ||||
|             return result | ||||
|         try: | ||||
|             result_str = cl.root.get_log_entries() | ||||
|             result = json.loads(result_str) | ||||
|         except EOFError: | ||||
|             LOG.error("Lost connection to RPC Host") | ||||
|             self._rpc_client = None | ||||
|         return result | ||||
| @ -1,99 +0,0 @@ | ||||
| import json | ||||
| import logging | ||||
| 
 | ||||
| from oslo_config import cfg | ||||
| import rpyc | ||||
| from rpyc.utils.authenticators import AuthenticationError | ||||
| from rpyc.utils.server import ThreadPoolServer | ||||
| 
 | ||||
| from aprsd import conf  # noqa: F401 | ||||
| from aprsd import packets, stats, threads | ||||
| from aprsd.threads import log_monitor | ||||
| 
 | ||||
| 
 | ||||
| CONF = cfg.CONF | ||||
| LOG = logging.getLogger("APRSD") | ||||
| 
 | ||||
| 
 | ||||
| def magic_word_authenticator(sock): | ||||
|     client_ip = sock.getpeername()[0] | ||||
|     magic = sock.recv(len(CONF.rpc_settings.magic_word)).decode() | ||||
|     if magic != CONF.rpc_settings.magic_word: | ||||
|         LOG.error( | ||||
|             f"wrong magic word passed from {client_ip} " | ||||
|             "'{magic}' != '{CONF.rpc_settings.magic_word}'", | ||||
|         ) | ||||
|         raise AuthenticationError( | ||||
|             f"wrong magic word passed in '{magic}'" | ||||
|             f" != '{CONF.rpc_settings.magic_word}'", | ||||
|         ) | ||||
|     return sock, None | ||||
| 
 | ||||
| 
 | ||||
| class APRSDRPCThread(threads.APRSDThread): | ||||
|     def __init__(self): | ||||
|         super().__init__(name="RPCThread") | ||||
|         self.thread = ThreadPoolServer( | ||||
|             APRSDService, | ||||
|             port=CONF.rpc_settings.port, | ||||
|             protocol_config={"allow_public_attrs": True}, | ||||
|             authenticator=magic_word_authenticator, | ||||
|         ) | ||||
| 
 | ||||
|     def stop(self): | ||||
|         if self.thread: | ||||
|             self.thread.close() | ||||
|         self.thread_stop = True | ||||
| 
 | ||||
|     def loop(self): | ||||
|         # there is no loop as run is blocked | ||||
|         if self.thread and not self.thread_stop: | ||||
|             # This is a blocking call | ||||
|             self.thread.start() | ||||
| 
 | ||||
| 
 | ||||
| @rpyc.service | ||||
| class APRSDService(rpyc.Service): | ||||
|     def on_connect(self, conn): | ||||
|         # code that runs when a connection is created | ||||
|         # (to init the service, if needed) | ||||
|         LOG.info("RPC Client Connected") | ||||
|         self._conn = conn | ||||
| 
 | ||||
|     def on_disconnect(self, conn): | ||||
|         # code that runs after the connection has already closed | ||||
|         # (to finalize the service, if needed) | ||||
|         LOG.info("RPC Client Disconnected") | ||||
|         self._conn = None | ||||
| 
 | ||||
|     @rpyc.exposed | ||||
|     def get_stats(self): | ||||
|         stat = stats.APRSDStats() | ||||
|         stats_dict = stat.stats() | ||||
|         return_str = json.dumps(stats_dict, indent=4, sort_keys=True, default=str) | ||||
|         return return_str | ||||
| 
 | ||||
|     @rpyc.exposed | ||||
|     def get_stats_obj(self): | ||||
|         return stats.APRSDStats() | ||||
| 
 | ||||
|     @rpyc.exposed | ||||
|     def get_packet_list(self): | ||||
|         return packets.PacketList() | ||||
| 
 | ||||
|     @rpyc.exposed | ||||
|     def get_packet_track(self): | ||||
|         return packets.PacketTrack() | ||||
| 
 | ||||
|     @rpyc.exposed | ||||
|     def get_watch_list(self): | ||||
|         return packets.WatchList() | ||||
| 
 | ||||
|     @rpyc.exposed | ||||
|     def get_seen_list(self): | ||||
|         return packets.SeenList() | ||||
| 
 | ||||
|     @rpyc.exposed | ||||
|     def get_log_entries(self): | ||||
|         entries = log_monitor.LogEntries().get_all_and_purge() | ||||
|         return json.dumps(entries, default=str) | ||||
							
								
								
									
										265
									
								
								aprsd/stats.py
									
									
									
									
									
								
							
							
						
						
									
										265
									
								
								aprsd/stats.py
									
									
									
									
									
								
							| @ -1,265 +0,0 @@ | ||||
| import datetime | ||||
| import logging | ||||
| import threading | ||||
| 
 | ||||
| from oslo_config import cfg | ||||
| import wrapt | ||||
| 
 | ||||
| import aprsd | ||||
| from aprsd import packets, plugin, utils | ||||
| 
 | ||||
| 
 | ||||
| CONF = cfg.CONF | ||||
| LOG = logging.getLogger("APRSD") | ||||
| 
 | ||||
| 
 | ||||
| class APRSDStats: | ||||
| 
 | ||||
|     _instance = None | ||||
|     lock = threading.Lock() | ||||
| 
 | ||||
|     start_time = None | ||||
|     _aprsis_server = None | ||||
|     _aprsis_keepalive = None | ||||
| 
 | ||||
|     _email_thread_last_time = None | ||||
|     _email_tx = 0 | ||||
|     _email_rx = 0 | ||||
| 
 | ||||
|     _mem_current = 0 | ||||
|     _mem_peak = 0 | ||||
| 
 | ||||
|     _thread_info = {} | ||||
| 
 | ||||
|     _pkt_cnt = { | ||||
|         "Packet": { | ||||
|             "tx": 0, | ||||
|             "rx": 0, | ||||
|         }, | ||||
|         "AckPacket": { | ||||
|             "tx": 0, | ||||
|             "rx": 0, | ||||
|         }, | ||||
|         "GPSPacket": { | ||||
|             "tx": 0, | ||||
|             "rx": 0, | ||||
|         }, | ||||
|         "StatusPacket": { | ||||
|             "tx": 0, | ||||
|             "rx": 0, | ||||
|         }, | ||||
|         "MicEPacket": { | ||||
|             "tx": 0, | ||||
|             "rx": 0, | ||||
|         }, | ||||
|         "MessagePacket": { | ||||
|             "tx": 0, | ||||
|             "rx": 0, | ||||
|         }, | ||||
|         "WeatherPacket": { | ||||
|             "tx": 0, | ||||
|             "rx": 0, | ||||
|         }, | ||||
|         "ObjectPacket": { | ||||
|             "tx": 0, | ||||
|             "rx": 0, | ||||
|         }, | ||||
|     } | ||||
| 
 | ||||
|     def __new__(cls, *args, **kwargs): | ||||
|         if cls._instance is None: | ||||
|             cls._instance = super().__new__(cls) | ||||
|             # any init here | ||||
|             cls._instance.start_time = datetime.datetime.now() | ||||
|             cls._instance._aprsis_keepalive = datetime.datetime.now() | ||||
|         return cls._instance | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     @property | ||||
|     def uptime(self): | ||||
|         return datetime.datetime.now() - self.start_time | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     @property | ||||
|     def memory(self): | ||||
|         return self._mem_current | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def set_memory(self, memory): | ||||
|         self._mem_current = memory | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     @property | ||||
|     def memory_peak(self): | ||||
|         return self._mem_peak | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def set_memory_peak(self, memory): | ||||
|         self._mem_peak = memory | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def set_thread_info(self, thread_info): | ||||
|         self._thread_info = thread_info | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     @property | ||||
|     def thread_info(self): | ||||
|         return self._thread_info | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     @property | ||||
|     def aprsis_server(self): | ||||
|         return self._aprsis_server | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def set_aprsis_server(self, server): | ||||
|         self._aprsis_server = server | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     @property | ||||
|     def aprsis_keepalive(self): | ||||
|         return self._aprsis_keepalive | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def set_aprsis_keepalive(self): | ||||
|         self._aprsis_keepalive = datetime.datetime.now() | ||||
| 
 | ||||
|     def rx(self, packet): | ||||
|         pkt_type = packet.__class__.__name__ | ||||
|         if pkt_type not in self._pkt_cnt: | ||||
|             self._pkt_cnt[pkt_type] = { | ||||
|                 "tx": 0, | ||||
|                 "rx": 0, | ||||
|             } | ||||
|         self._pkt_cnt[pkt_type]["rx"] += 1 | ||||
| 
 | ||||
|     def tx(self, packet): | ||||
|         pkt_type = packet.__class__.__name__ | ||||
|         if pkt_type not in self._pkt_cnt: | ||||
|             self._pkt_cnt[pkt_type] = { | ||||
|                 "tx": 0, | ||||
|                 "rx": 0, | ||||
|             } | ||||
|         self._pkt_cnt[pkt_type]["tx"] += 1 | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     @property | ||||
|     def msgs_tracked(self): | ||||
|         return packets.PacketTrack().total_tracked | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     @property | ||||
|     def email_tx(self): | ||||
|         return self._email_tx | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def email_tx_inc(self): | ||||
|         self._email_tx += 1 | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     @property | ||||
|     def email_rx(self): | ||||
|         return self._email_rx | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def email_rx_inc(self): | ||||
|         self._email_rx += 1 | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     @property | ||||
|     def email_thread_time(self): | ||||
|         return self._email_thread_last_time | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def email_thread_update(self): | ||||
|         self._email_thread_last_time = datetime.datetime.now() | ||||
| 
 | ||||
|     def stats(self): | ||||
|         now = datetime.datetime.now() | ||||
|         if self._email_thread_last_time: | ||||
|             last_update = str(now - self._email_thread_last_time) | ||||
|         else: | ||||
|             last_update = "never" | ||||
| 
 | ||||
|         if self._aprsis_keepalive: | ||||
|             last_aprsis_keepalive = str(now - self._aprsis_keepalive) | ||||
|         else: | ||||
|             last_aprsis_keepalive = "never" | ||||
| 
 | ||||
|         pm = plugin.PluginManager() | ||||
|         plugins = pm.get_plugins() | ||||
|         plugin_stats = {} | ||||
|         if plugins: | ||||
|             def full_name_with_qualname(obj): | ||||
|                 return "{}.{}".format( | ||||
|                     obj.__class__.__module__, | ||||
|                     obj.__class__.__qualname__, | ||||
|                 ) | ||||
| 
 | ||||
|             for p in plugins: | ||||
|                 plugin_stats[full_name_with_qualname(p)] = { | ||||
|                     "enabled": p.enabled, | ||||
|                     "rx": p.rx_count, | ||||
|                     "tx": p.tx_count, | ||||
|                     "version": p.version, | ||||
|                 } | ||||
| 
 | ||||
|         wl = packets.WatchList() | ||||
|         sl = packets.SeenList() | ||||
|         pl = packets.PacketList() | ||||
| 
 | ||||
|         stats = { | ||||
|             "aprsd": { | ||||
|                 "version": aprsd.__version__, | ||||
|                 "uptime": utils.strfdelta(self.uptime), | ||||
|                 "callsign": CONF.callsign, | ||||
|                 "memory_current": int(self.memory), | ||||
|                 "memory_current_str": utils.human_size(self.memory), | ||||
|                 "memory_peak": int(self.memory_peak), | ||||
|                 "memory_peak_str": utils.human_size(self.memory_peak), | ||||
|                 "threads": self._thread_info, | ||||
|                 "watch_list": wl.get_all(), | ||||
|                 "seen_list": sl.get_all(), | ||||
|             }, | ||||
|             "aprs-is": { | ||||
|                 "server": str(self.aprsis_server), | ||||
|                 "callsign": CONF.aprs_network.login, | ||||
|                 "last_update": last_aprsis_keepalive, | ||||
|             }, | ||||
|             "packets": { | ||||
|                 "total_tracked": int(pl.total_tx() + pl.total_rx()), | ||||
|                 "total_sent": int(pl.total_tx()), | ||||
|                 "total_received": int(pl.total_rx()), | ||||
|                 "by_type": self._pkt_cnt, | ||||
|             }, | ||||
|             "messages": { | ||||
|                 "sent": self._pkt_cnt["MessagePacket"]["tx"], | ||||
|                 "received": self._pkt_cnt["MessagePacket"]["tx"], | ||||
|                 "ack_sent": self._pkt_cnt["AckPacket"]["tx"], | ||||
|             }, | ||||
|             "email": { | ||||
|                 "enabled": CONF.email_plugin.enabled, | ||||
|                 "sent": int(self._email_tx), | ||||
|                 "received": int(self._email_rx), | ||||
|                 "thread_last_update": last_update, | ||||
|             }, | ||||
|             "plugins": plugin_stats, | ||||
|         } | ||||
|         return stats | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         pl = packets.PacketList() | ||||
|         return ( | ||||
|             "Uptime:{} Msgs TX:{} RX:{} " | ||||
|             "ACK: TX:{} RX:{} " | ||||
|             "Email TX:{} RX:{} LastLoop:{} ".format( | ||||
|                 self.uptime, | ||||
|                 pl.total_tx(), | ||||
|                 pl.total_rx(), | ||||
|                 self._pkt_cnt["AckPacket"]["tx"], | ||||
|                 self._pkt_cnt["AckPacket"]["rx"], | ||||
|                 self._email_tx, | ||||
|                 self._email_rx, | ||||
|                 self._email_thread_last_time, | ||||
|             ) | ||||
|         ) | ||||
							
								
								
									
										20
									
								
								aprsd/stats/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								aprsd/stats/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| from aprsd import client as aprs_client | ||||
| from aprsd import plugin | ||||
| from aprsd.packets import packet_list, seen_list, tracker, watch_list | ||||
| from aprsd.plugins import email | ||||
| from aprsd.stats import app, collector | ||||
| from aprsd.threads import aprsd | ||||
| 
 | ||||
| 
 | ||||
| # Create the collector and register all the objects | ||||
| # that APRSD has that implement the stats protocol | ||||
| stats_collector = collector.Collector() | ||||
| stats_collector.register_producer(app.APRSDStats()) | ||||
| stats_collector.register_producer(packet_list.PacketList()) | ||||
| stats_collector.register_producer(watch_list.WatchList()) | ||||
| stats_collector.register_producer(tracker.PacketTrack()) | ||||
| stats_collector.register_producer(plugin.PluginManager()) | ||||
| stats_collector.register_producer(aprsd.APRSDThreadList()) | ||||
| stats_collector.register_producer(email.EmailStats()) | ||||
| stats_collector.register_producer(aprs_client.APRSClientStats()) | ||||
| stats_collector.register_producer(seen_list.SeenList()) | ||||
							
								
								
									
										47
									
								
								aprsd/stats/app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								aprsd/stats/app.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| import datetime | ||||
| import tracemalloc | ||||
| 
 | ||||
| from oslo_config import cfg | ||||
| 
 | ||||
| import aprsd | ||||
| from aprsd import utils | ||||
| 
 | ||||
| 
 | ||||
| CONF = cfg.CONF | ||||
| 
 | ||||
| 
 | ||||
| class APRSDStats: | ||||
|     """The AppStats class is used to collect stats from the application.""" | ||||
| 
 | ||||
|     _instance = None | ||||
| 
 | ||||
|     def __new__(cls, *args, **kwargs): | ||||
|         """Have to override the new method to make this a singleton | ||||
| 
 | ||||
|         instead of using @singletone decorator so the unit tests work. | ||||
|         """ | ||||
|         if not cls._instance: | ||||
|             cls._instance = super().__new__(cls) | ||||
|         return cls._instance | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         self.start_time = datetime.datetime.now() | ||||
| 
 | ||||
|     def uptime(self): | ||||
|         return datetime.datetime.now() - self.start_time | ||||
| 
 | ||||
|     def stats(self, serializable=False) -> dict: | ||||
|         current, peak = tracemalloc.get_traced_memory() | ||||
|         uptime = self.uptime() | ||||
|         if serializable: | ||||
|             uptime = str(uptime) | ||||
|         stats = { | ||||
|             "version": aprsd.__version__, | ||||
|             "uptime": uptime, | ||||
|             "callsign": CONF.callsign, | ||||
|             "memory_current": int(current), | ||||
|             "memory_current_str": utils.human_size(current), | ||||
|             "memory_peak": int(peak), | ||||
|             "memory_peak_str": utils.human_size(peak), | ||||
|         } | ||||
|         return stats | ||||
							
								
								
									
										30
									
								
								aprsd/stats/collector.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								aprsd/stats/collector.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| from typing import Protocol | ||||
| 
 | ||||
| from aprsd.utils import singleton | ||||
| 
 | ||||
| 
 | ||||
| class StatsProducer(Protocol): | ||||
|     """The StatsProducer protocol is used to define the interface for collecting stats.""" | ||||
|     def stats(self, serializeable=False) -> dict: | ||||
|         """provide stats in a dictionary format.""" | ||||
|         ... | ||||
| 
 | ||||
| 
 | ||||
| @singleton | ||||
| class Collector: | ||||
|     """The Collector class is used to collect stats from multiple StatsProducer instances.""" | ||||
|     def __init__(self): | ||||
|         self.producers: dict[str, StatsProducer] = {} | ||||
| 
 | ||||
|     def collect(self, serializable=False) -> dict: | ||||
|         stats = {} | ||||
|         for name, producer in self.producers.items(): | ||||
|             # No need to put in empty stats | ||||
|             tmp_stats = producer.stats(serializable=serializable) | ||||
|             if tmp_stats: | ||||
|                 stats[name] = tmp_stats | ||||
|         return stats | ||||
| 
 | ||||
|     def register_producer(self, producer: StatsProducer): | ||||
|         name = producer.__class__.__name__ | ||||
|         self.producers[name] = producer | ||||
| @ -3,8 +3,9 @@ import queue | ||||
| # Make these available to anyone importing | ||||
| # aprsd.threads | ||||
| from .aprsd import APRSDThread, APRSDThreadList  # noqa: F401 | ||||
| from .keep_alive import KeepAliveThread  # noqa: F401 | ||||
| from .rx import APRSDRXThread, APRSDDupeRXThread, APRSDProcessPacketThread  # noqa: F401 | ||||
| from .rx import (  # noqa: F401 | ||||
|     APRSDDupeRXThread, APRSDProcessPacketThread, APRSDRXThread, | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| packet_queue = queue.Queue(maxsize=20) | ||||
|  | ||||
| @ -13,7 +13,7 @@ LOG = logging.getLogger("APRSD") | ||||
| class APRSDThread(threading.Thread, metaclass=abc.ABCMeta): | ||||
|     """Base class for all threads in APRSD.""" | ||||
| 
 | ||||
|     loop_interval = 1 | ||||
|     loop_count = 1 | ||||
| 
 | ||||
|     def __init__(self, name): | ||||
|         super().__init__(name=name) | ||||
| @ -47,8 +47,8 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta): | ||||
|     def run(self): | ||||
|         LOG.debug("Starting") | ||||
|         while not self._should_quit(): | ||||
|             self.loop_count += 1 | ||||
|             can_loop = self.loop() | ||||
|             self.loop_interval += 1 | ||||
|             self._last_loop = datetime.datetime.now() | ||||
|             if not can_loop: | ||||
|                 self.stop() | ||||
| @ -71,6 +71,20 @@ class APRSDThreadList: | ||||
|             cls.threads_list = [] | ||||
|         return cls._instance | ||||
| 
 | ||||
|     def stats(self, serializable=False) -> dict: | ||||
|         stats = {} | ||||
|         for th in self.threads_list: | ||||
|             age = th.loop_age() | ||||
|             if serializable: | ||||
|                 age = str(age) | ||||
|             stats[th.__class__.__name__] = { | ||||
|                 "name": th.name, | ||||
|                 "alive": th.is_alive(), | ||||
|                 "age": th.loop_age(), | ||||
|                 "loop_count": th.loop_count, | ||||
|             } | ||||
|         return stats | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def add(self, thread_obj): | ||||
|         self.threads_list.append(thread_obj) | ||||
|  | ||||
| @ -5,7 +5,8 @@ import tracemalloc | ||||
| 
 | ||||
| from oslo_config import cfg | ||||
| 
 | ||||
| from aprsd import client, packets, stats, utils | ||||
| from aprsd import client, packets, utils | ||||
| from aprsd.stats import collector | ||||
| from aprsd.threads import APRSDThread, APRSDThreadList | ||||
| 
 | ||||
| 
 | ||||
| @ -24,61 +25,66 @@ class KeepAliveThread(APRSDThread): | ||||
|         self.max_delta = datetime.timedelta(**max_timeout) | ||||
| 
 | ||||
|     def loop(self): | ||||
|         if self.cntr % 60 == 0: | ||||
|             pkt_tracker = packets.PacketTrack() | ||||
|             stats_obj = stats.APRSDStats() | ||||
|         if self.loop_count % 60 == 0: | ||||
|             stats_json = collector.Collector().collect() | ||||
|             pl = packets.PacketList() | ||||
|             thread_list = APRSDThreadList() | ||||
|             now = datetime.datetime.now() | ||||
|             last_email = stats_obj.email_thread_time | ||||
|             if last_email: | ||||
|                 email_thread_time = utils.strfdelta(now - last_email) | ||||
| 
 | ||||
|             if "EmailStats" in stats_json: | ||||
|                 email_stats = stats_json["EmailStats"] | ||||
|                 if email_stats["last_check_time"]: | ||||
|                     email_thread_time = utils.strfdelta(now - email_stats["last_check_time"]) | ||||
|                 else: | ||||
|                     email_thread_time = "N/A" | ||||
|             else: | ||||
|                 email_thread_time = "N/A" | ||||
| 
 | ||||
|             last_msg_time = utils.strfdelta(now - stats_obj.aprsis_keepalive) | ||||
|             if "APRSClientStats" in stats_json and stats_json["APRSClientStats"].get("transport") == "aprsis": | ||||
|                 if stats_json["APRSClientStats"].get("server_keepalive"): | ||||
|                     last_msg_time = utils.strfdelta(now - stats_json["APRSClientStats"]["server_keepalive"]) | ||||
|                 else: | ||||
|                     last_msg_time = "N/A" | ||||
|             else: | ||||
|                 last_msg_time = "N/A" | ||||
| 
 | ||||
|             current, peak = tracemalloc.get_traced_memory() | ||||
|             stats_obj.set_memory(current) | ||||
|             stats_obj.set_memory_peak(peak) | ||||
| 
 | ||||
|             login = CONF.callsign | ||||
| 
 | ||||
|             tracked_packets = len(pkt_tracker) | ||||
|             tracked_packets = stats_json["PacketTrack"]["total_tracked"] | ||||
|             tx_msg = 0 | ||||
|             rx_msg = 0 | ||||
|             if "PacketList" in stats_json: | ||||
|                 msg_packets = stats_json["PacketList"].get("MessagePacket") | ||||
|                 if msg_packets: | ||||
|                     tx_msg = msg_packets.get("tx", 0) | ||||
|                     rx_msg = msg_packets.get("rx", 0) | ||||
| 
 | ||||
|             keepalive = ( | ||||
|                 "{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} " | ||||
|                 "Last:{} Email: {} - RAM Current:{} Peak:{} Threads:{}" | ||||
|             ).format( | ||||
|                 login, | ||||
|                 utils.strfdelta(stats_obj.uptime), | ||||
|                 stats_json["APRSDStats"]["callsign"], | ||||
|                 stats_json["APRSDStats"]["uptime"], | ||||
|                 pl.total_rx(), | ||||
|                 pl.total_tx(), | ||||
|                 tracked_packets, | ||||
|                 stats_obj._pkt_cnt["MessagePacket"]["tx"], | ||||
|                 stats_obj._pkt_cnt["MessagePacket"]["rx"], | ||||
|                 tx_msg, | ||||
|                 rx_msg, | ||||
|                 last_msg_time, | ||||
|                 email_thread_time, | ||||
|                 utils.human_size(current), | ||||
|                 utils.human_size(peak), | ||||
|                 stats_json["APRSDStats"]["memory_current_str"], | ||||
|                 stats_json["APRSDStats"]["memory_peak_str"], | ||||
|                 len(thread_list), | ||||
|             ) | ||||
|             LOG.info(keepalive) | ||||
|             thread_out = [] | ||||
|             thread_info = {} | ||||
|             for thread in thread_list.threads_list: | ||||
|                 alive = thread.is_alive() | ||||
|                 age = thread.loop_age() | ||||
|                 key = thread.__class__.__name__ | ||||
|                 thread_out.append(f"{key}:{alive}:{age}") | ||||
|                 if key not in thread_info: | ||||
|                     thread_info[key] = {} | ||||
|                 thread_info[key]["alive"] = alive | ||||
|                 thread_info[key]["age"] = age | ||||
|             if "APRSDThreadList" in stats_json: | ||||
|                 thread_list = stats_json["APRSDThreadList"] | ||||
|                 for thread_name in thread_list: | ||||
|                     thread = thread_list[thread_name] | ||||
|                     alive = thread["alive"] | ||||
|                     age = thread["age"] | ||||
|                     key = thread["name"] | ||||
|                     if not alive: | ||||
|                         LOG.error(f"Thread {thread}") | ||||
|             LOG.info(",".join(thread_out)) | ||||
|             stats_obj.set_thread_info(thread_info) | ||||
|                     LOG.info(f"{key: <15} Alive? {str(alive): <5} {str(age): <20}") | ||||
| 
 | ||||
|             # check the APRS connection | ||||
|             cl = client.factory.create() | ||||
| @ -90,18 +96,18 @@ class KeepAliveThread(APRSDThread): | ||||
|             if not cl.is_alive() and self.cntr > 0: | ||||
|                 LOG.error(f"{cl.__class__.__name__} is not alive!!! Resetting") | ||||
|                 client.factory.create().reset() | ||||
|             else: | ||||
|                 # See if we should reset the aprs-is client | ||||
|                 # Due to losing a keepalive from them | ||||
|                 delta_dict = utils.parse_delta_str(last_msg_time) | ||||
|                 delta = datetime.timedelta(**delta_dict) | ||||
| 
 | ||||
|                 if delta > self.max_delta: | ||||
|                     #  We haven't gotten a keepalive from aprs-is in a while | ||||
|                     # reset the connection.a | ||||
|                     if not client.KISSClient.is_enabled(): | ||||
|                         LOG.warning(f"Resetting connection to APRS-IS {delta}") | ||||
|                         client.factory.create().reset() | ||||
|             # else: | ||||
|             #     # See if we should reset the aprs-is client | ||||
|             #     # Due to losing a keepalive from them | ||||
|             #     delta_dict = utils.parse_delta_str(last_msg_time) | ||||
|             #     delta = datetime.timedelta(**delta_dict) | ||||
|             # | ||||
|             #     if delta > self.max_delta: | ||||
|             #         #  We haven't gotten a keepalive from aprs-is in a while | ||||
|             #         # reset the connection.a | ||||
|             #         if not client.KISSClient.is_enabled(): | ||||
|             #             LOG.warning(f"Resetting connection to APRS-IS {delta}") | ||||
|             #             client.factory.create().reset() | ||||
| 
 | ||||
|             # Check version every day | ||||
|             delta = now - self.checker_time | ||||
|  | ||||
| @ -1,25 +1,56 @@ | ||||
| import datetime | ||||
| import logging | ||||
| import threading | ||||
| 
 | ||||
| from oslo_config import cfg | ||||
| import requests | ||||
| import wrapt | ||||
| 
 | ||||
| from aprsd import threads | ||||
| from aprsd.log import log | ||||
| 
 | ||||
| 
 | ||||
| CONF = cfg.CONF | ||||
| LOG = logging.getLogger("APRSD") | ||||
| 
 | ||||
| 
 | ||||
| def send_log_entries(force=False): | ||||
|     """Send all of the log entries to the web interface.""" | ||||
|     if CONF.admin.web_enabled: | ||||
|         if force or LogEntries().is_purge_ready(): | ||||
|             entries = LogEntries().get_all_and_purge() | ||||
|             print(f"Sending log entries {len(entries)}") | ||||
|             if entries: | ||||
|                 try: | ||||
|                     requests.post( | ||||
|                         f"http://{CONF.admin.web_ip}:{CONF.admin.web_port}/log_entries", | ||||
|                         json=entries, | ||||
|                         auth=(CONF.admin.user, CONF.admin.password), | ||||
|                     ) | ||||
|                 except Exception as ex: | ||||
|                     LOG.warning(f"Failed to send log entries {len(entries)}") | ||||
|                     LOG.warning(ex) | ||||
| 
 | ||||
| 
 | ||||
| class LogEntries: | ||||
|     entries = [] | ||||
|     lock = threading.Lock() | ||||
|     _instance = None | ||||
|     last_purge = datetime.datetime.now() | ||||
|     max_delta = datetime.timedelta( | ||||
|         hours=0.0, minutes=0, seconds=2, | ||||
|     ) | ||||
| 
 | ||||
|     def __new__(cls, *args, **kwargs): | ||||
|         if cls._instance is None: | ||||
|             cls._instance = super().__new__(cls) | ||||
|         return cls._instance | ||||
| 
 | ||||
|     def stats(self) -> dict: | ||||
|         return { | ||||
|             "log_entries": self.entries, | ||||
|         } | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def add(self, entry): | ||||
|         self.entries.append(entry) | ||||
| @ -28,8 +59,18 @@ class LogEntries: | ||||
|     def get_all_and_purge(self): | ||||
|         entries = self.entries.copy() | ||||
|         self.entries = [] | ||||
|         self.last_purge = datetime.datetime.now() | ||||
|         return entries | ||||
| 
 | ||||
|     def is_purge_ready(self): | ||||
|         now = datetime.datetime.now() | ||||
|         if ( | ||||
|             now - self.last_purge > self.max_delta | ||||
|             and len(self.entries) > 1 | ||||
|         ): | ||||
|             return True | ||||
|         return False | ||||
| 
 | ||||
|     @wrapt.synchronized(lock) | ||||
|     def __len__(self): | ||||
|         return len(self.entries) | ||||
| @ -40,6 +81,10 @@ class LogMonitorThread(threads.APRSDThread): | ||||
|     def __init__(self): | ||||
|         super().__init__("LogMonitorThread") | ||||
| 
 | ||||
|     def stop(self): | ||||
|         send_log_entries(force=True) | ||||
|         super().stop() | ||||
| 
 | ||||
|     def loop(self): | ||||
|         try: | ||||
|             record = log.logging_queue.get(block=True, timeout=2) | ||||
| @ -54,6 +99,7 @@ class LogMonitorThread(threads.APRSDThread): | ||||
|             # Just ignore thi | ||||
|             pass | ||||
| 
 | ||||
|         send_log_entries() | ||||
|         return True | ||||
| 
 | ||||
|     def json_record(self, record): | ||||
|  | ||||
| @ -6,7 +6,7 @@ import time | ||||
| import aprslib | ||||
| from oslo_config import cfg | ||||
| 
 | ||||
| from aprsd import client, packets, plugin, stats | ||||
| from aprsd import client, packets, plugin | ||||
| from aprsd.packets import log as packet_log | ||||
| from aprsd.threads import APRSDThread, tx | ||||
| 
 | ||||
| @ -27,7 +27,6 @@ class APRSDRXThread(APRSDThread): | ||||
|             self._client.stop() | ||||
| 
 | ||||
|     def loop(self): | ||||
|         LOG.debug(f"RX_MSG-LOOP {self.loop_interval}") | ||||
|         if not self._client: | ||||
|             self._client = client.factory.create() | ||||
|             time.sleep(1) | ||||
| @ -43,31 +42,29 @@ class APRSDRXThread(APRSDThread): | ||||
|             # and the aprslib developer didn't want to allow a PR to add | ||||
|             # kwargs.  :( | ||||
|             # https://github.com/rossengeorgiev/aprs-python/pull/56 | ||||
|             LOG.debug(f"Calling client consumer CL {self._client}") | ||||
|             self._client.consumer( | ||||
|                 self._process_packet, raw=False, blocking=False, | ||||
|             ) | ||||
|             LOG.debug(f"Consumer done {self._client}") | ||||
|         except ( | ||||
|             aprslib.exceptions.ConnectionDrop, | ||||
|             aprslib.exceptions.ConnectionError, | ||||
|         ): | ||||
|             LOG.error("Connection dropped, reconnecting") | ||||
|             time.sleep(5) | ||||
|             # Force the deletion of the client object connected to aprs | ||||
|             # This will cause a reconnect, next time client.get_client() | ||||
|             # is called | ||||
|             self._client.reset() | ||||
|         except Exception as ex: | ||||
|             LOG.error("Something bad happened!!!") | ||||
|             LOG.exception(ex) | ||||
|             return False | ||||
|             time.sleep(5) | ||||
|         except Exception: | ||||
|             # LOG.exception(ex) | ||||
|             LOG.error("Resetting connection and trying again.") | ||||
|             self._client.reset() | ||||
|             time.sleep(5) | ||||
|         # Continue to loop | ||||
|         return True | ||||
| 
 | ||||
|     def _process_packet(self, *args, **kwargs): | ||||
|         """Intermediate callback so we can update the keepalive time.""" | ||||
|         stats.APRSDStats().set_aprsis_keepalive() | ||||
|         # Now call the 'real' packet processing for a RX'x packet | ||||
|         self.process_packet(*args, **kwargs) | ||||
| 
 | ||||
| @ -155,7 +152,6 @@ class APRSDProcessPacketThread(APRSDThread): | ||||
|     def __init__(self, packet_queue): | ||||
|         self.packet_queue = packet_queue | ||||
|         super().__init__("ProcessPKT") | ||||
|         self._loop_cnt = 1 | ||||
| 
 | ||||
|     def process_ack_packet(self, packet): | ||||
|         """We got an ack for a message, no need to resend it.""" | ||||
| @ -178,12 +174,11 @@ class APRSDProcessPacketThread(APRSDThread): | ||||
|                 self.process_packet(packet) | ||||
|         except queue.Empty: | ||||
|             pass | ||||
|         self._loop_cnt += 1 | ||||
|         return True | ||||
| 
 | ||||
|     def process_packet(self, packet): | ||||
|         """Process a packet received from aprs-is server.""" | ||||
|         LOG.debug(f"ProcessPKT-LOOP {self._loop_cnt}") | ||||
|         LOG.debug(f"ProcessPKT-LOOP {self.loop_count}") | ||||
|         our_call = CONF.callsign.lower() | ||||
| 
 | ||||
|         from_call = packet.from_call | ||||
|  | ||||
							
								
								
									
										38
									
								
								aprsd/threads/stats.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								aprsd/threads/stats.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| import logging | ||||
| import threading | ||||
| import time | ||||
| 
 | ||||
| from oslo_config import cfg | ||||
| 
 | ||||
| from aprsd.stats import collector | ||||
| from aprsd.threads import APRSDThread | ||||
| from aprsd.utils import objectstore | ||||
| 
 | ||||
| 
 | ||||
| CONF = cfg.CONF | ||||
| LOG = logging.getLogger("APRSD") | ||||
| 
 | ||||
| 
 | ||||
| class StatsStore(objectstore.ObjectStoreMixin): | ||||
|     """Container to save the stats from the collector.""" | ||||
|     lock = threading.Lock() | ||||
| 
 | ||||
| 
 | ||||
| class APRSDStatsStoreThread(APRSDThread): | ||||
|     """Save APRSD Stats to disk periodically.""" | ||||
| 
 | ||||
|     # how often in seconds to write the file | ||||
|     save_interval = 10 | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         super().__init__("StatsStore") | ||||
| 
 | ||||
|     def loop(self): | ||||
|         if self.loop_count % self.save_interval == 0: | ||||
|             stats = collector.Collector().collect() | ||||
|             ss = StatsStore() | ||||
|             ss.data = stats | ||||
|             ss.save() | ||||
| 
 | ||||
|         time.sleep(1) | ||||
|         return True | ||||
| @ -77,7 +77,11 @@ def _send_direct(packet, aprs_client=None): | ||||
| 
 | ||||
|     packet.update_timestamp() | ||||
|     packet_log.log(packet, tx=True) | ||||
|     try: | ||||
|         cl.send(packet) | ||||
|     except Exception as e: | ||||
|         LOG.error(f"Failed to send packet: {packet}") | ||||
|         LOG.error(e) | ||||
| 
 | ||||
| 
 | ||||
| class SendPacketThread(aprsd_threads.APRSDThread): | ||||
| @ -232,7 +236,15 @@ class BeaconSendThread(aprsd_threads.APRSDThread): | ||||
|                 comment="APRSD GPS Beacon", | ||||
|                 symbol=CONF.beacon_symbol, | ||||
|             ) | ||||
|             try: | ||||
|                 # Only send it once | ||||
|                 pkt.retry_count = 1 | ||||
|                 send(pkt, direct=True) | ||||
|             except Exception as e: | ||||
|                 LOG.error(f"Failed to send beacon: {e}") | ||||
|                 client.factory.create().reset() | ||||
|                 time.sleep(5) | ||||
| 
 | ||||
|         self._loop_cnt += 1 | ||||
|         time.sleep(1) | ||||
|         return True | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| """Utilities and helper functions.""" | ||||
| 
 | ||||
| import errno | ||||
| import functools | ||||
| import os | ||||
| import re | ||||
| import sys | ||||
| @ -22,6 +23,17 @@ else: | ||||
|     from collections.abc import MutableMapping | ||||
| 
 | ||||
| 
 | ||||
| def singleton(cls): | ||||
|     """Make a class a Singleton class (only one instance)""" | ||||
|     @functools.wraps(cls) | ||||
|     def wrapper_singleton(*args, **kwargs): | ||||
|         if wrapper_singleton.instance is None: | ||||
|             wrapper_singleton.instance = cls(*args, **kwargs) | ||||
|         return wrapper_singleton.instance | ||||
|     wrapper_singleton.instance = None | ||||
|     return wrapper_singleton | ||||
| 
 | ||||
| 
 | ||||
| def env(*vars, **kwargs): | ||||
|     """This returns the first environment variable set. | ||||
|     if none are non-empty, defaults to '' or keyword arg default | ||||
|  | ||||
| @ -3,6 +3,8 @@ import decimal | ||||
| import json | ||||
| import sys | ||||
| 
 | ||||
| from aprsd.packets import core | ||||
| 
 | ||||
| 
 | ||||
| class EnhancedJSONEncoder(json.JSONEncoder): | ||||
|     def default(self, obj): | ||||
| @ -42,6 +44,24 @@ class EnhancedJSONEncoder(json.JSONEncoder): | ||||
|             return super().default(obj) | ||||
| 
 | ||||
| 
 | ||||
| class SimpleJSONEncoder(json.JSONEncoder): | ||||
|     def default(self, obj): | ||||
|         if isinstance(obj, datetime.datetime): | ||||
|             return obj.isoformat() | ||||
|         elif isinstance(obj, datetime.date): | ||||
|             return str(obj) | ||||
|         elif isinstance(obj, datetime.time): | ||||
|             return str(obj) | ||||
|         elif isinstance(obj, datetime.timedelta): | ||||
|             return str(obj) | ||||
|         elif isinstance(obj, decimal.Decimal): | ||||
|             return str(obj) | ||||
|         elif isinstance(obj, core.Packet): | ||||
|             return obj.to_dict() | ||||
|         else: | ||||
|             return super().default(obj) | ||||
| 
 | ||||
| 
 | ||||
| class EnhancedJSONDecoder(json.JSONDecoder): | ||||
| 
 | ||||
|     def __init__(self, *args, **kwargs): | ||||
|  | ||||
| @ -71,12 +71,13 @@ class ObjectStoreMixin: | ||||
|         if not CONF.enable_save: | ||||
|             return | ||||
|         if len(self) > 0: | ||||
|             save_filename = self._save_filename() | ||||
|             LOG.info( | ||||
|                 f"{self.__class__.__name__}::Saving" | ||||
|                 f" {len(self)} entries to disk at" | ||||
|                 f"{CONF.save_location}", | ||||
|                 f" {len(self)} entries to disk at " | ||||
|                 f"{save_filename}", | ||||
|             ) | ||||
|             with open(self._save_filename(), "wb+") as fp: | ||||
|             with open(save_filename, "wb+") as fp: | ||||
|                 pickle.dump(self._dump(), fp) | ||||
|         else: | ||||
|             LOG.debug( | ||||
|  | ||||
| @ -1,189 +1,4 @@ | ||||
| /* PrismJS 1.24.1 | ||||
| https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+log&plugins=show-language+toolbar */ | ||||
| /** | ||||
|  * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML | ||||
|  * Based on https://github.com/chriskempson/tomorrow-theme | ||||
|  * @author Rose Pritchard | ||||
|  */ | ||||
| 
 | ||||
| code[class*="language-"], | ||||
| pre[class*="language-"] { | ||||
| 	color: #ccc; | ||||
| 	background: none; | ||||
| 	font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; | ||||
| 	font-size: 1em; | ||||
| 	text-align: left; | ||||
| 	white-space: pre; | ||||
| 	word-spacing: normal; | ||||
| 	word-break: normal; | ||||
| 	word-wrap: normal; | ||||
| 	line-height: 1.5; | ||||
| 
 | ||||
| 	-moz-tab-size: 4; | ||||
| 	-o-tab-size: 4; | ||||
| 	tab-size: 4; | ||||
| 
 | ||||
| 	-webkit-hyphens: none; | ||||
| 	-moz-hyphens: none; | ||||
| 	-ms-hyphens: none; | ||||
| 	hyphens: none; | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /* Code blocks */ | ||||
| pre[class*="language-"] { | ||||
| 	padding: 1em; | ||||
| 	margin: .5em 0; | ||||
| 	overflow: auto; | ||||
| } | ||||
| 
 | ||||
| :not(pre) > code[class*="language-"], | ||||
| pre[class*="language-"] { | ||||
| 	background: #2d2d2d; | ||||
| } | ||||
| 
 | ||||
| /* Inline code */ | ||||
| :not(pre) > code[class*="language-"] { | ||||
| 	padding: .1em; | ||||
| 	border-radius: .3em; | ||||
| 	white-space: normal; | ||||
| } | ||||
| 
 | ||||
| .token.comment, | ||||
| .token.block-comment, | ||||
| .token.prolog, | ||||
| .token.doctype, | ||||
| .token.cdata { | ||||
| 	color: #999; | ||||
| } | ||||
| 
 | ||||
| .token.punctuation { | ||||
| 	color: #ccc; | ||||
| } | ||||
| 
 | ||||
| .token.tag, | ||||
| .token.attr-name, | ||||
| .token.namespace, | ||||
| .token.deleted { | ||||
| 	color: #e2777a; | ||||
| } | ||||
| 
 | ||||
| .token.function-name { | ||||
| 	color: #6196cc; | ||||
| } | ||||
| 
 | ||||
| .token.boolean, | ||||
| .token.number, | ||||
| .token.function { | ||||
| 	color: #f08d49; | ||||
| } | ||||
| 
 | ||||
| .token.property, | ||||
| .token.class-name, | ||||
| .token.constant, | ||||
| .token.symbol { | ||||
| 	color: #f8c555; | ||||
| } | ||||
| 
 | ||||
| .token.selector, | ||||
| .token.important, | ||||
| .token.atrule, | ||||
| .token.keyword, | ||||
| .token.builtin { | ||||
| 	color: #cc99cd; | ||||
| } | ||||
| 
 | ||||
| .token.string, | ||||
| .token.char, | ||||
| .token.attr-value, | ||||
| .token.regex, | ||||
| .token.variable { | ||||
| 	color: #7ec699; | ||||
| } | ||||
| 
 | ||||
| .token.operator, | ||||
| .token.entity, | ||||
| .token.url { | ||||
| 	color: #67cdcc; | ||||
| } | ||||
| 
 | ||||
| .token.important, | ||||
| .token.bold { | ||||
| 	font-weight: bold; | ||||
| } | ||||
| .token.italic { | ||||
| 	font-style: italic; | ||||
| } | ||||
| 
 | ||||
| .token.entity { | ||||
| 	cursor: help; | ||||
| } | ||||
| 
 | ||||
| .token.inserted { | ||||
| 	color: green; | ||||
| } | ||||
| 
 | ||||
| div.code-toolbar { | ||||
| 	position: relative; | ||||
| } | ||||
| 
 | ||||
| div.code-toolbar > .toolbar { | ||||
| 	position: absolute; | ||||
| 	top: .3em; | ||||
| 	right: .2em; | ||||
| 	transition: opacity 0.3s ease-in-out; | ||||
| 	opacity: 0; | ||||
| } | ||||
| 
 | ||||
| div.code-toolbar:hover > .toolbar { | ||||
| 	opacity: 1; | ||||
| } | ||||
| 
 | ||||
| /* Separate line b/c rules are thrown out if selector is invalid. | ||||
|    IE11 and old Edge versions don't support :focus-within. */ | ||||
| div.code-toolbar:focus-within > .toolbar { | ||||
| 	opacity: 1; | ||||
| } | ||||
| 
 | ||||
| div.code-toolbar > .toolbar > .toolbar-item { | ||||
| 	display: inline-block; | ||||
| } | ||||
| 
 | ||||
| div.code-toolbar > .toolbar > .toolbar-item > a { | ||||
| 	cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| div.code-toolbar > .toolbar > .toolbar-item > button { | ||||
| 	background: none; | ||||
| 	border: 0; | ||||
| 	color: inherit; | ||||
| 	font: inherit; | ||||
| 	line-height: normal; | ||||
| 	overflow: visible; | ||||
| 	padding: 0; | ||||
| 	-webkit-user-select: none; /* for button */ | ||||
| 	-moz-user-select: none; | ||||
| 	-ms-user-select: none; | ||||
| } | ||||
| 
 | ||||
| div.code-toolbar > .toolbar > .toolbar-item > a, | ||||
| div.code-toolbar > .toolbar > .toolbar-item > button, | ||||
| div.code-toolbar > .toolbar > .toolbar-item > span { | ||||
| 	color: #bbb; | ||||
| 	font-size: .8em; | ||||
| 	padding: 0 .5em; | ||||
| 	background: #f5f2f0; | ||||
| 	background: rgba(224, 224, 224, 0.2); | ||||
| 	box-shadow: 0 2px 0 0 rgba(0,0,0,0.2); | ||||
| 	border-radius: .5em; | ||||
| } | ||||
| 
 | ||||
| div.code-toolbar > .toolbar > .toolbar-item > a:hover, | ||||
| div.code-toolbar > .toolbar > .toolbar-item > a:focus, | ||||
| div.code-toolbar > .toolbar > .toolbar-item > button:hover, | ||||
| div.code-toolbar > .toolbar > .toolbar-item > button:focus, | ||||
| div.code-toolbar > .toolbar > .toolbar-item > span:hover, | ||||
| div.code-toolbar > .toolbar > .toolbar-item > span:focus { | ||||
| 	color: inherit; | ||||
| 	text-decoration: none; | ||||
| } | ||||
| /* PrismJS 1.29.0 | ||||
| https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+json+json5+log&plugins=show-language+toolbar */ | ||||
| code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green} | ||||
| div.code-toolbar{position:relative}div.code-toolbar>.toolbar{position:absolute;z-index:10;top:.3em;right:.2em;transition:opacity .3s ease-in-out;opacity:0}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{color:#bbb;font-size:.8em;padding:0 .5em;background:#f5f2f0;background:rgba(224,224,224,.2);box-shadow:0 2px 0 0 rgba(0,0,0,.2);border-radius:.5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none} | ||||
|  | ||||
| @ -219,15 +219,17 @@ function updateQuadData(chart, label, first, second, third, fourth) { | ||||
| } | ||||
| 
 | ||||
| function update_stats( data ) { | ||||
|     our_callsign = data["stats"]["aprsd"]["callsign"]; | ||||
|     $("#version").text( data["stats"]["aprsd"]["version"] ); | ||||
|     our_callsign = data["APRSDStats"]["callsign"]; | ||||
|     $("#version").text( data["APRSDStats"]["version"] ); | ||||
|     $("#aprs_connection").html( data["aprs_connection"] ); | ||||
|     $("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] ); | ||||
|     $("#uptime").text( "uptime: " + data["APRSDStats"]["uptime"] ); | ||||
|     const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json'); | ||||
|     $("#jsonstats").html(html_pretty); | ||||
|     short_time = data["time"].split(/\s(.+)/)[1]; | ||||
|     updateDualData(packets_chart, short_time, data["stats"]["packets"]["sent"], data["stats"]["packets"]["received"]); | ||||
|     updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["received"], data["stats"]["messages"]["ack_sent"], data["stats"]["messages"]["ack_recieved"]); | ||||
|     updateDualData(email_chart, short_time, data["stats"]["email"]["sent"], data["stats"]["email"]["recieved"]); | ||||
|     updateDualData(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]); | ||||
|     packet_list = data["PacketList"]["packets"]; | ||||
|     updateDualData(packets_chart, short_time, data["PacketList"]["sent"], data["PacketList"]["received"]); | ||||
|     updateQuadData(message_chart, short_time, packet_list["MessagePacket"]["tx"], packet_list["MessagePacket"]["rx"], | ||||
|      packet_list["AckPacket"]["tx"], packet_list["AckPacket"]["rx"]); | ||||
|     updateDualData(email_chart, short_time, data["EmailStats"]["sent"], data["EmailStats"]["recieved"]); | ||||
|     updateDualData(memory_chart, short_time, data["APRSDStats"]["memory_peak"], data["APRSDStats"]["memory_current"]); | ||||
| } | ||||
|  | ||||
| @ -327,7 +327,6 @@ function updatePacketTypesChart() { | ||||
|   option = { | ||||
|       series: series | ||||
|   } | ||||
|   console.log(option) | ||||
|   packet_types_chart.setOption(option); | ||||
| } | ||||
| 
 | ||||
| @ -381,22 +380,23 @@ function updateAcksChart() { | ||||
| } | ||||
| 
 | ||||
| function update_stats( data ) { | ||||
|     console.log(data); | ||||
|     our_callsign = data["stats"]["aprsd"]["callsign"]; | ||||
|     $("#version").text( data["stats"]["aprsd"]["version"] ); | ||||
|     $("#aprs_connection").html( data["aprs_connection"] ); | ||||
|     $("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] ); | ||||
|     console.log("update_stats() echarts.js called") | ||||
|     stats = data["stats"]; | ||||
|     our_callsign = stats["APRSDStats"]["callsign"]; | ||||
|     $("#version").text( stats["APRSDStats"]["version"] ); | ||||
|     $("#aprs_connection").html( stats["aprs_connection"] ); | ||||
|     $("#uptime").text( "uptime: " + stats["APRSDStats"]["uptime"] ); | ||||
|     const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json'); | ||||
|     $("#jsonstats").html(html_pretty); | ||||
| 
 | ||||
|     t = Date.parse(data["time"]); | ||||
|     ts = new Date(t); | ||||
|     updatePacketData(packets_chart, ts, data["stats"]["packets"]["sent"], data["stats"]["packets"]["received"]); | ||||
|     updatePacketTypesData(ts, data["stats"]["packets"]["types"]); | ||||
|     updatePacketData(packets_chart, ts, stats["PacketList"]["tx"], stats["PacketList"]["rx"]); | ||||
|     updatePacketTypesData(ts, stats["PacketList"]["types"]); | ||||
|     updatePacketTypesChart(); | ||||
|     updateMessagesChart(); | ||||
|     updateAcksChart(); | ||||
|     updateMemChart(ts, data["stats"]["aprsd"]["memory_current"], data["stats"]["aprsd"]["memory_peak"]); | ||||
|     updateMemChart(ts, stats["APRSDStats"]["memory_current"], stats["APRSDStats"]["memory_peak"]); | ||||
|     //updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["received"], data["stats"]["messages"]["ack_sent"], data["stats"]["messages"]["ack_recieved"]);
 | ||||
|     //updateDualData(email_chart, short_time, data["stats"]["email"]["sent"], data["stats"]["email"]["recieved"]);
 | ||||
|     //updateDualData(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]);
 | ||||
|  | ||||
| @ -25,10 +25,11 @@ function ord(str){return str.charCodeAt(0);} | ||||
| 
 | ||||
| function update_watchlist( data ) { | ||||
|     // Update the watch list
 | ||||
|     stats = data["stats"]; | ||||
|     var watchdiv = $("#watchDiv"); | ||||
|     var html_str = '<table class="ui celled striped table"><thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th></tr></thead><tbody>' | ||||
|     watchdiv.html('') | ||||
|     jQuery.each(data["stats"]["aprsd"]["watch_list"], function(i, val) { | ||||
|     jQuery.each(stats["WatchList"], function(i, val) { | ||||
|         html_str += '<tr><td class="collapsing"><img id="callsign_'+i+'" class="aprsd_1"></img>' + i + '</td><td>' + val["last"] + '</td></tr>' | ||||
|     }); | ||||
|     html_str += "</tbody></table>"; | ||||
| @ -60,12 +61,13 @@ function update_watchlist_from_packet(callsign, val) { | ||||
| } | ||||
| 
 | ||||
| function update_seenlist( data ) { | ||||
|     stats = data["stats"]; | ||||
|     var seendiv = $("#seenDiv"); | ||||
|     var html_str = '<table class="ui celled striped table">' | ||||
|     html_str    += '<thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th>' | ||||
|     html_str    += '<th>Number of packets RX</th></tr></thead><tbody>' | ||||
|     seendiv.html('') | ||||
|     var seen_list = data["stats"]["aprsd"]["seen_list"] | ||||
|     var seen_list = stats["SeenList"] | ||||
|     var len = Object.keys(seen_list).length | ||||
|     $('#seen_count').html(len) | ||||
|     jQuery.each(seen_list, function(i, val) { | ||||
| @ -79,6 +81,7 @@ function update_seenlist( data ) { | ||||
| } | ||||
| 
 | ||||
| function update_plugins( data ) { | ||||
|     stats = data["stats"]; | ||||
|     var plugindiv = $("#pluginDiv"); | ||||
|     var html_str = '<table class="ui celled striped table"><thead><tr>' | ||||
|     html_str +=      '<th>Plugin Name</th><th>Plugin Enabled?</th>' | ||||
| @ -87,7 +90,7 @@ function update_plugins( data ) { | ||||
|     html_str +=    '</tr></thead><tbody>' | ||||
|     plugindiv.html('') | ||||
| 
 | ||||
|     var plugins = data["stats"]["plugins"]; | ||||
|     var plugins = stats["PluginManager"]; | ||||
|     var keys = Object.keys(plugins); | ||||
|     keys.sort(); | ||||
|     for (var i=0; i<keys.length; i++) { // now lets iterate in sort order
 | ||||
| @ -107,8 +110,8 @@ function update_packets( data ) { | ||||
|     if (size_dict(packet_list) == 0 && size_dict(data) > 0) { | ||||
|         packetsdiv.html('') | ||||
|     } | ||||
|     jQuery.each(data, function(i, val) { | ||||
|         pkt = JSON.parse(val); | ||||
|     jQuery.each(data.packets, function(i, val) { | ||||
|         pkt = val; | ||||
| 
 | ||||
|         update_watchlist_from_packet(pkt['from_call'], pkt); | ||||
|         if ( packet_list.hasOwnProperty(pkt['timestamp']) == false ) { | ||||
|  | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -1,57 +0,0 @@ | ||||
| /* Root element */ | ||||
| .json-document { | ||||
|   padding: 1em 2em; | ||||
| } | ||||
| 
 | ||||
| /* Syntax highlighting for JSON objects */ | ||||
| ul.json-dict, ol.json-array { | ||||
|   list-style-type: none; | ||||
|   margin: 0 0 0 1px; | ||||
|   border-left: 1px dotted #ccc; | ||||
|   padding-left: 2em; | ||||
| } | ||||
| .json-string { | ||||
|   color: #0B7500; | ||||
| } | ||||
| .json-literal { | ||||
|   color: #1A01CC; | ||||
|   font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| /* Toggle button */ | ||||
| a.json-toggle { | ||||
|   position: relative; | ||||
|   color: inherit; | ||||
|   text-decoration: none; | ||||
| } | ||||
| a.json-toggle:focus { | ||||
|   outline: none; | ||||
| } | ||||
| a.json-toggle:before { | ||||
|   font-size: 1.1em; | ||||
|   color: #c0c0c0; | ||||
|   content: "\25BC"; /* down arrow */ | ||||
|   position: absolute; | ||||
|   display: inline-block; | ||||
|   width: 1em; | ||||
|   text-align: center; | ||||
|   line-height: 1em; | ||||
|   left: -1.2em; | ||||
| } | ||||
| a.json-toggle:hover:before { | ||||
|   color: #aaa; | ||||
| } | ||||
| a.json-toggle.collapsed:before { | ||||
|   /* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */ | ||||
|   transform: rotate(-90deg); | ||||
| } | ||||
| 
 | ||||
| /* Collapsable placeholder links */ | ||||
| a.json-placeholder { | ||||
|   color: #aaa; | ||||
|   padding: 0 1em; | ||||
|   text-decoration: none; | ||||
| } | ||||
| a.json-placeholder:hover { | ||||
|   text-decoration: underline; | ||||
| } | ||||
| @ -1,158 +0,0 @@ | ||||
| /** | ||||
|  * jQuery json-viewer | ||||
|  * @author: Alexandre Bodelot <alexandre.bodelot@gmail.com> | ||||
|  * @link: https://github.com/abodelot/jquery.json-viewer
 | ||||
|  */ | ||||
| (function($) { | ||||
| 
 | ||||
|   /** | ||||
|    * Check if arg is either an array with at least 1 element, or a dict with at least 1 key | ||||
|    * @return boolean | ||||
|    */ | ||||
|   function isCollapsable(arg) { | ||||
|     return arg instanceof Object && Object.keys(arg).length > 0; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Check if a string represents a valid url | ||||
|    * @return boolean | ||||
|    */ | ||||
|   function isUrl(string) { | ||||
|     var urlRegexp = /^(https?:\/\/|ftps?:\/\/)?([a-z0-9%-]+\.){1,}([a-z0-9-]+)?(:(\d{1,5}))?(\/([a-z0-9\-._~:/?#[\]@!$&'()*+,;=%]+)?)?$/i; | ||||
|     return urlRegexp.test(string); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Transform a json object into html representation | ||||
|    * @return string | ||||
|    */ | ||||
|   function json2html(json, options) { | ||||
|     var html = ''; | ||||
|     if (typeof json === 'string') { | ||||
|       // Escape tags and quotes
 | ||||
|       json = json | ||||
|         .replace(/&/g, '&') | ||||
|         .replace(/</g, '<') | ||||
|         .replace(/>/g, '>') | ||||
|         .replace(/'/g, ''') | ||||
|         .replace(/"/g, '"'); | ||||
| 
 | ||||
|       if (options.withLinks && isUrl(json)) { | ||||
|         html += '<a href="' + json + '" class="json-string" target="_blank">' + json + '</a>'; | ||||
|       } else { | ||||
|         // Escape double quotes in the rendered non-URL string.
 | ||||
|         json = json.replace(/"/g, '\\"'); | ||||
|         html += '<span class="json-string">"' + json + '"</span>'; | ||||
|       } | ||||
|     } else if (typeof json === 'number') { | ||||
|       html += '<span class="json-literal">' + json + '</span>'; | ||||
|     } else if (typeof json === 'boolean') { | ||||
|       html += '<span class="json-literal">' + json + '</span>'; | ||||
|     } else if (json === null) { | ||||
|       html += '<span class="json-literal">null</span>'; | ||||
|     } else if (json instanceof Array) { | ||||
|       if (json.length > 0) { | ||||
|         html += '[<ol class="json-array">'; | ||||
|         for (var i = 0; i < json.length; ++i) { | ||||
|           html += '<li>'; | ||||
|           // Add toggle button if item is collapsable
 | ||||
|           if (isCollapsable(json[i])) { | ||||
|             html += '<a href class="json-toggle"></a>'; | ||||
|           } | ||||
|           html += json2html(json[i], options); | ||||
|           // Add comma if item is not last
 | ||||
|           if (i < json.length - 1) { | ||||
|             html += ','; | ||||
|           } | ||||
|           html += '</li>'; | ||||
|         } | ||||
|         html += '</ol>]'; | ||||
|       } else { | ||||
|         html += '[]'; | ||||
|       } | ||||
|     } else if (typeof json === 'object') { | ||||
|       var keyCount = Object.keys(json).length; | ||||
|       if (keyCount > 0) { | ||||
|         html += '{<ul class="json-dict">'; | ||||
|         for (var key in json) { | ||||
|           if (Object.prototype.hasOwnProperty.call(json, key)) { | ||||
|             html += '<li>'; | ||||
|             var keyRepr = options.withQuotes ? | ||||
|               '<span class="json-string">"' + key + '"</span>' : key; | ||||
|             // Add toggle button if item is collapsable
 | ||||
|             if (isCollapsable(json[key])) { | ||||
|               html += '<a href class="json-toggle">' + keyRepr + '</a>'; | ||||
|             } else { | ||||
|               html += keyRepr; | ||||
|             } | ||||
|             html += ': ' + json2html(json[key], options); | ||||
|             // Add comma if item is not last
 | ||||
|             if (--keyCount > 0) { | ||||
|               html += ','; | ||||
|             } | ||||
|             html += '</li>'; | ||||
|           } | ||||
|         } | ||||
|         html += '</ul>}'; | ||||
|       } else { | ||||
|         html += '{}'; | ||||
|       } | ||||
|     } | ||||
|     return html; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * jQuery plugin method | ||||
|    * @param json: a javascript object | ||||
|    * @param options: an optional options hash | ||||
|    */ | ||||
|   $.fn.jsonViewer = function(json, options) { | ||||
|     // Merge user options with default options
 | ||||
|     options = Object.assign({}, { | ||||
|       collapsed: false, | ||||
|       rootCollapsable: true, | ||||
|       withQuotes: false, | ||||
|       withLinks: true | ||||
|     }, options); | ||||
| 
 | ||||
|     // jQuery chaining
 | ||||
|     return this.each(function() { | ||||
| 
 | ||||
|       // Transform to HTML
 | ||||
|       var html = json2html(json, options); | ||||
|       if (options.rootCollapsable && isCollapsable(json)) { | ||||
|         html = '<a href class="json-toggle"></a>' + html; | ||||
|       } | ||||
| 
 | ||||
|       // Insert HTML in target DOM element
 | ||||
|       $(this).html(html); | ||||
|       $(this).addClass('json-document'); | ||||
| 
 | ||||
|       // Bind click on toggle buttons
 | ||||
|       $(this).off('click'); | ||||
|       $(this).on('click', 'a.json-toggle', function() { | ||||
|         var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array'); | ||||
|         target.toggle(); | ||||
|         if (target.is(':visible')) { | ||||
|           target.siblings('.json-placeholder').remove(); | ||||
|         } else { | ||||
|           var count = target.children('li').length; | ||||
|           var placeholder = count + (count > 1 ? ' items' : ' item'); | ||||
|           target.after('<a href class="json-placeholder">' + placeholder + '</a>'); | ||||
|         } | ||||
|         return false; | ||||
|       }); | ||||
| 
 | ||||
|       // Simulate click on toggle button when placeholder is clicked
 | ||||
|       $(this).on('click', 'a.json-placeholder', function() { | ||||
|         $(this).siblings('a.json-toggle').click(); | ||||
|         return false; | ||||
|       }); | ||||
| 
 | ||||
|       if (options.collapsed == true) { | ||||
|         // Trigger click to collapse all nodes
 | ||||
|         $(this).find('a.json-toggle').click(); | ||||
|       } | ||||
|     }); | ||||
|   }; | ||||
| })(jQuery); | ||||
| @ -30,7 +30,6 @@ | ||||
|             var color = Chart.helpers.color; | ||||
| 
 | ||||
|             $(document).ready(function() { | ||||
|                 console.log(initial_stats); | ||||
|                 start_update(); | ||||
|                 start_charts(); | ||||
|                 init_messages(); | ||||
| @ -174,7 +173,7 @@ | ||||
| 
 | ||||
|         <div class="ui bottom attached tab segment" data-tab="raw-tab"> | ||||
|           <h3 class="ui dividing header">Raw JSON</h3> | ||||
|           <pre id="jsonstats" class="language-yaml" style="height:600px;overflow-y:auto;">{{ stats|safe }}</pre> | ||||
|           <pre id="jsonstats" class="language-yaml" style="height:600px;overflow-y:auto;">{{ initial_stats|safe }}</pre> | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="ui text container"> | ||||
|  | ||||
| @ -19,9 +19,10 @@ function show_aprs_icon(item, symbol) { | ||||
| function ord(str){return str.charCodeAt(0);} | ||||
| 
 | ||||
| function update_stats( data ) { | ||||
|     $("#version").text( data["stats"]["aprsd"]["version"] ); | ||||
|     console.log(data); | ||||
|     $("#version").text( data["stats"]["APRSDStats"]["version"] ); | ||||
|     $("#aprs_connection").html( data["aprs_connection"] ); | ||||
|     $("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] ); | ||||
|     $("#uptime").text( "uptime: " + data["stats"]["APRSDStats"]["uptime"] ); | ||||
|     short_time = data["time"].split(/\s(.+)/)[1]; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,57 +0,0 @@ | ||||
| /* Root element */ | ||||
| .json-document { | ||||
|   padding: 1em 2em; | ||||
| } | ||||
| 
 | ||||
| /* Syntax highlighting for JSON objects */ | ||||
| ul.json-dict, ol.json-array { | ||||
|   list-style-type: none; | ||||
|   margin: 0 0 0 1px; | ||||
|   border-left: 1px dotted #ccc; | ||||
|   padding-left: 2em; | ||||
| } | ||||
| .json-string { | ||||
|   color: #0B7500; | ||||
| } | ||||
| .json-literal { | ||||
|   color: #1A01CC; | ||||
|   font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| /* Toggle button */ | ||||
| a.json-toggle { | ||||
|   position: relative; | ||||
|   color: inherit; | ||||
|   text-decoration: none; | ||||
| } | ||||
| a.json-toggle:focus { | ||||
|   outline: none; | ||||
| } | ||||
| a.json-toggle:before { | ||||
|   font-size: 1.1em; | ||||
|   color: #c0c0c0; | ||||
|   content: "\25BC"; /* down arrow */ | ||||
|   position: absolute; | ||||
|   display: inline-block; | ||||
|   width: 1em; | ||||
|   text-align: center; | ||||
|   line-height: 1em; | ||||
|   left: -1.2em; | ||||
| } | ||||
| a.json-toggle:hover:before { | ||||
|   color: #aaa; | ||||
| } | ||||
| a.json-toggle.collapsed:before { | ||||
|   /* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */ | ||||
|   transform: rotate(-90deg); | ||||
| } | ||||
| 
 | ||||
| /* Collapsable placeholder links */ | ||||
| a.json-placeholder { | ||||
|   color: #aaa; | ||||
|   padding: 0 1em; | ||||
|   text-decoration: none; | ||||
| } | ||||
| a.json-placeholder:hover { | ||||
|   text-decoration: underline; | ||||
| } | ||||
| @ -1,158 +0,0 @@ | ||||
| /** | ||||
|  * jQuery json-viewer | ||||
|  * @author: Alexandre Bodelot <alexandre.bodelot@gmail.com> | ||||
|  * @link: https://github.com/abodelot/jquery.json-viewer
 | ||||
|  */ | ||||
| (function($) { | ||||
| 
 | ||||
|   /** | ||||
|    * Check if arg is either an array with at least 1 element, or a dict with at least 1 key | ||||
|    * @return boolean | ||||
|    */ | ||||
|   function isCollapsable(arg) { | ||||
|     return arg instanceof Object && Object.keys(arg).length > 0; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Check if a string represents a valid url | ||||
|    * @return boolean | ||||
|    */ | ||||
|   function isUrl(string) { | ||||
|     var urlRegexp = /^(https?:\/\/|ftps?:\/\/)?([a-z0-9%-]+\.){1,}([a-z0-9-]+)?(:(\d{1,5}))?(\/([a-z0-9\-._~:/?#[\]@!$&'()*+,;=%]+)?)?$/i; | ||||
|     return urlRegexp.test(string); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Transform a json object into html representation | ||||
|    * @return string | ||||
|    */ | ||||
|   function json2html(json, options) { | ||||
|     var html = ''; | ||||
|     if (typeof json === 'string') { | ||||
|       // Escape tags and quotes
 | ||||
|       json = json | ||||
|         .replace(/&/g, '&') | ||||
|         .replace(/</g, '<') | ||||
|         .replace(/>/g, '>') | ||||
|         .replace(/'/g, ''') | ||||
|         .replace(/"/g, '"'); | ||||
| 
 | ||||
|       if (options.withLinks && isUrl(json)) { | ||||
|         html += '<a href="' + json + '" class="json-string" target="_blank">' + json + '</a>'; | ||||
|       } else { | ||||
|         // Escape double quotes in the rendered non-URL string.
 | ||||
|         json = json.replace(/"/g, '\\"'); | ||||
|         html += '<span class="json-string">"' + json + '"</span>'; | ||||
|       } | ||||
|     } else if (typeof json === 'number') { | ||||
|       html += '<span class="json-literal">' + json + '</span>'; | ||||
|     } else if (typeof json === 'boolean') { | ||||
|       html += '<span class="json-literal">' + json + '</span>'; | ||||
|     } else if (json === null) { | ||||
|       html += '<span class="json-literal">null</span>'; | ||||
|     } else if (json instanceof Array) { | ||||
|       if (json.length > 0) { | ||||
|         html += '[<ol class="json-array">'; | ||||
|         for (var i = 0; i < json.length; ++i) { | ||||
|           html += '<li>'; | ||||
|           // Add toggle button if item is collapsable
 | ||||
|           if (isCollapsable(json[i])) { | ||||
|             html += '<a href class="json-toggle"></a>'; | ||||
|           } | ||||
|           html += json2html(json[i], options); | ||||
|           // Add comma if item is not last
 | ||||
|           if (i < json.length - 1) { | ||||
|             html += ','; | ||||
|           } | ||||
|           html += '</li>'; | ||||
|         } | ||||
|         html += '</ol>]'; | ||||
|       } else { | ||||
|         html += '[]'; | ||||
|       } | ||||
|     } else if (typeof json === 'object') { | ||||
|       var keyCount = Object.keys(json).length; | ||||
|       if (keyCount > 0) { | ||||
|         html += '{<ul class="json-dict">'; | ||||
|         for (var key in json) { | ||||
|           if (Object.prototype.hasOwnProperty.call(json, key)) { | ||||
|             html += '<li>'; | ||||
|             var keyRepr = options.withQuotes ? | ||||
|               '<span class="json-string">"' + key + '"</span>' : key; | ||||
|             // Add toggle button if item is collapsable
 | ||||
|             if (isCollapsable(json[key])) { | ||||
|               html += '<a href class="json-toggle">' + keyRepr + '</a>'; | ||||
|             } else { | ||||
|               html += keyRepr; | ||||
|             } | ||||
|             html += ': ' + json2html(json[key], options); | ||||
|             // Add comma if item is not last
 | ||||
|             if (--keyCount > 0) { | ||||
|               html += ','; | ||||
|             } | ||||
|             html += '</li>'; | ||||
|           } | ||||
|         } | ||||
|         html += '</ul>}'; | ||||
|       } else { | ||||
|         html += '{}'; | ||||
|       } | ||||
|     } | ||||
|     return html; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * jQuery plugin method | ||||
|    * @param json: a javascript object | ||||
|    * @param options: an optional options hash | ||||
|    */ | ||||
|   $.fn.jsonViewer = function(json, options) { | ||||
|     // Merge user options with default options
 | ||||
|     options = Object.assign({}, { | ||||
|       collapsed: false, | ||||
|       rootCollapsable: true, | ||||
|       withQuotes: false, | ||||
|       withLinks: true | ||||
|     }, options); | ||||
| 
 | ||||
|     // jQuery chaining
 | ||||
|     return this.each(function() { | ||||
| 
 | ||||
|       // Transform to HTML
 | ||||
|       var html = json2html(json, options); | ||||
|       if (options.rootCollapsable && isCollapsable(json)) { | ||||
|         html = '<a href class="json-toggle"></a>' + html; | ||||
|       } | ||||
| 
 | ||||
|       // Insert HTML in target DOM element
 | ||||
|       $(this).html(html); | ||||
|       $(this).addClass('json-document'); | ||||
| 
 | ||||
|       // Bind click on toggle buttons
 | ||||
|       $(this).off('click'); | ||||
|       $(this).on('click', 'a.json-toggle', function() { | ||||
|         var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array'); | ||||
|         target.toggle(); | ||||
|         if (target.is(':visible')) { | ||||
|           target.siblings('.json-placeholder').remove(); | ||||
|         } else { | ||||
|           var count = target.children('li').length; | ||||
|           var placeholder = count + (count > 1 ? ' items' : ' item'); | ||||
|           target.after('<a href class="json-placeholder">' + placeholder + '</a>'); | ||||
|         } | ||||
|         return false; | ||||
|       }); | ||||
| 
 | ||||
|       // Simulate click on toggle button when placeholder is clicked
 | ||||
|       $(this).on('click', 'a.json-placeholder', function() { | ||||
|         $(this).siblings('a.json-toggle').click(); | ||||
|         return false; | ||||
|       }); | ||||
| 
 | ||||
|       if (options.collapsed == true) { | ||||
|         // Trigger click to collapse all nodes
 | ||||
|         $(this).find('a.json-toggle').click(); | ||||
|       } | ||||
|     }); | ||||
|   }; | ||||
| })(jQuery); | ||||
							
								
								
									
										168
									
								
								aprsd/wsgi.py
									
									
									
									
									
								
							
							
						
						
									
										168
									
								
								aprsd/wsgi.py
									
									
									
									
									
								
							| @ -3,10 +3,10 @@ import importlib.metadata as imp | ||||
| import io | ||||
| import json | ||||
| import logging | ||||
| import time | ||||
| import queue | ||||
| 
 | ||||
| import flask | ||||
| from flask import Flask | ||||
| from flask import Flask, request | ||||
| from flask_httpauth import HTTPBasicAuth | ||||
| from oslo_config import cfg, generator | ||||
| import socketio | ||||
| @ -15,11 +15,13 @@ from werkzeug.security import check_password_hash | ||||
| import aprsd | ||||
| from aprsd import cli_helper, client, conf, packets, plugin, threads | ||||
| from aprsd.log import log | ||||
| from aprsd.rpc import client as aprsd_rpc_client | ||||
| from aprsd.threads import stats as stats_threads | ||||
| from aprsd.utils import json as aprsd_json | ||||
| 
 | ||||
| 
 | ||||
| CONF = cfg.CONF | ||||
| LOG = logging.getLogger("gunicorn.access") | ||||
| logging_queue = queue.Queue() | ||||
| 
 | ||||
| auth = HTTPBasicAuth() | ||||
| users: dict[str, str] = {} | ||||
| @ -45,105 +47,26 @@ def verify_password(username, password): | ||||
| 
 | ||||
| 
 | ||||
| def _stats(): | ||||
|     track = aprsd_rpc_client.RPCClient().get_packet_track() | ||||
|     stats_obj = stats_threads.StatsStore() | ||||
|     stats_obj.load() | ||||
|     now = datetime.datetime.now() | ||||
| 
 | ||||
|     time_format = "%m-%d-%Y %H:%M:%S" | ||||
| 
 | ||||
|     stats_dict = aprsd_rpc_client.RPCClient().get_stats_dict() | ||||
|     if not stats_dict: | ||||
|         stats_dict = { | ||||
|             "aprsd": {}, | ||||
|             "aprs-is": {"server": ""}, | ||||
|             "messages": { | ||||
|                 "sent": 0, | ||||
|                 "received": 0, | ||||
|             }, | ||||
|             "email": { | ||||
|                 "sent": 0, | ||||
|                 "received": 0, | ||||
|             }, | ||||
|             "seen_list": { | ||||
|                 "sent": 0, | ||||
|                 "received": 0, | ||||
|             }, | ||||
|         } | ||||
| 
 | ||||
|     # Convert the watch_list entries to age | ||||
|     wl = aprsd_rpc_client.RPCClient().get_watch_list() | ||||
|     new_list = {} | ||||
|     if wl: | ||||
|         for call in wl.get_all(): | ||||
|             # call_date = datetime.datetime.strptime( | ||||
|             #    str(wl.last_seen(call)), | ||||
|             #    "%Y-%m-%d %H:%M:%S.%f", | ||||
|             # ) | ||||
| 
 | ||||
|             # We have to convert the RingBuffer to a real list | ||||
|             # so that json.dumps works. | ||||
|             # pkts = [] | ||||
|             # for pkt in wl.get(call)["packets"].get(): | ||||
|             #     pkts.append(pkt) | ||||
| 
 | ||||
|             new_list[call] = { | ||||
|                 "last": wl.age(call), | ||||
|                 # "packets": pkts | ||||
|             } | ||||
| 
 | ||||
|     stats_dict["aprsd"]["watch_list"] = new_list | ||||
|     packet_list = aprsd_rpc_client.RPCClient().get_packet_list() | ||||
|     rx = tx = 0 | ||||
|     types = {} | ||||
|     if packet_list: | ||||
|         rx = packet_list.total_rx() | ||||
|         tx = packet_list.total_tx() | ||||
|         types_copy = packet_list.types.copy() | ||||
| 
 | ||||
|         for key in types_copy: | ||||
|             types[str(key)] = dict(types_copy[key]) | ||||
| 
 | ||||
|     stats_dict["packets"] = { | ||||
|         "sent": tx, | ||||
|         "received": rx, | ||||
|         "types": types, | ||||
|     } | ||||
|     if track: | ||||
|         size_tracker = len(track) | ||||
|     else: | ||||
|         size_tracker = 0 | ||||
| 
 | ||||
|     result = { | ||||
|     stats = { | ||||
|         "time": now.strftime(time_format), | ||||
|         "size_tracker": size_tracker, | ||||
|         "stats": stats_dict, | ||||
|         "stats": stats_obj.data, | ||||
|     } | ||||
| 
 | ||||
|     return result | ||||
|     return stats | ||||
| 
 | ||||
| 
 | ||||
| @app.route("/stats") | ||||
| def stats(): | ||||
|     LOG.debug("/stats called") | ||||
|     return json.dumps(_stats()) | ||||
|     return json.dumps(_stats(), cls=aprsd_json.SimpleJSONEncoder) | ||||
| 
 | ||||
| 
 | ||||
| @app.route("/") | ||||
| def index(): | ||||
|     stats = _stats() | ||||
|     wl = aprsd_rpc_client.RPCClient().get_watch_list() | ||||
|     if wl and wl.is_enabled(): | ||||
|         watch_count = len(wl) | ||||
|         watch_age = wl.max_delta() | ||||
|     else: | ||||
|         watch_count = 0 | ||||
|         watch_age = 0 | ||||
| 
 | ||||
|     sl = aprsd_rpc_client.RPCClient().get_seen_list() | ||||
|     if sl: | ||||
|         seen_count = len(sl) | ||||
|     else: | ||||
|         seen_count = 0 | ||||
| 
 | ||||
|     pm = plugin.PluginManager() | ||||
|     plugins = pm.get_plugins() | ||||
|     plugin_count = len(plugins) | ||||
| @ -152,7 +75,7 @@ def index(): | ||||
|         transport = "aprs-is" | ||||
|         aprs_connection = ( | ||||
|             "APRS-IS Server: <a href='http://status.aprs2.net' >" | ||||
|             "{}</a>".format(stats["stats"]["aprs-is"]["server"]) | ||||
|             "{}</a>".format(stats["stats"]["APRSClientStats"]["server_string"]) | ||||
|         ) | ||||
|     else: | ||||
|         # We might be connected to a KISS socket? | ||||
| @ -173,13 +96,13 @@ def index(): | ||||
|                     ) | ||||
|                 ) | ||||
| 
 | ||||
|     stats["transport"] = transport | ||||
|     stats["aprs_connection"] = aprs_connection | ||||
|     stats["stats"]["APRSClientStats"]["transport"] = transport | ||||
|     stats["stats"]["APRSClientStats"]["aprs_connection"] = aprs_connection | ||||
|     entries = conf.conf_to_dict() | ||||
| 
 | ||||
|     return flask.render_template( | ||||
|         "index.html", | ||||
|         initial_stats=stats, | ||||
|         initial_stats=json.dumps(stats, cls=aprsd_json.SimpleJSONEncoder), | ||||
|         aprs_connection=aprs_connection, | ||||
|         callsign=CONF.callsign, | ||||
|         version=aprsd.__version__, | ||||
| @ -187,9 +110,6 @@ def index(): | ||||
|             entries, indent=4, | ||||
|             sort_keys=True, default=str, | ||||
|         ), | ||||
|         watch_count=watch_count, | ||||
|         watch_age=watch_age, | ||||
|         seen_count=seen_count, | ||||
|         plugin_count=plugin_count, | ||||
|         # oslo_out=generate_oslo() | ||||
|     ) | ||||
| @ -209,19 +129,10 @@ def messages(): | ||||
| @auth.login_required | ||||
| @app.route("/packets") | ||||
| def get_packets(): | ||||
|     LOG.debug("/packets called") | ||||
|     packet_list = aprsd_rpc_client.RPCClient().get_packet_list() | ||||
|     if packet_list: | ||||
|         tmp_list = [] | ||||
|         pkts = packet_list.copy() | ||||
|         for key in pkts: | ||||
|             pkt = packet_list.get(key) | ||||
|             if pkt: | ||||
|                 tmp_list.append(pkt.json) | ||||
| 
 | ||||
|         return json.dumps(tmp_list) | ||||
|     else: | ||||
|         return json.dumps([]) | ||||
|     stats = _stats() | ||||
|     stats_dict = stats["stats"] | ||||
|     packets = stats_dict.get("PacketList", {}) | ||||
|     return json.dumps(packets, cls=aprsd_json.SimpleJSONEncoder) | ||||
| 
 | ||||
| 
 | ||||
| @auth.login_required | ||||
| @ -273,23 +184,34 @@ def save(): | ||||
|     return json.dumps({"messages": "saved"}) | ||||
| 
 | ||||
| 
 | ||||
| @app.route("/log_entries", methods=["POST"]) | ||||
| def log_entries(): | ||||
|     """The url that the server can call to update the logs.""" | ||||
|     entries = request.json | ||||
|     LOG.info(f"Log entries called {len(entries)}") | ||||
|     for entry in entries: | ||||
|         logging_queue.put(entry) | ||||
|     return json.dumps({"messages": "saved"}) | ||||
| 
 | ||||
| 
 | ||||
| class LogUpdateThread(threads.APRSDThread): | ||||
| 
 | ||||
|     def __init__(self): | ||||
|     def __init__(self, logging_queue=None): | ||||
|         super().__init__("LogUpdate") | ||||
|         self.logging_queue = logging_queue | ||||
| 
 | ||||
|     def loop(self): | ||||
|         if sio: | ||||
|             log_entries = aprsd_rpc_client.RPCClient().get_log_entries() | ||||
| 
 | ||||
|             if log_entries: | ||||
|                 LOG.info(f"Sending log entries! {len(log_entries)}") | ||||
|                 for entry in log_entries: | ||||
|             try: | ||||
|                 log_entry = self.logging_queue.get(block=True, timeout=1) | ||||
|                 if log_entry: | ||||
|                     sio.emit( | ||||
|                         "log_entry", entry, | ||||
|                         "log_entry", | ||||
|                         log_entry, | ||||
|                         namespace="/logs", | ||||
|                     ) | ||||
|         time.sleep(5) | ||||
|             except queue.Empty: | ||||
|                 pass | ||||
|         return True | ||||
| 
 | ||||
| 
 | ||||
| @ -297,17 +219,17 @@ class LoggingNamespace(socketio.Namespace): | ||||
|     log_thread = None | ||||
| 
 | ||||
|     def on_connect(self, sid, environ): | ||||
|         global sio | ||||
|         LOG.debug(f"LOG on_connect {sid}") | ||||
|         global sio, logging_queue | ||||
|         LOG.info(f"LOG on_connect {sid}") | ||||
|         sio.emit( | ||||
|             "connected", {"data": "/logs Connected"}, | ||||
|             namespace="/logs", | ||||
|         ) | ||||
|         self.log_thread = LogUpdateThread() | ||||
|         self.log_thread = LogUpdateThread(logging_queue=logging_queue) | ||||
|         self.log_thread.start() | ||||
| 
 | ||||
|     def on_disconnect(self, sid): | ||||
|         LOG.debug(f"LOG Disconnected {sid}") | ||||
|         LOG.info(f"LOG Disconnected {sid}") | ||||
|         if self.log_thread: | ||||
|             self.log_thread.stop() | ||||
| 
 | ||||
| @ -332,7 +254,7 @@ if __name__ == "__main__": | ||||
|     async_mode = "threading" | ||||
|     sio = socketio.Server(logger=True, async_mode=async_mode) | ||||
|     app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) | ||||
|     log_level = init_app(log_level="DEBUG") | ||||
|     log_level = init_app() | ||||
|     log.setup_logging(log_level) | ||||
|     sio.register_namespace(LoggingNamespace("/logs")) | ||||
|     CONF.log_opt_values(LOG, logging.DEBUG) | ||||
| @ -352,7 +274,7 @@ if __name__ == "uwsgi_file_aprsd_wsgi": | ||||
|     sio = socketio.Server(logger=True, async_mode=async_mode) | ||||
|     app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) | ||||
|     log_level = init_app( | ||||
|         log_level="DEBUG", | ||||
|         # log_level="DEBUG", | ||||
|         config_file="/config/aprsd.conf", | ||||
|         # Commented out for local development. | ||||
|         # config_file=cli_helper.DEFAULT_CONFIG_FILE | ||||
| @ -371,7 +293,7 @@ if __name__ == "aprsd.wsgi": | ||||
|     app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) | ||||
| 
 | ||||
|     log_level = init_app( | ||||
|         log_level="DEBUG", | ||||
|         # log_level="DEBUG", | ||||
|         config_file="/config/aprsd.conf", | ||||
|         # config_file=cli_helper.DEFAULT_CONFIG_FILE, | ||||
|     ) | ||||
|  | ||||
| @ -9,7 +9,7 @@ alabaster==0.7.16         # via sphinx | ||||
| autoflake==1.5.3          # via gray | ||||
| babel==2.14.0             # via sphinx | ||||
| black==24.3.0             # via gray | ||||
| build==1.1.1              # via pip-tools | ||||
| build==1.2.1              # via pip-tools | ||||
| cachetools==5.3.3         # via tox | ||||
| certifi==2024.2.2         # via requests | ||||
| cfgv==3.4.0               # via pre-commit | ||||
| @ -23,7 +23,7 @@ coverage[toml]==7.4.4     # via pytest-cov | ||||
| distlib==0.3.8            # via virtualenv | ||||
| docutils==0.20.1          # via sphinx | ||||
| exceptiongroup==1.2.0     # via pytest | ||||
| filelock==3.13.1          # via tox, virtualenv | ||||
| filelock==3.13.3          # via tox, virtualenv | ||||
| fixit==2.1.0              # via gray | ||||
| flake8==7.0.0             # via -r dev-requirements.in, pep8-naming | ||||
| gray==0.14.0              # via -r dev-requirements.in | ||||
| @ -33,12 +33,12 @@ imagesize==1.4.1          # via sphinx | ||||
| iniconfig==2.0.0          # via pytest | ||||
| isort==5.13.2             # via -r dev-requirements.in, gray | ||||
| jinja2==3.1.3             # via sphinx | ||||
| libcst==1.2.0             # via fixit | ||||
| libcst==1.3.1             # via fixit | ||||
| markupsafe==2.1.5         # via jinja2 | ||||
| mccabe==0.7.0             # via flake8 | ||||
| moreorless==0.4.0         # via fixit | ||||
| mypy==1.9.0               # via -r dev-requirements.in | ||||
| mypy-extensions==1.0.0    # via black, mypy, typing-inspect | ||||
| mypy-extensions==1.0.0    # via black, mypy | ||||
| nodeenv==1.8.0            # via pre-commit | ||||
| packaging==24.0           # via black, build, fixit, pyproject-api, pytest, sphinx, tox | ||||
| pathspec==0.12.1          # via black, trailrunner | ||||
| @ -71,8 +71,7 @@ toml==0.10.2              # via autoflake | ||||
| tomli==2.0.1              # via black, build, coverage, fixit, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox | ||||
| tox==4.14.2               # via -r dev-requirements.in | ||||
| trailrunner==1.4.0        # via fixit | ||||
| typing-extensions==4.10.0  # via black, libcst, mypy, typing-inspect | ||||
| typing-inspect==0.9.0     # via libcst | ||||
| typing-extensions==4.11.0  # via black, mypy | ||||
| unify==0.5                # via gray | ||||
| untokenize==0.1.1         # via unify | ||||
| urllib3==2.2.1            # via requests | ||||
|  | ||||
| @ -29,7 +29,6 @@ kiss3 | ||||
| attrs | ||||
| dataclasses | ||||
| oslo.config | ||||
| rpyc>=6.0.0 | ||||
| # Pin this here so it doesn't require a compile on | ||||
| # raspi | ||||
| shellingham | ||||
|  | ||||
| @ -22,7 +22,7 @@ dataclasses-json==0.6.4   # via -r requirements.in | ||||
| debtcollector==3.0.0      # via oslo-config | ||||
| deprecated==1.2.14        # via click-params | ||||
| dnspython==2.6.1          # via eventlet | ||||
| eventlet==0.36.0          # via -r requirements.in | ||||
| eventlet==0.36.1          # via -r requirements.in | ||||
| flask==3.0.2              # via -r requirements.in, flask-httpauth, flask-socketio | ||||
| flask-httpauth==4.8.0     # via -r requirements.in | ||||
| flask-socketio==5.3.6     # via -r requirements.in | ||||
| @ -47,7 +47,6 @@ oslo-i18n==6.3.0          # via oslo-config | ||||
| packaging==24.0           # via marshmallow | ||||
| pbr==6.0.0                # via -r requirements.in, oslo-i18n, stevedore | ||||
| pluggy==1.4.0             # via -r requirements.in | ||||
| plumbum==1.8.2            # via rpyc | ||||
| pygments==2.17.2          # via rich | ||||
| pyserial==3.5             # via pyserial-asyncio | ||||
| pyserial-asyncio==0.6     # via kiss3 | ||||
| @ -58,7 +57,6 @@ pyyaml==6.0.1             # via -r requirements.in, oslo-config | ||||
| requests==2.31.0          # via -r requirements.in, oslo-config, update-checker | ||||
| rfc3986==2.0.0            # via oslo-config | ||||
| rich==12.6.0              # via -r requirements.in | ||||
| rpyc==6.0.0               # via -r requirements.in | ||||
| rush==2021.4.0            # via -r requirements.in | ||||
| shellingham==1.5.4        # via -r requirements.in, click-completion | ||||
| simple-websocket==1.0.0   # via python-engineio | ||||
| @ -67,12 +65,12 @@ soupsieve==2.5            # via beautifulsoup4 | ||||
| stevedore==5.2.0          # via oslo-config | ||||
| tabulate==0.9.0           # via -r requirements.in | ||||
| thesmuggler==1.0.1        # via -r requirements.in | ||||
| typing-extensions==4.10.0  # via typing-inspect | ||||
| typing-extensions==4.11.0  # via typing-inspect | ||||
| typing-inspect==0.9.0     # via dataclasses-json | ||||
| update-checker==0.18.0    # via -r requirements.in | ||||
| urllib3==2.2.1            # via requests | ||||
| validators==0.22.0        # via click-params | ||||
| werkzeug==3.0.1           # via -r requirements.in, flask | ||||
| werkzeug==3.0.2           # via -r requirements.in, flask | ||||
| wrapt==1.16.0             # via -r requirements.in, debtcollector, deprecated | ||||
| wsproto==1.2.0            # via simple-websocket | ||||
| zipp==3.18.1              # via importlib-metadata | ||||
|  | ||||
| @ -1,54 +0,0 @@ | ||||
| from unittest import mock | ||||
| 
 | ||||
| from oslo_config import cfg | ||||
| 
 | ||||
| from aprsd import packets | ||||
| from aprsd.packets import tracker | ||||
| from aprsd.plugins import query as query_plugin | ||||
| 
 | ||||
| from .. import fake, test_plugin | ||||
| 
 | ||||
| 
 | ||||
| CONF = cfg.CONF | ||||
| 
 | ||||
| 
 | ||||
| class TestQueryPlugin(test_plugin.TestPlugin): | ||||
|     @mock.patch("aprsd.packets.tracker.PacketTrack.flush") | ||||
|     def test_query_flush(self, mock_flush): | ||||
|         packet = fake.fake_packet(message="!delete") | ||||
|         CONF.callsign = fake.FAKE_TO_CALLSIGN | ||||
|         CONF.save_enabled = True | ||||
|         CONF.query_plugin.callsign = fake.FAKE_FROM_CALLSIGN | ||||
|         query = query_plugin.QueryPlugin() | ||||
|         query.enabled = True | ||||
| 
 | ||||
|         expected = "Deleted ALL pending msgs." | ||||
|         actual = query.filter(packet) | ||||
|         mock_flush.assert_called_once() | ||||
|         self.assertEqual(expected, actual) | ||||
| 
 | ||||
|     @mock.patch("aprsd.packets.tracker.PacketTrack.restart_delayed") | ||||
|     def test_query_restart_delayed(self, mock_restart): | ||||
|         CONF.callsign = fake.FAKE_TO_CALLSIGN | ||||
|         CONF.save_enabled = True | ||||
|         CONF.query_plugin.callsign = fake.FAKE_FROM_CALLSIGN | ||||
|         track = tracker.PacketTrack() | ||||
|         track.data = {} | ||||
|         packet = fake.fake_packet(message="!4") | ||||
|         query = query_plugin.QueryPlugin() | ||||
| 
 | ||||
|         expected = "No pending msgs to resend" | ||||
|         actual = query.filter(packet) | ||||
|         mock_restart.assert_not_called() | ||||
|         self.assertEqual(expected, actual) | ||||
|         mock_restart.reset_mock() | ||||
| 
 | ||||
|         # add a message | ||||
|         pkt = packets.MessagePacket( | ||||
|             from_call=self.fromcall, | ||||
|             to_call="testing", | ||||
|             msgNo=self.ack, | ||||
|         ) | ||||
|         track.add(pkt) | ||||
|         actual = query.filter(packet) | ||||
|         mock_restart.assert_called_once() | ||||
| @ -1,3 +1,5 @@ | ||||
| from unittest import mock | ||||
| 
 | ||||
| from oslo_config import cfg | ||||
| 
 | ||||
| import aprsd | ||||
| @ -11,7 +13,9 @@ CONF = cfg.CONF | ||||
| 
 | ||||
| class TestVersionPlugin(test_plugin.TestPlugin): | ||||
| 
 | ||||
|     def test_version(self): | ||||
|     @mock.patch("aprsd.stats.app.APRSDStats.uptime") | ||||
|     def test_version(self, mock_stats): | ||||
|         mock_stats.return_value = "00:00:00" | ||||
|         expected = f"APRSD ver:{aprsd.__version__} uptime:00:00:00" | ||||
|         CONF.callsign = fake.FAKE_TO_CALLSIGN | ||||
|         version = version_plugin.VersionPlugin() | ||||
| @ -31,10 +35,3 @@ class TestVersionPlugin(test_plugin.TestPlugin): | ||||
|         ) | ||||
|         actual = version.filter(packet) | ||||
|         self.assertEqual(expected, actual) | ||||
| 
 | ||||
|         packet = fake.fake_packet( | ||||
|             message="Version", | ||||
|             msg_number=1, | ||||
|         ) | ||||
|         actual = version.filter(packet) | ||||
|         self.assertEqual(expected, actual) | ||||
|  | ||||
| @ -6,7 +6,7 @@ from oslo_config import cfg | ||||
| from aprsd import conf  # noqa: F401 | ||||
| from aprsd import packets | ||||
| from aprsd import plugin as aprsd_plugin | ||||
| from aprsd import plugins, stats | ||||
| from aprsd import plugins | ||||
| from aprsd.packets import core | ||||
| 
 | ||||
| from . import fake | ||||
| @ -89,7 +89,6 @@ class TestPlugin(unittest.TestCase): | ||||
|         self.config_and_init() | ||||
| 
 | ||||
|     def tearDown(self) -> None: | ||||
|         stats.APRSDStats._instance = None | ||||
|         packets.WatchList._instance = None | ||||
|         packets.SeenList._instance = None | ||||
|         packets.PacketTrack._instance = None | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user