aprsd 3.3.3__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 (72) 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 +20 -18
  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.3.dist-info → aprsd-3.4.0.dist-info}/METADATA +14 -16
  55. {aprsd-3.3.3.dist-info → aprsd-3.4.0.dist-info}/RECORD +60 -65
  56. {aprsd-3.3.3.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/threads/store.py +0 -30
  64. aprsd/utils/converters.py +0 -15
  65. aprsd/web/admin/static/json-viewer/jquery.json-viewer.css +0 -57
  66. aprsd/web/admin/static/json-viewer/jquery.json-viewer.js +0 -158
  67. aprsd/web/chat/static/json-viewer/jquery.json-viewer.css +0 -57
  68. aprsd/web/chat/static/json-viewer/jquery.json-viewer.js +0 -158
  69. aprsd-3.3.3.dist-info/pbr.json +0 -1
  70. {aprsd-3.3.3.dist-info → aprsd-3.4.0.dist-info}/LICENSE +0 -0
  71. {aprsd-3.3.3.dist-info → aprsd-3.4.0.dist-info}/entry_points.txt +0 -0
  72. {aprsd-3.3.3.dist-info → aprsd-3.4.0.dist-info}/top_level.txt +0 -0
aprsd/packets/log.py ADDED
@@ -0,0 +1,143 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ from loguru import logger
5
+ from oslo_config import cfg
6
+
7
+ from aprsd.packets.core import AckPacket, RejectPacket
8
+
9
+
10
+ LOG = logging.getLogger()
11
+ LOGU = logger
12
+ CONF = cfg.CONF
13
+
14
+ FROM_COLOR = "fg #C70039"
15
+ TO_COLOR = "fg #D033FF"
16
+ TX_COLOR = "red"
17
+ RX_COLOR = "green"
18
+ PACKET_COLOR = "cyan"
19
+
20
+
21
+ def log_multiline(packet, tx: Optional[bool] = False, header: Optional[bool] = True) -> None:
22
+ """LOG a packet to the logfile."""
23
+ if not CONF.enable_packet_logging:
24
+ return
25
+ if CONF.log_packet_format == "compact":
26
+ return
27
+
28
+ # asdict(packet)
29
+ logit = ["\n"]
30
+ name = packet.__class__.__name__
31
+
32
+ if isinstance(packet, AckPacket):
33
+ pkt_max_send_count = CONF.default_ack_send_count
34
+ else:
35
+ pkt_max_send_count = CONF.default_packet_send_count
36
+
37
+ if header:
38
+ if tx:
39
+ header_str = f"<{TX_COLOR}>TX</{TX_COLOR}>"
40
+ logit.append(
41
+ f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}> "
42
+ f"TX:{packet.send_count + 1} of {pkt_max_send_count}",
43
+ )
44
+ else:
45
+ header_str = f"<{RX_COLOR}>RX</{RX_COLOR}>"
46
+ logit.append(
47
+ f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)",
48
+ )
49
+
50
+ else:
51
+ header_str = ""
52
+ logit.append(f"__________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)")
53
+ # log_list.append(f" Packet : {packet.__class__.__name__}")
54
+ if packet.msgNo:
55
+ logit.append(f" Msg # : {packet.msgNo}")
56
+ if packet.from_call:
57
+ logit.append(f" From : <{FROM_COLOR}>{packet.from_call}</{FROM_COLOR}>")
58
+ if packet.to_call:
59
+ logit.append(f" To : <{TO_COLOR}>{packet.to_call}</{TO_COLOR}>")
60
+ if hasattr(packet, "path") and packet.path:
61
+ logit.append(f" Path : {'=>'.join(packet.path)}")
62
+ if hasattr(packet, "via") and packet.via:
63
+ logit.append(f" VIA : {packet.via}")
64
+
65
+ if not isinstance(packet, AckPacket) and not isinstance(packet, RejectPacket):
66
+ msg = packet.human_info
67
+
68
+ if msg:
69
+ msg = msg.replace("<", "\\<")
70
+ logit.append(f" Info : <light-yellow><b>{msg}</b></light-yellow>")
71
+
72
+ if hasattr(packet, "comment") and packet.comment:
73
+ logit.append(f" Comment : {packet.comment}")
74
+
75
+ raw = packet.raw.replace("<", "\\<")
76
+ logit.append(f" Raw : <fg #828282>{raw}</fg #828282>")
77
+ logit.append(f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)")
78
+
79
+ LOGU.opt(colors=True).info("\n".join(logit))
80
+ LOG.debug(repr(packet))
81
+
82
+
83
+ def log(packet, tx: Optional[bool] = False, header: Optional[bool] = True) -> None:
84
+ if not CONF.enable_packet_logging:
85
+ return
86
+ if CONF.log_packet_format == "multiline":
87
+ log_multiline(packet, tx, header)
88
+ return
89
+
90
+ logit = []
91
+ name = packet.__class__.__name__
92
+ if isinstance(packet, AckPacket):
93
+ pkt_max_send_count = CONF.default_ack_send_count
94
+ else:
95
+ pkt_max_send_count = CONF.default_packet_send_count
96
+
97
+ if header:
98
+ if tx:
99
+ via_color = "red"
100
+ arrow = f"<{via_color}>-></{via_color}>"
101
+ logit.append(
102
+ f"<red>TX {arrow}</red> "
103
+ f"<cyan>{name}</cyan>"
104
+ f":{packet.msgNo}"
105
+ f" ({packet.send_count + 1} of {pkt_max_send_count})",
106
+ )
107
+ else:
108
+ via_color = "fg #828282"
109
+ arrow = f"<{via_color}>-></{via_color}>"
110
+ left_arrow = f"<{via_color}><-</{via_color}>"
111
+ logit.append(
112
+ f"<fg #1AA730>RX</fg #1AA730> {left_arrow} "
113
+ f"<cyan>{name}</cyan>"
114
+ f":{packet.msgNo}",
115
+ )
116
+ else:
117
+ via_color = "green"
118
+ arrow = f"<{via_color}>-></{via_color}>"
119
+ logit.append(
120
+ f"<cyan>{name}</cyan>"
121
+ f":{packet.msgNo}",
122
+ )
123
+
124
+ tmp = None
125
+ if packet.path:
126
+ tmp = f"{arrow}".join(packet.path) + f"{arrow} "
127
+
128
+ logit.append(
129
+ f"<{FROM_COLOR}>{packet.from_call}</{FROM_COLOR}> {arrow}"
130
+ f"{tmp if tmp else ' '}"
131
+ f"<{TO_COLOR}>{packet.to_call}</{TO_COLOR}>",
132
+ )
133
+
134
+ if not isinstance(packet, AckPacket) and not isinstance(packet, RejectPacket):
135
+ logit.append(":")
136
+ msg = packet.human_info
137
+
138
+ if msg:
139
+ msg = msg.replace("<", "\\<")
140
+ logit.append(f"<light-yellow><b>{msg}</b></light-yellow>")
141
+
142
+ LOGU.opt(colors=True).info(" ".join(logit))
143
+ log_multiline(packet, tx, header)
@@ -1,99 +1,116 @@
1
1
  from collections import OrderedDict
