aprsd 3.4.3__py3-none-any.whl → 3.4.4__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.
aprsd/client/aprsis.py CHANGED
@@ -23,13 +23,24 @@ class APRSISClient(base.APRSClient):
23
23
  max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
24
24
  self.max_delta = datetime.timedelta(**max_timeout)
25
25
 
26
- def stats(self) -> dict:
26
+ def stats(self, serializable=False) -> dict:
27
27
  stats = {}
28
28
  if self.is_configured():
29
+ if self._client:
30
+ keepalive = self._client.aprsd_keepalive
31
+ server_string = self._client.server_string
32
+ if serializable:
33
+ keepalive = keepalive.isoformat()
34
+ else:
35
+ keepalive = "None"
36
+ server_string = "None"
29
37
  stats = {
30
- "server_string": self._client.server_string,
31
- "sever_keepalive": self._client.aprsd_keepalive,
38
+ "connected": self.is_connected,
32
39
  "filter": self.filter,
40
+ "login_status": self.login_status,
41
+ "connection_keepalive": keepalive,
42
+ "server_string": server_string,
43
+ "transport": self.transport(),
33
44
  }
34
45
 
35
46
  return stats
@@ -99,22 +110,31 @@ class APRSISClient(base.APRSClient):
99
110
  self.connected = False
100
111
  backoff = 1
101
112
  aprs_client = None
113
+ retries = 3
114
+ retry_count = 0
102
115
  while not self.connected:
116
+ retry_count += 1
117
+ if retry_count >= retries:
118
+ break
103
119
  try:
104
120
  LOG.info(f"Creating aprslib client({host}:{port}) and logging in {user}.")
105
121
  aprs_client = aprsis.Aprsdis(user, passwd=password, host=host, port=port)
106
122
  # Force the log to be the same
107
123
  aprs_client.logger = LOG
108
124
  aprs_client.connect()
109
- self.connected = True
125
+ self.connected = self.login_status["success"] = True
126
+ self.login_status["message"] = aprs_client.server_string
110
127
  backoff = 1
111
128
  except LoginError as e:
112
129
  LOG.error(f"Failed to login to APRS-IS Server '{e}'")
113
- self.connected = False
130
+ self.connected = self.login_status["success"] = False
131
+ self.login_status["message"] = e.message
132
+ LOG.error(e.message)
114
133
  time.sleep(backoff)
115
134
  except Exception as e:
116
135
  LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
117
- self.connected = False
136
+ self.connected = self.login_status["success"] = False
137
+ self.login_status["message"] = e.message
118
138
  time.sleep(backoff)
119
139
  # Don't allow the backoff to go to inifinity.
120
140
  if backoff > 5:
@@ -126,7 +146,16 @@ class APRSISClient(base.APRSClient):
126
146
  return aprs_client
127
147
 
128
148
  def consumer(self, callback, blocking=False, immortal=False, raw=False):
129
- self._client.consumer(
130
- callback, blocking=blocking,
131
- immortal=immortal, raw=raw,
132
- )
149
+ if self._client:
150
+ try:
151
+ self._client.consumer(
152
+ callback, blocking=blocking,
153
+ immortal=immortal, raw=raw,
154
+ )
155
+ except Exception as e:
156
+ LOG.error(e)
157
+ LOG.info(e.__cause__)
158
+ raise e
159
+ else:
160
+ LOG.warning("client is None, might be resetting.")
161
+ self.connected = False
aprsd/client/base.py CHANGED
@@ -19,6 +19,10 @@ class APRSClient:
19
19
  _client = None
20
20
 
21
21
  connected = False
22
+ login_status = {
23
+ "success": False,
24
+ "message": None,
25
+ }
22
26
  filter = None
23
27
  lock = threading.Lock()
24
28
 
@@ -32,7 +36,23 @@ class APRSClient:
32
36
 
33
37
  @abc.abstractmethod
34
38
  def stats(self) -> dict:
35
- pass
39
+ """Return statistics about the client connection.
40
+
41
+ Returns:
42
+ dict: Statistics about the connection and packet handling
43
+ """
44
+
45
+ @property
46
+ def is_connected(self):
47
+ return self.connected
48
+
49
+ @property
50
+ def login_success(self):
51
+ return self.login_status.get("success", False)
52
+
53
+ @property
54
+ def login_failure(self):
55
+ return self.login_status["message"]
36
56
 
