aprsd 3.3.4__py2.py3-none-any.whl → 3.4.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. aprsd/client.py +133 -20
  2. aprsd/clients/aprsis.py +6 -3
  3. aprsd/clients/fake.py +1 -1
  4. aprsd/clients/kiss.py +1 -1
  5. aprsd/cmds/completion.py +13 -27
  6. aprsd/cmds/fetch_stats.py +53 -57
  7. aprsd/cmds/healthcheck.py +32 -30
  8. aprsd/cmds/list_plugins.py +2 -2
  9. aprsd/cmds/listen.py +33 -17
  10. aprsd/cmds/send_message.py +2 -2
  11. aprsd/cmds/server.py +26 -9
  12. aprsd/cmds/webchat.py +34 -29
  13. aprsd/conf/common.py +46 -31
  14. aprsd/log/log.py +28 -6
  15. aprsd/main.py +4 -17
  16. aprsd/packets/__init__.py +3 -2
  17. aprsd/packets/collector.py +56 -0
  18. aprsd/packets/core.py +456 -321
  19. aprsd/packets/log.py +143 -0
  20. aprsd/packets/packet_list.py +83 -66
  21. aprsd/packets/seen_list.py +30 -19
  22. aprsd/packets/tracker.py +60 -62
  23. aprsd/packets/watch_list.py +64 -38
  24. aprsd/plugin.py +41 -16
  25. aprsd/plugins/email.py +35 -7
  26. aprsd/plugins/time.py +3 -2
  27. aprsd/plugins/version.py +4 -5
  28. aprsd/plugins/weather.py +0 -1
  29. aprsd/stats/__init__.py +20 -0
  30. aprsd/stats/app.py +46 -0
  31. aprsd/stats/collector.py +38 -0
  32. aprsd/threads/__init__.py +3 -2
  33. aprsd/threads/aprsd.py +67 -36
  34. aprsd/threads/keep_alive.py +55 -49
  35. aprsd/threads/log_monitor.py +46 -0
  36. aprsd/threads/rx.py +43 -24
  37. aprsd/threads/stats.py +44 -0
  38. aprsd/threads/tx.py +36 -17
  39. aprsd/utils/__init__.py +12 -0
  40. aprsd/utils/counter.py +6 -3
  41. aprsd/utils/json.py +20 -0
  42. aprsd/utils/objectstore.py +22 -17
  43. aprsd/web/admin/static/css/prism.css +4 -189
  44. aprsd/web/admin/static/js/charts.js +9 -7
  45. aprsd/web/admin/static/js/echarts.js +71 -9
  46. aprsd/web/admin/static/js/main.js +47 -6
  47. aprsd/web/admin/static/js/prism.js +11 -2246
  48. aprsd/web/admin/templates/index.html +18 -7
  49. aprsd/web/chat/static/js/gps.js +3 -1
  50. aprsd/web/chat/static/js/main.js +4 -3
  51. aprsd/web/chat/static/js/send-message.js +5 -2
  52. aprsd/web/chat/templates/index.html +1 -0
  53. aprsd/wsgi.py +62 -127
  54. {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/METADATA +14 -16
  55. {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/RECORD +60 -63
  56. {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/WHEEL +1 -1
  57. aprsd-3.4.0.dist-info/pbr.json +1 -0
  58. aprsd/plugins/query.py +0 -81
  59. aprsd/rpc/__init__.py +0 -14
  60. aprsd/rpc/client.py +0 -165
  61. aprsd/rpc/server.py +0 -99
  62. aprsd/stats.py +0 -266
  63. aprsd/web/admin/static/json-viewer/jquery.json-viewer.css +0 -57
  64. aprsd/web/admin/static/json-viewer/jquery.json-viewer.js +0 -158
  65. aprsd/web/chat/static/json-viewer/jquery.json-viewer.css +0 -57
  66. aprsd/web/chat/static/json-viewer/jquery.json-viewer.js +0 -158
  67. aprsd-3.3.4.dist-info/pbr.json +0 -1
  68. {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/LICENSE +0 -0
  69. {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/entry_points.txt +0 -0
  70. {aprsd-3.3.4.dist-info → aprsd-3.4.0.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,10 @@
1
1
  import datetime
2
2
  import logging
3
- import threading
4
3
 
5
4
  from oslo_config import cfg
6
- import wrapt
7
5
 
8
6
  from aprsd import utils
7
+ from aprsd.packets import collector, core
9
8
  from aprsd.utils import objectstore
10
9
 
11
10
 
@@ -17,56 +16,75 @@ class WatchList(objectstore.ObjectStoreMixin):
17
16
  """Global watch list and info for callsigns."""
18
17
 
19
18
  _instance = None
20
- lock = threading.Lock()
21
19
  data = {}
22
20
 
23
21
  def __new__(cls, *args, **kwargs):
24
22
  if cls._instance is None:
25
23
  cls._instance = super().__new__(cls)
26
- cls._instance._init_store()
27
- cls._instance.data = {}
28
24
  return cls._instance
29
25
 
30
- def __init__(self, config=None):
31
- ring_size = CONF.watch_list.packet_keep_count
32
-
33
- if CONF.watch_list.callsigns:
34
- for callsign in CONF.watch_list.callsigns:
35
- call = callsign.replace("*", "")
36
- # FIXME(waboring) - we should fetch the last time we saw
37
- # a beacon from a callsign or some other mechanism to find
38
- # last time a message was seen by aprs-is. For now this
39
- # is all we can do.
40
- self.data[call] = {
41
- "last": datetime.datetime.now(),
42
- "packets": utils.RingBuffer(
43
- ring_size,
44
- ),
26
+ def __init__(self):
27
+ super().__init__()
28
+ self._update_from_conf()
29
+
30
+ def _update_from_conf(self, config=None):
31
+ with self.lock:
32
+ if CONF.watch_list.enabled and CONF.watch_list.callsigns:
33
+ for callsign in CONF.watch_list.callsigns:
34
+ call = callsign.replace("*", "")
35
+ # FIXME(waboring) - we should fetch the last time we saw
36
+ # a beacon from a callsign or some other mechanism to find
37
+ # last time a message was seen by aprs-is. For now this
38
+ # is all we can do.
39
+ if call not in self.data:
40
+ self.data[call] = {
41
+ "last": None,
42
+ "packet": None,
43
+ }
44
+
45
+ def stats(self, serializable=False) -> dict:
46
+ stats = {}
47
+ with self.lock:
48
+ for callsign in self.data:
49
+ stats[callsign] = {
50
+ "last": self.data[callsign]["last"],
51
+ "packet": self.data[callsign]["packet"],
52
+ "age": self.age(callsign),
53
+ "old": self.is_old(callsign),
45
54
  }
55
+ return stats
46
56
 
47
57
  def is_enabled(self):
48
58
  return CONF.watch_list.enabled
49
59
 
50
60
  def callsign_in_watchlist(self, callsign):
51
- return callsign in self.data
61
+ with self.lock:
62
+ return callsign in self.data
63
+
64
+ def rx(self, packet: type[core.Packet]) -> None:
65
+ """Track when we got a packet from the network."""
66
+ callsign = packet.from_call
52
67
 
53
- @wrapt.synchronized(lock)
54
- def update_seen(self, packet):
55
- if packet.addresse:
56
- callsign = packet.addresse
57
- else:
58
- callsign = packet.from_call
59
68
  if self.callsign_in_watchlist(callsign):
60
- self.data[callsign]["last"] = datetime.datetime.now()
61
- self.data[callsign]["packets"].append(packet)
69
+ with self.lock:
70
+ self.data[callsign]["last"] = datetime.datetime.now()
71
+ self.data[callsign]["packet"] = packet
72
+
73
+ def tx(self, packet: type[core.Packet]) -> None:
74
+ """We don't care about TX packets."""
62
75
 
63
76
  def last_seen(self, callsign):
64
- if self.callsign_in_watchlist(callsign):
65
- return self.data[callsign]["last"]
77
+ with self.lock:
78
+ if self.callsign_in_watchlist(callsign):
79
+ return self.data[callsign]["last"]
66
80
 
67
81
  def age(self, callsign):
68
82
  now = datetime.datetime.now()
69
- return str(now - self.last_seen(callsign))
83
+ last_seen_time = self.last_seen(callsign)
84
+ if last_seen_time:
85
+ return str(now - last_seen_time)
86
+ else:
87
+ return None
70
88
 
71
89
  def max_delta(self, seconds=None):
72
90
  if not seconds:
@@ -83,14 +101,22 @@ class WatchList(objectstore.ObjectStoreMixin):
83
101
  We put this here so any notification plugin can use this
84
102
  same test.
85
103
  """
86
- age = self.age(callsign)
104
+ if not self.callsign_in_watchlist(callsign):
105
+ return False
87
106
 
88
- delta = utils.parse_delta_str(age)
89
- d = datetime.timedelta(**delta)
107
+ age = self.age(callsign)
108
+ if age:
109
+ delta = utils.parse_delta_str(age)
110
+ d = datetime.timedelta(**delta)
90
111
 
91
- max_delta = self.max_delta(seconds=seconds)
112
+ max_delta = self.max_delta(seconds=seconds)
92
113
 
93
- if d > max_delta:
94
- return True
114
+ if d > max_delta:
115
+ return True
116
+ else:
117
+ return False
95
118
  else:
96
119
  return False
120
+
121
+
122
+ collector.PacketCollector().register(WatchList)
aprsd/plugin.py CHANGED
@@ -1,4 +1,5 @@
1
- # The base plugin class
1
+ from __future__ import annotations
2
+
2
3
  import abc
3
4
  import importlib
4
5
  import inspect
@@ -42,7 +43,7 @@ class APRSDPluginSpec:
42
43
  """A hook specification namespace."""
43
44
 
44
45
  @hookspec
45
- def filter(self, packet: packets.core.Packet):
46
+ def filter(self, packet: type[packets.Packet]):
46
47
  """My special little hook that you can customize."""
47
48
 
48
49
 
@@ -65,7 +66,7 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
65
66
  self.threads = self.create_threads() or []
66
67
  self.start_threads()
67
68
 
68
- def start_threads(self):
69
+ def start_threads(self) -> None:
69
70
  if self.enabled and self.threads:
70
71
  if not isinstance(self.threads, list):
71
72
  self.threads = [self.threads]
@@ -90,10 +91,10 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
90
91
  )
91
92
 
92
93
  @property
93
- def message_count(self):
94
+ def message_count(self) -> int:
94
95
  return self.message_counter
95
96
 
96
- def help(self):
97
+ def help(self) -> str:
97
98
  return "Help!"
98
99
 
99
100
  @abc.abstractmethod
@@ -118,11 +119,11 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
118
119
  thread.stop()
119
120
 
120
121
  @abc.abstractmethod
121
- def filter(self, packet: packets.core.Packet):
122
+ def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket:
122
123
  pass
123
124
 
124
125
  @abc.abstractmethod
125
- def process(self, packet: packets.core.Packet):
126
+ def process(self, packet: type[packets.Packet]):
126
127
  """This is called when the filter passes."""
127
128
 
128
129
 
@@ -154,7 +155,7 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
154
155
  LOG.warning("Watch list enabled, but no callsigns set.")
155
156
 
156
157
  @hookimpl
157
- def filter(self, packet: packets.core.Packet):
158
+ def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket:
158
159
  result = packets.NULL_MESSAGE
159
160
  if self.enabled:
160
161
  wl = watch_list.WatchList()
@@ -206,14 +207,14 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
206
207
  self.enabled = True
207
208
 
208
209
  @hookimpl
209
- def filter(self, packet: packets.core.MessagePacket):
210
- LOG.info(f"{self.__class__.__name__} called")
210
+ def filter(self, packet: packets.MessagePacket) -> str | packets.MessagePacket:
211
+ LOG.debug(f"{self.__class__.__name__} called")
211
212
  if not self.enabled:
212
213
  result = f"{self.__class__.__name__} isn't enabled"
213
214
  LOG.warning(result)
214
215
  return result
215
216
 
216
- if not isinstance(packet, packets.core.MessagePacket):
217
+ if not isinstance(packet, packets.MessagePacket):
217
218
  LOG.warning(f"{self.__class__.__name__} Got a {packet.__class__.__name__} ignoring")
218
219
  return packets.NULL_MESSAGE
219
220
 
@@ -226,7 +227,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
226
227
  # and is an APRS message format and has a message.
227
228
  if (
228
229
  tocall == CONF.callsign
229
- and isinstance(packet, packets.core.MessagePacket)
230
+ and isinstance(packet, packets.MessagePacket)
230
231
  and message
231
232
  ):
232
233
  if re.search(self.command_regex, message, re.IGNORECASE):
@@ -269,7 +270,7 @@ class HelpPlugin(APRSDRegexCommandPluginBase):
269
270
  def help(self):
270
271
  return "Help: send APRS help or help <plugin>"
271
272
 
272
- def process(self, packet: packets.core.MessagePacket):
273
+ def process(self, packet: packets.MessagePacket):
273
274
  LOG.info("HelpPlugin")
274
275
  # fromcall = packet.get("from")
275
276
  message = packet.message_text
@@ -343,6 +344,28 @@ class PluginManager:
343
344
  self._watchlist_pm = pluggy.PluginManager("aprsd")
344
345
  self._watchlist_pm.add_hookspecs(APRSDPluginSpec)
345
346
 
347
+ def stats(self, serializable=False) -> dict:
348
+ """Collect and return stats for all plugins."""
349
+ def full_name_with_qualname(obj):
350
+ return "{}.{}".format(
351
+ obj.__class__.__module__,
352
+ obj.__class__.__qualname__,
353
+ )
354
+
355
+ plugin_stats = {}
356
+ plugins = self.get_plugins()
357
+ if plugins:
358
+
359
+ for p in plugins:
360
+ plugin_stats[full_name_with_qualname(p)] = {
361
+ "enabled": p.enabled,
362
+ "rx": p.rx_count,
363
+ "tx": p.tx_count,
364
+ "version": p.version,
365
+ }
366
+
367
+ return plugin_stats
368
+
346
369
  def is_plugin(self, obj):
347
370
  for c in inspect.getmro(obj):
348
371
  if issubclass(c, APRSDPluginBase):
@@ -368,7 +391,9 @@ class PluginManager:
368
391
  try:
369
392
  module_name, class_name = module_class_string.rsplit(".", 1)
370
393
  module = importlib.import_module(module_name)
371
- module = importlib.reload(module)
394
+ # Commented out because the email thread starts in a different context
395
+ # and hence gives a different singleton for the EmailStats
396
+ # module = importlib.reload(module)
372
397
  except Exception as ex:
373
398
  if not module_name:
374
399
  LOG.error(f"Failed to load Plugin {module_class_string}")
@@ -469,12 +494,12 @@ class PluginManager:
469
494
 
470
495
  LOG.info("Completed Plugin Loading.")
471
496
 
472
- def run(self, packet: packets.core.MessagePacket):
497
+ def run(self, packet: packets.MessagePacket):
473
498
  """Execute all the plugins run method."""
474
499
  with self.lock:
475
500
  return self._pluggy_pm.hook.filter(packet=packet)
476
501
 
477
- def run_watchlist(self, packet: packets.core.Packet):
502
+ def run_watchlist(self, packet: packets.Packet):
478
503
  with self.lock:
479
504
  return self._watchlist_pm.hook.filter(packet=packet)
480
505
 
aprsd/plugins/email.py CHANGED
@@ -11,7 +11,7 @@ import time
11
11
  import imapclient
12
12
  from oslo_config import cfg
13
13
 
14
- from aprsd import packets, plugin, stats, threads
14
+ from aprsd import packets, plugin, threads, utils
15
15
  from aprsd.threads import tx
16
16
  from aprsd.utils import trace
17
17
 
@@ -60,6 +60,38 @@ class EmailInfo:
60
60
  self._delay = val
61
61
 
62
62
 
63
+ @utils.singleton
64
+ class EmailStats:
65
+ """Singleton object to store stats related to email."""
66
+ _instance = None
67
+ tx = 0
68
+ rx = 0
69
+ email_thread_last_time = None
70
+
71
+ def stats(self, serializable=False):
72
+ if CONF.email_plugin.enabled:
73
+ last_check_time = self.email_thread_last_time
74
+ if serializable and last_check_time:
75
+ last_check_time = last_check_time.isoformat()
76
+ stats = {
77
+ "tx": self.tx,
78
+ "rx": self.rx,
79
+ "last_check_time": last_check_time,
80
+ }
81
+ else:
82
+ stats = {}
83
+ return stats
84
+
85
+ def tx_inc(self):
86
+ self.tx += 1
87
+
88
+ def rx_inc(self):
89
+ self.rx += 1
90
+
91
+ def email_thread_update(self):
92
+ self.email_thread_last_time = datetime.datetime.now()
93
+
94
+
63
95
  class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
64
96
  """Email Plugin."""
65
97
 
@@ -190,10 +222,6 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
190
222
  def _imap_connect():
191
223
  imap_port = CONF.email_plugin.imap_port
192
224
  use_ssl = CONF.email_plugin.imap_use_ssl
193
- # host = CONFIG["aprsd"]["email"]["imap"]["host"]
194
- # msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port)
195
- # LOG.debug("Connect to IMAP host {} with user '{}'".
196
- # format(msg, CONFIG['imap']['login']))
197
225
 
198
226
  try:
199
227
  server = imapclient.IMAPClient(
@@ -440,7 +468,7 @@ def send_email(to_addr, content):
440
468
  [to_addr],
441
469
  msg.as_string(),
442
470
  )
443
- stats.APRSDStats().email_tx_inc()
471
+ EmailStats().tx_inc()
444
472
  except Exception:
445
473
  LOG.exception("Sendmail Error!!!!")
446
474
  server.quit()
@@ -545,7 +573,7 @@ class APRSDEmailThread(threads.APRSDThread):
545
573
 
546
574
  def loop(self):
547
575
  time.sleep(5)
548
- stats.APRSDStats().email_thread_update()
576
+ EmailStats().email_thread_update()
549
577
  # always sleep for 5 seconds and see if we need to check email
550
578
  # This allows CTRL-C to stop the execution of this loop sooner
551
579
  # than check_email_delay time
aprsd/plugins/time.py CHANGED
@@ -1,9 +1,9 @@
1
1
  import logging
2
2
  import re
3
- import time
4
3
 
5
4
  from oslo_config import cfg
6
5
  import pytz
6
+ from tzlocal import get_localzone
7
7
 
8
8
  from aprsd import packets, plugin, plugin_utils
9
9
  from aprsd.utils import fuzzy, trace
@@ -22,7 +22,8 @@ class TimePlugin(plugin.APRSDRegexCommandPluginBase):
22
22
  short_description = "What is the current local time."
23
23
 
24
24
  def _get_local_tz(self):
25
- return pytz.timezone(time.strftime("%Z"))
25
+ lz = get_localzone()
26
+ return pytz.timezone(str(lz))
26
27
 
27
28
  def _get_utcnow(self):
28
29
  return pytz.datetime.datetime.utcnow()
aprsd/plugins/version.py CHANGED
@@ -1,7 +1,8 @@
1
1
  import logging
2
2
 
3
3
  import aprsd
4
- from aprsd import plugin, stats
4
+ from aprsd import plugin
5
+ from aprsd.stats import collector
5
6
 
6
7
 
7
8
  LOG = logging.getLogger("APRSD")
@@ -23,10 +24,8 @@ class VersionPlugin(plugin.APRSDRegexCommandPluginBase):
23
24
  # fromcall = packet.get("from")
24
25
  # message = packet.get("message_text", None)
25
26
  # ack = packet.get("msgNo", "0")
26
- stats_obj = stats.APRSDStats()
27
- s = stats_obj.stats()
28
- print(s)
27
+ s = collector.Collector().collect()
29
28
  return "APRSD ver:{} uptime:{}".format(
30
29
  aprsd.__version__,
31
- s["aprsd"]["uptime"],
30
+ s["APRSDStats"]["uptime"],
32
31
  )
aprsd/plugins/weather.py CHANGED
@@ -110,7 +110,6 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
110
110
 
111
111
  @trace.trace
112
112
  def process(self, packet):
113
- print("FISTY")
114
113
  fromcall = packet.get("from")
115
114
  message = packet.get("message_text", None)
116
115
  # ack = packet.get("msgNo", "0")
@@ -0,0 +1,20 @@
1
+ from aprsd import client as aprs_client
2
+ from aprsd import plugin
3
+ from aprsd.packets import packet_list, seen_list, tracker, watch_list
4
+ from aprsd.plugins import email
5
+ from aprsd.stats import app, collector
6
+ from aprsd.threads import aprsd
7
+
8
+
9
+ # Create the collector and register all the objects
10
+ # that APRSD has that implement the stats protocol
11
+ stats_collector = collector.Collector()
12
+ stats_collector.register_producer(app.APRSDStats)
13
+ stats_collector.register_producer(packet_list.PacketList)
14
+ stats_collector.register_producer(watch_list.WatchList)
15
+ stats_collector.register_producer(tracker.PacketTrack)
16
+ stats_collector.register_producer(plugin.PluginManager)
17
+ stats_collector.register_producer(aprsd.APRSDThreadList)
18
+ stats_collector.register_producer(email.EmailStats)
19
+ stats_collector.register_producer(aprs_client.APRSClientStats)
20
+ stats_collector.register_producer(seen_list.SeenList)
aprsd/stats/app.py ADDED
@@ -0,0 +1,46 @@
1
+ import datetime
2
+ import tracemalloc
3
+
4
+ from oslo_config import cfg
5
+
6
+ import aprsd
7
+ from aprsd import utils
8
+
9
+
10
+ CONF = cfg.CONF
11
+
12
+
13
+ class APRSDStats:
14
+ """The AppStats class is used to collect stats from the application."""
15
+
16
+ _instance = None
17
+ start_time = None
18
+
19
+ def __new__(cls, *args, **kwargs):
20
+ """Have to override the new method to make this a singleton
21
+
22
+ instead of using @singletone decorator so the unit tests work.
23
+ """
24
+ if not cls._instance:
25
+ cls._instance = super().__new__(cls)
26
+ cls._instance.start_time = datetime.datetime.now()
27
+ return cls._instance
28
+
29
+ def uptime(self):
30
+ return datetime.datetime.now() - self.start_time
31
+
32
+ def stats(self, serializable=False) -> dict:
33
+ current, peak = tracemalloc.get_traced_memory()
34
+ uptime = self.uptime()
35
+ if serializable:
36
+ uptime = str(uptime)
37
+ stats = {
38
+ "version": aprsd.__version__,
39
+ "uptime": uptime,
40
+ "callsign": CONF.callsign,
41
+ "memory_current": int(current),
42
+ "memory_current_str": utils.human_size(current),
43
+ "memory_peak": int(peak),
44
+ "memory_peak_str": utils.human_size(peak),
45
+ }
46
+ return stats
@@ -0,0 +1,38 @@
1
+ import logging
2
+ from typing import Callable, Protocol, runtime_checkable
3
+
4
+ from aprsd.utils import singleton
5
+
6
+
7
+ LOG = logging.getLogger("APRSD")
8
+
9
+
10
+ @runtime_checkable
11
+ class StatsProducer(Protocol):
12
+ """The StatsProducer protocol is used to define the interface for collecting stats."""
13
+ def stats(self, serializeable=False) -> dict:
14
+ """provide stats in a dictionary format."""
15
+ ...
16
+
17
+
18
+ @singleton
19
+ class Collector:
20
+ """The Collector class is used to collect stats from multiple StatsProducer instances."""
21
+ def __init__(self):
22
+ self.producers: list[Callable] = []
23
+
24
+ def collect(self, serializable=False) -> dict:
25
+ stats = {}
26
+ for name in self.producers:
27
+ cls = name()
28
+ if isinstance(cls, StatsProducer):
29
+ try:
30
+ stats[cls.__class__.__name__] = cls.stats(serializable=serializable).copy()
31
+ except Exception as e:
32
+ LOG.error(f"Error in producer {name} (stats): {e}")
33
+ else:
34
+ raise TypeError(f"{cls} is not an instance of StatsProducer")
35
+ return stats
36
+
37
+ def register_producer(self, producer_name: Callable):
38
+ self.producers.append(producer_name)
aprsd/threads/__init__.py CHANGED
@@ -3,8 +3,9 @@ import queue
3
3
  # Make these available to anyone importing
4
4
  # aprsd.threads
5
5
  from .aprsd import APRSDThread, APRSDThreadList # noqa: F401
6
- from .keep_alive import KeepAliveThread # noqa: F401
7
- from .rx import APRSDRXThread, APRSDDupeRXThread, APRSDProcessPacketThread # noqa: F401
6
+ from .rx import ( # noqa: F401
7
+ APRSDDupeRXThread, APRSDProcessPacketThread, APRSDRXThread,
8
+ )
8
9
 
9
10
 
10
11
  packet_queue = queue.Queue(maxsize=20)
aprsd/threads/aprsd.py CHANGED
@@ -2,6 +2,7 @@ import abc
2
2
  import datetime
3
3
  import logging
4
4
  import threading
5
+ from typing import List
5
6
 
6
7
  import wrapt
7
8
 
@@ -9,43 +10,10 @@ import wrapt
9
10
  LOG = logging.getLogger("APRSD")
10
11
 
11
12
 
12
- class APRSDThreadList:
13
- """Singleton class that keeps track of application wide threads."""
14
-
15
- _instance = None
16
-
17
- threads_list = []
18
- lock = threading.Lock()
19
-
20
- def __new__(cls, *args, **kwargs):
21
- if cls._instance is None:
22
- cls._instance = super().__new__(cls)
23
- cls.threads_list = []
24
- return cls._instance
25
-
26
- @wrapt.synchronized(lock)
27
- def add(self, thread_obj):
28
- self.threads_list.append(thread_obj)
29
-
30
- @wrapt.synchronized(lock)
31
- def remove(self, thread_obj):
32
- self.threads_list.remove(thread_obj)
33
-
34
- @wrapt.synchronized(lock)
35
- def stop_all(self):
36
- """Iterate over all threads and call stop on them."""
37
- for th in self.threads_list:
38
- LOG.info(f"Stopping Thread {th.name}")
39
- if hasattr(th, "packet"):
40
- LOG.info(F"{th.name} packet {th.packet}")
41
- th.stop()
42
-
43
- @wrapt.synchronized(lock)
44
- def __len__(self):
45
- return len(self.threads_list)
46
-
47
-
48
13
  class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
14
+ """Base class for all threads in APRSD."""
15
+
16
+ loop_count = 1
49
17
 
50
18
  def __init__(self, name):
51
19
  super().__init__(name=name)
@@ -79,6 +47,7 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
79
47
  def run(self):
80
48
  LOG.debug("Starting")
81
49
  while not self._should_quit():
50
+ self.loop_count += 1
82
51
  can_loop = self.loop()
83
52
  self._last_loop = datetime.datetime.now()
84
53
  if not can_loop:
@@ -86,3 +55,65 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
86
55
  self._cleanup()
87
56
  APRSDThreadList().remove(self)
88
57
  LOG.debug("Exiting")
58
+
59
+
60
+ class APRSDThreadList:
61
+ """Singleton class that keeps track of application wide threads."""
62
+
63
+ _instance = None
64
+
65
+ threads_list: List[APRSDThread] = []
66
+ lock = threading.Lock()
67
+
68
+ def __new__(cls, *args, **kwargs):
69
+ if cls._instance is None:
70
+ cls._instance = super().__new__(cls)
71
+ cls.threads_list = []
72
+ return cls._instance
73
+
74
+ def stats(self, serializable=False) -> dict:
75
+ stats = {}
76
+ for th in self.threads_list:
77
+ age = th.loop_age()
78
+ if serializable:
79
+ age = str(age)
80
+ stats[th.name] = {
81
+ "name": th.name,
82
+ "class": th.__class__.__name__,
83
+ "alive": th.is_alive(),
84
+ "age": th.loop_age(),
85
+ "loop_count": th.loop_count,
86
+ }
87
+ return stats
88
+
89
+ @wrapt.synchronized(lock)
90
+ def add(self, thread_obj):
91
+ self.threads_list.append(thread_obj)
92
+
93
+ @wrapt.synchronized(lock)
94
+ def remove(self, thread_obj):
95
+ self.threads_list.remove(thread_obj)
96
+
97
+ @wrapt.synchronized(lock)
98
+ def stop_all(self):
99
+ """Iterate over all threads and call stop on them."""
100
+ for th in self.threads_list:
101
+ LOG.info(f"Stopping Thread {th.name}")
102
+ if hasattr(th, "packet"):
103
+ LOG.info(F"{th.name} packet {th.packet}")
104
+ th.stop()
105
+
106
+ @wrapt.synchronized(lock)
107
+ def info(self):
108
+ """Go through all the threads and collect info about each."""
109
+ info = {}
110
+ for thread in self.threads_list:
111
+ alive = thread.is_alive()
112
+ age = thread.loop_age()
113
+ key = thread.__class__.__name__
114
+ info[key] = {"alive": True if alive else False, "age": age, "name": thread.name}
115
+ return info
116
+
117
+ @wrapt.synchronized(lock)
118
+ def __len__(self):
119
+ return len(self.threads_list)