2
- from collections.abc import MutableMapping
3
2
  import logging
4
- import threading
5
3
 
6
4
  from oslo_config import cfg
7
- import wrapt
8
5
 
9
- from aprsd import stats
10
- from aprsd.packets import seen_list
6
+ from aprsd.packets import collector, core
7
+ from aprsd.utils import objectstore
11
8
 
12
9
 
13
10
  CONF = cfg.CONF
14
11
  LOG = logging.getLogger("APRSD")
15
12
 
16
13
 
17
- class PacketList(MutableMapping):
14
+ class PacketList(objectstore.ObjectStoreMixin):
15
+ """Class to keep track of the packets we tx/rx."""
18
16
  _instance = None
19
- lock = threading.Lock()
20
17
  _total_rx: int = 0
21
18
  _total_tx: int = 0
22
- types = {}
19
+ maxlen: int = 100
23
20
 
24
21
  def __new__(cls, *args, **kwargs):
25
22
  if cls._instance is None:
26
23
  cls._instance = super().__new__(cls)
27
- cls._maxlen = 100
28
- cls.d = OrderedDict()
24
+ cls._instance.maxlen = CONF.packet_list_maxlen
25
+ cls._instance._init_data()
29
26
  return cls._instance
30
27
 
31
- @wrapt.synchronized(lock)
32
- def rx(self, packet):
28
+ def _init_data(self):
29
+ self.data = {
30
+ "types": {},
31
+ "packets": OrderedDict(),
32
+ }
33
+
34
+ def rx(self, packet: type[core.Packet]):
33
35
  """Add a packet that was received."""