37
57
  def set_filter(self, filter):
38
58
  self.filter = filter
@@ -46,22 +66,31 @@ class APRSClient:
46
66
  return self._client
47
67
 
48
68
  def _create_client(self):
49
- self._client = self.setup_connection()
50
- if self.filter:
51
- LOG.info("Creating APRS client filter")
52
- self._client.set_filter(self.filter)
69
+ try:
70
+ self._client = self.setup_connection()
71
+ if self.filter:
72
+ LOG.info("Creating APRS client filter")
73
+ self._client.set_filter(self.filter)
74
+ except Exception as e:
75
+ LOG.error(f"Failed to create APRS client: {e}")
76
+ self._client = None
77
+ raise
53
78
 
54
79
  def stop(self):
55
80
  if self._client:
56
81
  LOG.info("Stopping client connection.")
57
82
  self._client.stop()
58
83
 
59
- def send(self, packet: core.Packet):
60
- """Send a packet to the network."""
84
+ def send(self, packet: core.Packet) -> None:
85
+ """Send a packet to the network.
86
+
87
+ Args:
88
+ packet: The APRS packet to send
89
+ """
61
90
  self.client.send(packet)
62
91
 
63
92
  @wrapt.synchronized(lock)
64
- def reset(self):
93
+ def reset(self) -> None:
65
94
  """Call this to force a rebuild/reconnect."""
66
95
  LOG.info("Resetting client connection.")
67
96
  if self._client:
@@ -76,7 +105,11 @@ class APRSClient:
76
105
 
77
106
  @abc.abstractmethod
78
107
  def setup_connection(self):
79
- pass
108
+ """Initialize and return the underlying APRS connection.
109
+
110
+ Returns:
111
+ object: The initialized connection object
112
+ """
80
113
 
81
114
  @staticmethod
82
115
  @abc.abstractmethod
@@ -90,7 +123,11 @@ class APRSClient:
90
123
 
91
124
  @abc.abstractmethod
92
125
  def decode_packet(self, *args, **kwargs):
93
- pass
126
+ """Decode raw APRS packet data into a Packet object.
127
+
128
+ Returns:
129
+ Packet: Decoded APRS packet
130
+ """
94
131
 
95
132
  @abc.abstractmethod
96
133
  def consumer(self, callback, blocking=False, immortal=False, raw=False):
@@ -27,6 +27,9 @@ class Aprsdis(aprslib.IS):
27
27
  # date for last time we heard from the server
28
28
  aprsd_keepalive = datetime.datetime.now()
29
29
 
30
+ # Which server we are connected to?
31
+ server_string = "None"
32
+
30
33
  # timeout in seconds
31
34
  select_timeout = 1
32
35
  lock = threading.Lock()
@@ -193,14 +196,14 @@ class Aprsdis(aprslib.IS):
193
196
  except ParseError as exp:
194
197
  self.logger.log(
195
198
  11,
196
- "%s\n Packet: %s",
199
+ "%s Packet: '%s'",
197
200
  exp,
198
201
  exp.packet,
199
202
  )
200
203
  except UnknownFormat as exp:
201
204
  self.logger.log(
202
205
  9,
203
- "%s\n Packet: %s",
206
+ "%s Packet: '%s'",
204
207
  exp,
205
208
  exp.packet,
206
209
  )
aprsd/client/factory.py CHANGED
@@ -42,6 +42,7 @@ class Client(Protocol):
42
42
  class ClientFactory:
43
43
  _instance = None
44
44
  clients = []
45
+ client = None
45
46
 
46
47
  def __new__(cls, *args, **kwargs):
47
48
  """This magic turns this into a singleton."""
@@ -62,9 +63,13 @@ class ClientFactory:
62
63
  def create(self, key=None):
63
64
  for client in self.clients:
64
65
  if client.is_enabled():
65
- return client()
66
+ self.client = client()
67
+ return self.client
66
68
  raise Exception("No client is configured!!")
67
69
 
70
+ def client_exists(self):
71
+ return bool(self.client)
72
+
68
73
  def is_client_enabled(self):
69
74
  """Make sure at least one client is enabled."""
70
75
  enabled = False
aprsd/client/fake.py CHANGED
@@ -14,8 +14,11 @@ LOG = logging.getLogger("APRSD")
14
14
 
15
15
  class APRSDFakeClient(base.APRSClient, metaclass=trace.TraceWrapperMetaclass):