34
- self._total_rx += 1
35
- self._add(packet)
36
- ptype = packet.__class__.__name__
37
- if not ptype in self.types:
38
- self.types[ptype] = {"tx": 0, "rx": 0}
39
- self.types[ptype]["rx"] += 1
40
- seen_list.SeenList().update_seen(packet)
41
- stats.APRSDStats().rx(packet)
42
-
43
- @wrapt.synchronized(lock)
44
- def tx(self, packet):
36
+ with self.lock:
37
+ self._total_rx += 1
38
+ self._add(packet)
39
+ ptype = packet.__class__.__name__
40
+ if not ptype in self.data["types"]:
41
+ self.data["types"][ptype] = {"tx": 0, "rx": 0}
42
+ self.data["types"][ptype]["rx"] += 1
43
+
44
+ def tx(self, packet: type[core.Packet]):
45
45
  """Add a packet that was received."""
46
- self._total_tx += 1
47
- self._add(packet)
48
- ptype = packet.__class__.__name__
49
- if not ptype in self.types:
50
- self.types[ptype] = {"tx": 0, "rx": 0}
51
- self.types[ptype]["tx"] += 1
52
- seen_list.SeenList().update_seen(packet)
53
- stats.APRSDStats().tx(packet)
54
-
55
- @wrapt.synchronized(lock)
46
+ with self.lock:
47
+ self._total_tx += 1
48
+ self._add(packet)
49
+ ptype = packet.__class__.__name__
50
+ if not ptype in self.data["types"]:
51
+ self.data["types"][ptype] = {"tx": 0, "rx": 0}
52
+ self.data["types"][ptype]["tx"] += 1
53
+
56
54
  def add(self, packet):
57
- self._add(packet)
55
+ with self.lock:
56
+ self._add(packet)
58
57
 
59
58
  def _add(self, packet):
60
- self[packet.key] = packet
61
-
62
- def copy(self):
63
- return self.d.copy()
64
-
65
- @property
66
- def maxlen(self):
67
- return self._maxlen
59
+ if not self.data.get("packets"):
60
+ self._init_data()
61
+ if packet.key in self.data["packets"]:
62
+ self.data["packets"].move_to_end(packet.key)
63
+ elif len(self.data["packets"]) == self.maxlen:
64
+ self.data["packets"].popitem(last=False)
65
+ self.data["packets"][packet.key] = packet
68
66
 
69
- @wrapt.synchronized(lock)
70
67
  def find(self, packet):
71
- return self.get(packet.key)
72
-
73
- def __getitem__(self, key):
74
- # self.d.move_to_end(key)
75
- return self.d[key]
76
-
77
- def __setitem__(self, key, value):
78
- if key in self.d:
79
- self.d.move_to_end(key)
80
- elif len(self.d) == self.maxlen:
81
- self.d.popitem(last=False)
82
- self.d[key] = value
83
-
84
- def __delitem__(self, key):
85
- del self.d[key]
86
-
87
- def __iter__(self):
88
- return self.d.__iter__()
68
+ with self.lock:
69
+ return self.data["packets"][packet.key]
89
70
 
90
71
  def __len__(self):
91
- return len(self.d)
72
+ with self.lock:
73
+ return len(self.data["packets"])
92
74
 
93
- @wrapt.synchronized(lock)
94
75
  def total_rx(self):
95
- return self._total_rx
76
+ with self.lock:
77
+ return self._total_rx
96
78
 
97
- @wrapt.synchronized(lock)
98
79
  def total_tx(self):
99
- return self._total_tx
80
+ with self.lock:
81
+ return self._total_tx
82
+
83
+ def stats(self, serializable=False) -> dict:
84
+ # limit the number of packets to return to 50
85
+ with self.lock:
86
+ tmp = OrderedDict(
87
+ reversed(
88
+ list(
89
+ self.data.get("packets", OrderedDict()).items(),
90
+ ),
91
+ ),
92
+ )
93
+ pkts = []
94
+ count = 1
95
+ for packet in tmp:
96
+ pkts.append(tmp[packet])
97
+ count += 1
98
+ if count > CONF.packet_list_stats_maxlen:
99
+ break
100
+
101
+ stats = {
102
+ "total_tracked": self._total_rx + self._total_rx,
103
+ "rx": self._total_rx,
104
+ "tx": self._total_tx,
105
+ "types": self.data.get("types", []),
106
+ "packet_count": len(self.data.get("packets", [])),
107
+ "maxlen": self.maxlen,
108
+ "packets": pkts,
109
+ }
110
+ return stats
111
+
112
+
113
+ # Now register the PacketList with the collector
114
+ # every packet we RX and TX goes through the collector
115
+ # for processing for whatever reason is needed.
116
+ collector.PacketCollector().register(PacketList)
@@ -1,10 +1,9 @@
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
 
6
+ from aprsd.packets import collector, core
8
7
  from aprsd.utils import objectstore
9
8
 
10
9
 
@@ -16,28 +15,40 @@ class SeenList(objectstore.ObjectStoreMixin):
16
15
  """Global callsign seen list."""
17
16
 
18
17
  _instance = None
19
- lock = threading.Lock()
20
18
  data: dict = {}
21
19
 
22
20
  def __new__(cls, *args, **kwargs):
23
21
  if cls._instance is None:
24
22
  cls._instance = super().__new__(cls)
25
- cls._instance._init_store()
26
23
  cls._instance.data = {}
27
24
  return cls._instance
28
25
 
29
- @wrapt.synchronized(lock)
30
- def update_seen(self, packet):
31
- callsign = None
32
- if packet.from_call:
33
- callsign = packet.from_call
34
- else:
35
- LOG.warning(f"Can't find FROM in packet {packet}")
36
- return
37
- if callsign not in self.data:
38
- self.data[callsign] = {
39
- "last": None,
40
- "count": 0,
41
- }
42
- self.data[callsign]["last"] = str(datetime.datetime.now())
43
- self.data[callsign]["count"] += 1
26
+ def stats(self, serializable=False):
27
+ """Return the stats for the PacketTrack class."""
28
+ with self.lock:
29
+ return self.data
30
+
31
+ def rx(self, packet: type[core.Packet]):
32
+ """When we get a packet from the network, update the seen list."""
33
+ with self.lock:
34
+ callsign = None
35
+ if packet.from_call:
36
+ callsign = packet.from_call
37
+ else:
38
+ LOG.warning(f"Can't find FROM in packet {packet}")
39
+ return
40
+ if callsign not in self.data:
41
+ self.data[callsign] = {
42
+ "last": None,
43
+ "count": 0,
44
+ }
45
+ self.data[callsign]["last"] = datetime.datetime.now()
46
+ self.data[callsign]["count"] += 1
47
+
48
+ def tx(self, packet: type[core.Packet]):
49
+ """We don't care about TX packets."""
50
+
51
+
52
+ # Register with the packet collector so we can process the packet
53
+ # when we get it off the client (network)
54
+ collector.PacketCollector().register(SeenList)
aprsd/packets/tracker.py CHANGED
@@ -1,14 +1,14 @@
1
1
  import datetime
2
- import threading
2
+ import logging
3
3
 
4
4
  from oslo_config import cfg
5
- import wrapt
6
5
 
7
- from aprsd.threads import tx
6
+ from aprsd.packets import collector, core
8
7
  from aprsd.utils import objectstore
9
8
 
10
9
 
11
10
  CONF = cfg.CONF
11
+ LOG = logging.getLogger("APRSD")
12
12
 
13
13
 
14
14
  class PacketTrack(objectstore.ObjectStoreMixin):
@@ -26,7 +26,6 @@ class PacketTrack(objectstore.ObjectStoreMixin):
26
26
 
27
27
  _instance = None
28
28
  _start_time = None
29
- lock = threading.Lock()
30
29
 
31
30
  data: dict = {}
32
31
  total_tracked: int = 0
@@ -38,74 +37,73 @@ class PacketTrack(objectstore.ObjectStoreMixin):
38
37
  cls._instance._init_store()
39
38
  return cls._instance
40
39
 
41
- @wrapt.synchronized(lock)
42
40
  def __getitem__(self, name):
43
- return self.data[name]
41
+ with self.lock:
42
+ return self.data[name]
44
43
 