16
16
 
17
- def stats(self) -> dict:
18
- return {}
17
+ def stats(self, serializable=False) -> dict:
18
+ return {
19
+ "transport": "Fake",
20
+ "connected": True,
21
+ }
19
22
 
20
23
  @staticmethod
21
24
  def is_enabled():
aprsd/client/kiss.py CHANGED
@@ -1,3 +1,4 @@
1
+ import datetime
1
2
  import logging
2
3
 
3
4
  import aprslib
@@ -16,13 +17,24 @@ LOG = logging.getLogger("APRSD")
16
17
  class KISSClient(base.APRSClient):
17
18
 
18
19
  _client = None
20
+ keepalive = datetime.datetime.now()
19
21
 
20
- def stats(self) -> dict:
22
+ def stats(self, serializable=False) -> dict:
21
23
  stats = {}
22
24
  if self.is_configured():
23
- return {
25
+ keepalive = self.keepalive
26
+ if serializable:
27
+ keepalive = keepalive.isoformat()
28
+ stats = {
29
+ "connected": self.is_connected,
30
+ "connection_keepalive": keepalive,
24
31
  "transport": self.transport(),
25
32
  }
33
+ if self.transport() == client.TRANSPORT_TCPKISS:
34
+ stats["host"] = CONF.kiss_tcp.host
35
+ stats["port"] = CONF.kiss_tcp.port
36
+ elif self.transport() == client.TRANSPORT_SERIALKISS:
37
+ stats["device"] = CONF.kiss_serial.device
26
38
  return stats
27
39
 
28
40
  @staticmethod
@@ -95,9 +107,18 @@ class KISSClient(base.APRSClient):
95
107
  return packet
96
108
 
97
109
  def setup_connection(self):
98
- self._client = kiss.KISS3Client()
99
- self.connected = True
110
+ try:
111
+ self._client = kiss.KISS3Client()
112
+ self.connected = self.login_status["success"] = True
113
+ except Exception as ex:
114
+ self.connected = self.login_status["success"] = False
115
+ self.login_status["message"] = str(ex)
100
116
  return self._client
101
117
 
102
118
  def consumer(self, callback, blocking=False, immortal=False, raw=False):
103
- self._client.consumer(callback)
119
+ try:
120
+ self._client.consumer(callback)
121
+ self.keepalive = datetime.datetime.now()
122
+ except Exception as ex:
123
+ LOG.error(f"Consumer failed {ex}")
124
+ LOG.error(ex)
aprsd/client/stats.py CHANGED
@@ -17,22 +17,4 @@ class APRSClientStats:
17
17
 
18
18
  @wrapt.synchronized(lock)
19
19
  def stats(self, serializable=False):
20
- cl = client.client_factory.create()
21
- stats = {
22
- "transport": cl.transport(),
23
- "filter": cl.filter,
24
- "connected": cl.connected,
25
- }
26
-
27
- if cl.transport() == client.TRANSPORT_APRSIS:
28
- stats["server_string"] = cl.client.server_string
29
- keepalive = cl.client.aprsd_keepalive
30
- if serializable:
31
- keepalive = keepalive.isoformat()
32
- stats["server_keepalive"] = keepalive
33
- elif cl.transport() == client.TRANSPORT_TCPKISS:
34
- stats["host"] = CONF.kiss_tcp.host
35
- stats["port"] = CONF.kiss_tcp.port
36
- elif cl.transport() == client.TRANSPORT_SERIALKISS:
37
- stats["device"] = CONF.kiss_serial.device
38
- return stats
20
+ return client.client_factory.create().stats(serializable=serializable)
aprsd/cmds/admin.py ADDED
@@ -0,0 +1,57 @@
1
+ import logging
2
+ import os
3
+ import signal
4
+
5
+ import click
6
+ from oslo_config import cfg
7
+ import socketio
8
+
9
+ import aprsd
10
+ from aprsd import cli_helper
11
+ from aprsd import main as aprsd_main
12
+ from aprsd import utils
13
+ from aprsd.main import cli
14
+
15
+
16
+ os.environ["APRSD_ADMIN_COMMAND"] = "1"
17
+ # this import has to happen AFTER we set the
18
+ # above environment variable, so that the code
19
+ # inside the wsgi.py has the value
20
+ from aprsd import wsgi as aprsd_wsgi # noqa
21
+
22
+
23
+ CONF = cfg.CONF
24
+ LOG = logging.getLogger("APRSD")
25
+
26
+
27
+ # main() ###
28
+ @cli.command()
29
+ @cli_helper.add_options(cli_helper.common_options)
30
+ @click.pass_context
31
+ @cli_helper.process_standard_options
32
+ def admin(ctx):
33
+ """Start the aprsd admin interface."""
34
+ signal.signal(signal.SIGINT, aprsd_main.signal_handler)
35
+ signal.signal(signal.SIGTERM, aprsd_main.signal_handler)
36
+
37
+ level, msg = utils._check_version()
38
+ if level:
39
+ LOG.warning(msg)
40
+ else:
41
+ LOG.info(msg)
42
+ LOG.info(f"APRSD Started version: {aprsd.__version__}")
43
+ # Dump all the config options now.
44
+ CONF.log_opt_values(LOG, logging.DEBUG)
45
+
46
+ async_mode = "threading"
47
+ sio = socketio.Server(logger=True, async_mode=async_mode)
48
+ aprsd_wsgi.app.wsgi_app = socketio.WSGIApp(sio, aprsd_wsgi.app.wsgi_app)
49
+ aprsd_wsgi.init_app()
50
+ sio.register_namespace(aprsd_wsgi.LoggingNamespace("/logs"))
51
+ CONF.log_opt_values(LOG, logging.DEBUG)
52
+ aprsd_wsgi.app.run(
53
+ threaded=True,
54
+ debug=False,
55
+ port=CONF.admin.web_port,
56
+ host=CONF.admin.web_ip,
57
+ )
aprsd/cmds/dev.py CHANGED
@@ -101,7 +101,7 @@ def test_plugin(
101
101
 
102
102
  pm = plugin.PluginManager()
103
103
  if load_all:
104
- pm.setup_plugins()
104
+ pm.setup_plugins(load_help_plugin=CONF.load_help_plugin)
105
105
  obj = pm._create_class(plugin_path, plugin.APRSDPluginBase)
106
106
  if not obj:
107
107
  click.echo(ctx.get_help())
aprsd/cmds/fetch_stats.py CHANGED
@@ -11,6 +11,7 @@ from rich.table import Table
11
11
  import aprsd
12
12
  from aprsd import cli_helper
13
13
  from aprsd.main import cli
14
+ from aprsd.threads.stats import StatsStore
14
15
 
15
16
 
16
17
  # setup the global logger
@@ -154,3 +155,157 @@ def fetch_stats(ctx, host, port):
154
155
  watch_table.add_row(key, value["last"])
155
156
 
156
157
  console.print(watch_table)
158
+
159
+
160
+ @cli.command()
161
+ @cli_helper.add_options(cli_helper.common_options)
162
+ @click.option(
163
+ "--raw",
164
+ is_flag=True,
165
+ default=False,
166
+ help="Dump raw stats instead of formatted output.",
167
+ )
168
+ @click.option(
169
+ "--show-section",
170
+ default=["All"],
171
+ help="Show specific sections of the stats. "
172
+ " Choices: All, APRSDStats, APRSDThreadList, APRSClientStats,"
173
+ " PacketList, SeenList, WatchList",
174
+ multiple=True,
175
+ type=click.Choice(
176
+ [
177
+ "All",
178
+ "APRSDStats",
179
+ "APRSDThreadList",
180
+ "APRSClientStats",
181
+ "PacketList",
182
+ "SeenList",
183
+ "WatchList",
184
+ ],
185
+ case_sensitive=False,
186
+ ),
187
+ )
188
+ @click.pass_context
189
+ @cli_helper.process_standard_options
190
+ def dump_stats(ctx, raw, show_section):
191
+ """Dump the current stats from the running APRSD instance."""
192
+ console = Console()
193
+ console.print(f"APRSD Dump-Stats started version: {aprsd.__version__}")
194
+
195
+ with console.status("Dumping stats"):
196
+ ss = StatsStore()
197
+ ss.load()
198
+ stats = ss.data
199
+ if raw:
200
+ if "All" in show_section:
201
+ console.print(stats)
202
+ return
203
+ else:
204
+ for section in show_section:
205
+ console.print(f"Dumping {section} section:")
206
+ console.print(stats[section])
207
+ return
208
+
209
+ t = Table(title="APRSD Stats")
210
+ t.add_column("Key")
211
+ t.add_column("Value")
212
+ for key, value in stats["APRSDStats"].items():
213
+ t.add_row(key, str(value))
214
+
215
+ if "All" in show_section or "APRSDStats" in show_section:
216
+ console.print(t)
217
+
218
+ # Show the thread list
219
+ t = Table(title="Thread List")
220
+ t.add_column("Name")
221
+ t.add_column("Class")
222
+ t.add_column("Alive?")
223
+ t.add_column("Loop Count")
224
+ t.add_column("Age")
225
+ for name, value in stats["APRSDThreadList"].items():
226
+ t.add_row(
227
+ name,
228
+ value["class"],
229
+ str(value["alive"]),
230
+ str(value["loop_count"]),
231
+ str(value["age"]),
232
+ )
233
+
234
+ if "All" in show_section or "APRSDThreadList" in show_section:
235
+ console.print(t)
236
+
237
+ # Show the plugins
238
+ t = Table(title="Plugin List")
239
+ t.add_column("Name")
240
+ t.add_column("Enabled")
241
+ t.add_column("Version")
242
+ t.add_column("TX")
243
+ t.add_column("RX")
244
+ for name, value in stats["PluginManager"].items():
245
+ t.add_row(
246
+ name,
247
+ str(value["enabled"]),
248
+ value["version"],
249
+ str(value["tx"]),
250
+ str(value["rx"]),
251
+ )
252
+
253
+ if "All" in show_section or "PluginManager" in show_section:
254
+ console.print(t)
255
+
256
+ # Now show the client stats
257
+ t = Table(title="Client Stats")
258
+ t.add_column("Key")
259
+ t.add_column("Value")
260
+ for key, value in stats["APRSClientStats"].items():
261
+ t.add_row(key, str(value))
262
+
263
+ if "All" in show_section or "APRSClientStats" in show_section:
264
+ console.print(t)
265
+
266
+ # now show the packet list
267
+ packet_list = stats.get("PacketList")
268
+ t = Table(title="Packet List")
269
+ t.add_column("Key")
270
+ t.add_column("Value")
271
+ t.add_row("Total Received", str(packet_list["rx"]))
272
+ t.add_row("Total Sent", str(packet_list["tx"]))
273
+
274
+ if "All" in show_section or "PacketList" in show_section:
275
+ console.print(t)
276
+
277
+ # now show the seen list
278
+ seen_list = stats.get("SeenList")
279
+ sorted_seen_list = sorted(
280
+ seen_list.items(),
281
+ )
282
+ t = Table(title="Seen List")
283
+ t.add_column("Callsign")
284
+ t.add_column("Message Count")
285
+ t.add_column("Last Heard")
286
+ for key, value in sorted_seen_list:
287
+ t.add_row(
288
+ key,
289
+ str(value["count"]),
290
+ str(value["last"]),
291
+ )
292
+
293
+ if "All" in show_section or "SeenList" in show_section:
294
+ console.print(t)
295
+
296
+ # now show the watch list
297
+ watch_list = stats.get("WatchList")
298
+ sorted_watch_list = sorted(
299
+ watch_list.items(),
300
+ )
301
+ t = Table(title="Watch List")
302
+ t.add_column("Callsign")
303
+ t.add_column("Last Heard")
304
+ for key, value in sorted_watch_list:
305
+ t.add_row(
306
+ key,
307
+ str(value["last"]),
308
+ )
309
+
310
+ if "All" in show_section or "WatchList" in show_section:
311
+ console.print(t)
aprsd/cmds/healthcheck.py CHANGED
@@ -63,7 +63,7 @@ def healthcheck(ctx, timeout):
63
63
 
64
64
  if email_thread_last_update != "never":
65
65
  d = now - email_thread_last_update
66
- max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
66
+ max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 30}
67
67
  max_delta = datetime.timedelta(**max_timeout)
68
68
  if d > max_delta:
69
69
  console.log(f"Email thread is very old! {d}")
@@ -74,7 +74,7 @@ def healthcheck(ctx, timeout):
74
74
  console.log("No APRSClientStats")
75
75
  sys.exit(-1)
76
76
  else:
77
- aprsis_last_update = client_stats["server_keepalive"]
77
+ aprsis_last_update = client_stats["connection_keepalive"]
78
78
  d = now - aprsis_last_update
79
79
  max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
80
80
  max_delta = datetime.timedelta(**max_timeout)