45
- @wrapt.synchronized(lock)
46
44
  def __iter__(self):
47
- return iter(self.data)
45
+ with self.lock:
46
+ return iter(self.data)
48
47
 
49
- @wrapt.synchronized(lock)
50
48
  def keys(self):
51
- return self.data.keys()
49
+ with self.lock:
50
+ return self.data.keys()
52
51
 
53
- @wrapt.synchronized(lock)
54
52
  def items(self):
55
- return self.data.items()
53
+ with self.lock:
54
+ return self.data.items()
56
55
 
57
- @wrapt.synchronized(lock)
58
56
  def values(self):
59
- return self.data.values()
57
+ with self.lock:
58
+ return self.data.values()
59
+
60
+ def stats(self, serializable=False):
61
+ with self.lock:
62
+ stats = {
63
+ "total_tracked": self.total_tracked,
64
+ }
65
+ pkts = {}
66
+ for key in self.data:
67
+ last_send_time = self.data[key].last_send_time
68
+ pkts[key] = {
69
+ "last_send_time": last_send_time,
70
+ "send_count": self.data[key].send_count,
71
+ "retry_count": self.data[key].retry_count,
72
+ "message": self.data[key].raw,
73
+ }
74
+ stats["packets"] = pkts
75
+ return stats
76
+
77
+ def rx(self, packet: type[core.Packet]) -> None:
78
+ """When we get a packet from the network, check if we should remove it."""
79
+ if isinstance(packet, core.AckPacket):
80
+ self._remove(packet.msgNo)
81
+ elif isinstance(packet, core.RejectPacket):
82
+ self._remove(packet.msgNo)
83
+ elif hasattr(packet, "ackMsgNo"):
84
+ # Got a piggyback ack, so remove the original message
85
+ self._remove(packet.ackMsgNo)
86
+
87
+ def tx(self, packet: type[core.Packet]) -> None:
88
+ """Add a packet that was sent."""
89
+ with self.lock:
90
+ key = packet.msgNo
91
+ packet.send_count = 0
92
+ self.data[key] = packet
93
+ self.total_tracked += 1
60
94
 
61
- @wrapt.synchronized(lock)
62
- def __len__(self):
63
- return len(self.data)
95
+ def remove(self, key):
96
+ self._remove(key)
64
97
 
65
- @wrapt.synchronized(lock)
66
- def add(self, packet):
67
- key = packet.msgNo
68
- packet._last_send_attempt = 0
69
- self.data[key] = packet
70
- self.total_tracked += 1
98
+ def _remove(self, key):
99
+ with self.lock:
100
+ try:
101
+ del self.data[key]
102
+ except KeyError:
103
+ pass
71
104
 
72
- @wrapt.synchronized(lock)
73
- def get(self, key):
74
- return self.data.get(key, None)
75
105
 
76
- @wrapt.synchronized(lock)
77
- def remove(self, key):
78
- try:
79
- del self.data[key]
80
- except KeyError:
81
- pass
82
-
83
- def restart(self):
84
- """Walk the list of messages and restart them if any."""
85
- for key in self.data.keys():
86
- pkt = self.data[key]
87
- if pkt._last_send_attempt < pkt.retry_count:
88
- tx.send(pkt)
89
-
90
- def _resend(self, packet):
91
- packet._last_send_attempt = 0
92
- tx.send(packet)
93
-
94
- def restart_delayed(self, count=None, most_recent=True):
95
- """Walk the list of delayed messages and restart them if any."""
96
- if not count:
97
- # Send all the delayed messages
98
- for key in self.data.keys():
99
- pkt = self.data[key]
100
- if pkt._last_send_attempt == pkt._retry_count:
101
- self._resend(pkt)
102
- else:
103
- # They want to resend <count> delayed messages
104
- tmp = sorted(
105
- self.data.items(),
106
- reverse=most_recent,
107
- key=lambda x: x[1].last_send_time,
108
- )
109
- pkt_list = tmp[:count]
110
- for (_key, pkt) in pkt_list:
111
- self._resend(pkt)
106
+ # Now register the PacketList with the collector
107
+ # every packet we RX and TX goes through the collector
108
+ # for processing for whatever reason is needed.
109
+ collector.PacketCollector().register(PacketTrack)