aprsd 4.1.2__py3-none-any.whl → 4.2.0__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/__init__.py CHANGED
@@ -1,13 +1,5 @@
1
- from aprsd.client import aprsis, factory, fake, kiss
2
-
3
-
4
- TRANSPORT_APRSIS = "aprsis"
5
- TRANSPORT_TCPKISS = "tcpkiss"
6
- TRANSPORT_SERIALKISS = "serialkiss"
7
- TRANSPORT_FAKE = "fake"
8
-
9
-
10
- client_factory = factory.ClientFactory()
11
- client_factory.register(aprsis.APRSISClient)
12
- client_factory.register(kiss.KISSClient)
13
- client_factory.register(fake.APRSDFakeClient)
1
+ # define the client transports here
2
+ TRANSPORT_APRSIS = 'aprsis'
3
+ TRANSPORT_TCPKISS = 'tcpkiss'
4
+ TRANSPORT_SERIALKISS = 'serialkiss'
5
+ TRANSPORT_FAKE = 'fake'
aprsd/client/client.py ADDED
@@ -0,0 +1,141 @@
1
+ import logging
2
+ import threading
3
+ from typing import Callable
4
+
5
+ import timeago
6
+ import wrapt
7
+ from loguru import logger
8
+ from oslo_config import cfg
9
+
10
+ from aprsd.client import drivers # noqa - ensure drivers are registered
11
+ from aprsd.client.drivers.registry import DriverRegistry
12
+ from aprsd.packets import core
13
+ from aprsd.utils import keepalive_collector
14
+
15
+ CONF = cfg.CONF
16
+ LOG = logging.getLogger('APRSD')
17
+ LOGU = logger
18
+
19
+
20
+ class APRSDClient:
21
+ """APRSD client class.
22
+
23
+ This is a singleton class that provides a single instance of the APRSD client.
24
+ It is responsible for connecting to the appropriate APRSD client driver based on
25
+ the configuration.
26
+
27
+ """
28
+
29
+ _instance = None
30
+ driver = None
31
+ lock = threading.Lock()
32
+ filter = None
33
+
34
+ def __new__(cls, *args, **kwargs):
35
+ """This magic turns this into a singleton."""
36
+ if cls._instance is None:
37
+ cls._instance = super().__new__(cls)
38
+ keepalive_collector.KeepAliveCollector().register(cls)
39
+ return cls._instance
40
+
41
+ def __init__(self):
42
+ self.connected = False
43
+ self.login_status = {
44
+ 'success': False,
45
+ 'message': None,
46
+ }
47
+ if not self.driver:
48
+ self.driver = DriverRegistry().get_driver()
49
+ self.driver.setup_connection()
50
+
51
+ def stats(self, serializable=False) -> dict:
52
+ stats = {}
53
+ if self.driver:
54
+ stats = self.driver.stats(serializable=serializable)
55
+ return stats
56
+
57
+ @property
58
+ def is_enabled(self):
59
+ if not self.driver:
60
+ return False
61
+ return self.driver.is_enabled()
62
+
63
+ @property
64
+ def is_configured(self):
65
+ if not self.driver:
66
+ return False
67
+ return self.driver.is_configured()
68
+
69
+ # @property
70
+ # def is_connected(self):
71
+ # if not self.driver:
72
+ # return False
73
+ # return self.driver.is_connected()
74
+
75
+ @property
76
+ def login_success(self):
77
+ if not self.driver:
78
+ return False
79
+ return self.driver.login_success
80
+
81
+ @property
82
+ def login_failure(self):
83
+ if not self.driver:
84
+ return None
85
+ return self.driver.login_failure
86
+
87
+ def set_filter(self, filter):
88
+ self.filter = filter
89
+ if not self.driver:
90
+ return
91
+ self.driver.set_filter(filter)
92
+
93
+ def get_filter(self):
94
+ if not self.driver:
95
+ return None
96
+ return self.driver.filter
97
+
98
+ def is_alive(self):
99
+ return self.driver.is_alive()
100
+
101
+ def close(self):
102
+ if not self.driver:
103
+ return
104
+ self.driver.close()
105
+
106
+ @wrapt.synchronized(lock)
107
+ def reset(self):
108
+ """Call this to force a rebuild/reconnect."""
109
+ LOG.info('Resetting client connection.')
110
+ if self.driver:
111
+ self.driver.close()
112
+ self.driver.setup_connection()
113
+ if self.filter:
114
+ self.driver.set_filter(self.filter)
115
+ else:
116
+ LOG.warning('Client not initialized, nothing to reset.')
117
+
118
+ def send(self, packet: core.Packet) -> bool:
119
+ return self.driver.send(packet)
120
+
121
+ # For the keepalive collector
122
+ def keepalive_check(self):
123
+ # Don't check the first time through.
124
+ if not self.driver.is_alive and self._checks:
125
+ LOG.warning("Resetting client. It's not alive.")
126
+ self.reset()
127
+ self._checks = True
128
+
129
+ # For the keepalive collector
130
+ def keepalive_log(self):
131
+ if ka := self.driver.keepalive:
132
+ keepalive = timeago.format(ka)
133
+ else:
134
+ keepalive = 'N/A'
135
+ LOGU.opt(colors=True).info(f'<green>Client keepalive {keepalive}</green>')
136
+
137
+ def consumer(self, callback: Callable, raw: bool = False):
138
+ return self.driver.consumer(callback=callback, raw=raw)
139
+
140
+ def decode_packet(self, *args, **kwargs) -> core.Packet:
141
+ return self.driver.decode_packet(*args, **kwargs)
@@ -0,0 +1,10 @@
1
+ # All client drivers must be registered here
2
+ from aprsd.client.drivers.aprsis import APRSISDriver
3
+ from aprsd.client.drivers.fake import APRSDFakeDriver
4
+ from aprsd.client.drivers.registry import DriverRegistry
5
+ from aprsd.client.drivers.tcpkiss import TCPKISSDriver
6
+
7
+ driver_registry = DriverRegistry()
8
+ driver_registry.register(APRSDFakeDriver)
9
+ driver_registry.register(APRSISDriver)
10
+ driver_registry.register(TCPKISSDriver)
@@ -1,286 +1,205 @@
1
1
  import datetime
2
2
  import logging
3
- import select
4
- import socket
5
- import threading
3
+ import time
4
+ from typing import Callable
6
5
 
7
- import aprslib
8
- import wrapt
9
- from aprslib import is_py3
10
- from aprslib.exceptions import (
11
- ConnectionDrop,
12
- ConnectionError,
13
- GenericError,
14
- LoginError,
15
- ParseError,
16
- UnknownFormat,
17
- )
6
+ from aprslib.exceptions import LoginError
7
+ from loguru import logger
8
+ from oslo_config import cfg
18
9
 
19
- import aprsd
10
+ from aprsd import client, exception
11
+ from aprsd.client.drivers.lib.aprslib import APRSLibClient
20
12
  from aprsd.packets import core
21
13
 
14
+ CONF = cfg.CONF
22
15
  LOG = logging.getLogger('APRSD')
16
+ LOGU = logger
23
17
 
24
18
 
25
- class Aprsdis(aprslib.IS):
26
- """Extend the aprslib class so we can exit properly."""
19
+ # class APRSISDriver(metaclass=trace.TraceWrapperMetaclass):
20
+ class APRSISDriver:
21
+ """This is the APRS-IS driver for the APRSD client.
27
22
 
28
- # flag to tell us to stop
29
- thread_stop = False
23
+ This driver uses our modified aprslib.IS class to connect to the APRS-IS server.
30
24
 
31
- # date for last time we heard from the server
32
- aprsd_keepalive = datetime.datetime.now()
25
+ """
33
26
 
34
- # Which server we are connected to?
35
- server_string = 'None'
27
+ _client = None
28
+ _checks = False
36
29
 
37
- # timeout in seconds
38
- select_timeout = 1
39
- lock = threading.Lock()
40
-
41
- def stop(self):
42
- self.thread_stop = True
43
- LOG.warning('Shutdown Aprsdis client.')
44
-
45
- def close(self):
46
- LOG.warning('Closing Aprsdis client.')
47
- super().close()
48
-
49
- @wrapt.synchronized(lock)
50
- def send(self, packet: core.Packet):
51
- """Send an APRS Message object."""
52
- self.sendall(packet.raw)
53
-
54
- def is_alive(self):
55
- """If the connection is alive or not."""
56
- return self._connected
57
-
58
- def _connect(self):
59
- """
60
- Attemps connection to the server
61
- """
62
-
63
- self.logger.info(
64
- 'Attempting connection to %s:%s', self.server[0], self.server[1]
65
- )
30
+ def __init__(self):
31
+ max_timeout = {'hours': 0.0, 'minutes': 2, 'seconds': 0}
32
+ self.max_delta = datetime.timedelta(**max_timeout)
33
+ self.login_status = {
34
+ 'success': False,
35
+ 'message': None,
36
+ }
66
37
 
38
+ @staticmethod
39
+ def is_enabled():
40
+ # Defaults to True if the enabled flag is non existent
67
41
  try:
68
- self._open_socket()
69
-
70
- peer = self.sock.getpeername()
71
-
72
- self.logger.info('Connected to %s', str(peer))
73
-
74
- # 5 second timeout to receive server banner
75
- self.sock.setblocking(1)
76
- self.sock.settimeout(5)
77
-
78
- self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
79
- # MACOS doesn't have TCP_KEEPIDLE
80
- if hasattr(socket, 'TCP_KEEPIDLE'):
81
- self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
82
- self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 3)
83
- self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)
84
-
85
- banner = self.sock.recv(512)
86
- if is_py3:
87
- banner = banner.decode('latin-1')
88
-
89
- if banner[0] == '#':
90
- self.logger.debug('Banner: %s', banner.rstrip())
91
- else:
92
- raise ConnectionError('invalid banner from server')
93
-
94
- except ConnectionError as e:
95
- self.logger.error(str(e))
96
- self.close()
97
- raise
98
- except (socket.error, socket.timeout) as e:
99
- self.close()
100
-
101
- self.logger.error('Socket error: %s' % str(e))
102
- if str(e) == 'timed out':
103
- raise ConnectionError('no banner from server') from e
104
- else:
105
- raise ConnectionError(e) from e
106
-
107
- self._connected = True
108
-
109
- def _socket_readlines(self, blocking=False):
110
- """
111
- Generator for complete lines, received from the server
112
- """
113
- try:
114
- self.sock.setblocking(0)
115
- except OSError as e:
116
- self.logger.error(f'socket error when setblocking(0): {str(e)}')
117
- raise aprslib.ConnectionDrop('connection dropped') from e
42
+ return CONF.aprs_network.enabled
43
+ except KeyError:
44
+ return False
45
+
46
+ @staticmethod
47
+ def is_configured():
48
+ if APRSISDriver.is_enabled():
49
+ # Ensure that the config vars are correctly set
50
+ if not CONF.aprs_network.login:
51
+ LOG.error('Config aprs_network.login not set.')
52
+ raise exception.MissingConfigOptionException(
53
+ 'aprs_network.login is not set.',
54
+ )
55
+ if not CONF.aprs_network.password:
56
+ LOG.error('Config aprs_network.password not set.')
57
+ raise exception.MissingConfigOptionException(
58
+ 'aprs_network.password is not set.',
59
+ )
60
+ if not CONF.aprs_network.host:
61
+ LOG.error('Config aprs_network.host not set.')
62
+ raise exception.MissingConfigOptionException(
63
+ 'aprs_network.host is not set.',
64
+ )
118
65
 
119
- while not self.thread_stop:
120
- short_buf = b''
121
- newline = b'\r\n'
66
+ return True
67
+ return True
122
68
 
123
- # set a select timeout, so we get a chance to exit
124
- # when user hits CTRL-C
125
- readable, writable, exceptional = select.select(
126
- [self.sock],
127
- [],
128
- [],
129
- self.select_timeout,
130
- )
131
- if not readable:
132
- if not blocking:
133
- break
134
- else:
135
- continue
69
+ @property
70
+ def is_alive(self):
71
+ if not self._client:
72
+ LOG.warning(f'APRS_CLIENT {self._client} alive? NO!!!')
73
+ return False
74
+ return self._client.is_alive() and not self._is_stale_connection()
136
75
 
76
+ def close(self):
77
+ if self._client:
78
+ self._client.stop()
79
+ self._client.close()
80
+
81
+ def send(self, packet: core.Packet) -> bool:
82
+ return self._client.send(packet)
83
+
84
+ def setup_connection(self):
85
+ user = CONF.aprs_network.login
86
+ password = CONF.aprs_network.password
87
+ host = CONF.aprs_network.host
88
+ port = CONF.aprs_network.port
89
+ self.connected = False
90
+ backoff = 1
91
+ retries = 3
92
+ retry_count = 0
93
+ while not self.connected:
94
+ retry_count += 1
95
+ if retry_count >= retries:
96
+ break
137
97
  try:
138
- short_buf = self.sock.recv(4096)
139
-
140
- # sock.recv returns empty if the connection drops
141
- if not short_buf:
142
- if not blocking:
143
- # We could just not be blocking, so empty is expected
144
- continue
145
- else:
146
- self.logger.error('socket.recv(): returned empty')
147
- raise aprslib.ConnectionDrop('connection dropped')
148
- except OSError as e:
149
- # self.logger.error("socket error on recv(): %s" % str(e))
150
- if 'Resource temporarily unavailable' in str(e):
151
- if not blocking:
152
- if len(self.buf) == 0:
153
- break
154
-
155
- self.buf += short_buf
156
-
157
- while newline in self.buf:
158
- line, self.buf = self.buf.split(newline, 1)
159
-
160
- yield line
161
-
162
- def _send_login(self):
163
- """
164
- Sends login string to server
165
- """
166
- login_str = 'user {0} pass {1} vers Python-APRSD {3}{2}\r\n'
167
- login_str = login_str.format(
168
- self.callsign,
169
- self.passwd,
170
- (' filter ' + self.filter) if self.filter != '' else '',
171
- aprsd.__version__,
172
- )
173
-
174
- self.logger.debug('Sending login information')
175
-
176
- try:
177
- self._sendall(login_str)
178
- self.sock.settimeout(5)
179
- test = self.sock.recv(len(login_str) + 100)
180
- if is_py3:
181
- test = test.decode('latin-1')
182
- test = test.rstrip()
183
-
184
- self.logger.debug("Server: '%s'", test)
185
-
186
- if not test:
187
- raise LoginError(f"Server Response Empty: '{test}'")
188
-
189
- _, _, callsign, status, e = test.split(' ', 4)
190
- s = e.split(',')
191
- if len(s):
192
- server_string = s[0].replace('server ', '')
193
- else:
194
- server_string = e.replace('server ', '')
195
-
196
- if callsign == '':
197
- raise LoginError('Server responded with empty callsign???')
198
- if callsign != self.callsign:
199
- raise LoginError(f'Server: {test}')
200
- if status != 'verified,' and self.passwd != '-1':
201
- raise LoginError('Password is incorrect')
98
+ LOG.info(
99
+ f'Creating aprslib client({host}:{port}) and logging in {user}.'
100
+ )
101
+ self._client = APRSLibClient(
102
+ user, passwd=password, host=host, port=port
103
+ )
104
+ # Force the log to be the same
105
+ self._client.logger = LOG
106
+ self._client.connect()
107
+ self.connected = self.login_status['success'] = True
108
+ self.login_status['message'] = self._client.server_string
109
+ backoff = 1
110
+ except LoginError as e:
111
+ LOG.error(f"Failed to login to APRS-IS Server '{e}'")
112
+ self.connected = self.login_status['success'] = False
113
+ self.login_status['message'] = (
114
+ e.message if hasattr(e, 'message') else str(e)
115
+ )
116
+ LOG.error(self.login_status['message'])
117
+ time.sleep(backoff)
118
+ except Exception as e:
119
+ LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
120
+ self.connected = self.login_status['success'] = False
121
+ self.login_status['message'] = getattr(e, 'message', str(e))
122
+ time.sleep(backoff)
123
+ # Don't allow the backoff to go to inifinity.
124
+ if backoff > 5:
125
+ backoff = 5
126
+ else:
127
+ backoff += 1
128
+ continue
202
129
 
203
- if self.passwd == '-1':
204
- self.logger.info('Login successful (receive only)')
205
- else:
206
- self.logger.info('Login successful')
130
+ def set_filter(self, filter):
131
+ self._client.set_filter(filter)
207
132
 
208
- self.logger.info(f'Connected to {server_string}')
209
- self.server_string = server_string
133
+ def login_success(self) -> bool:
134
+ return self.login_status.get('success', False)
210
135
 
211
- except LoginError as e:
212
- self.logger.error(str(e))
213
- self.close()
214
- raise
215
- except Exception as e:
216
- self.close()
217
- self.logger.error(f"Failed to login '{e}'")
218
- self.logger.exception(e)
219
- raise LoginError('Failed to login') from e
136
+ def login_failure(self) -> str:
137
+ return self.login_status.get('message', None)
220
138
 
221
- def consumer(self, callback, blocking=True, immortal=False, raw=False):
222
- """
223
- When a position sentence is received, it will be passed to the callback function
139
+ @property
140
+ def filter(self):
141
+ return self._client.filter
224
142
 
225
- blocking: if true (default), runs forever, otherwise will return after one sentence
226
- You can still exit the loop, by raising StopIteration in the callback function
143
+ @property
144
+ def server_string(self):
145
+ return self._client.server_string
227
146
 
228
- immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions
229
- if false (default), consumer will return
147
+ @property
148
+ def keepalive(self):
149
+ return self._client.aprsd_keepalive
230
150
 
231
- raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
232
- """
151
+ def _is_stale_connection(self):
152
+ delta = datetime.datetime.now() - self._client.aprsd_keepalive
153
+ if delta > self.max_delta:
154
+ LOG.error(f'Connection is stale, last heard {delta} ago.')
155
+ return True
156
+ return False
233
157
 
234
- if not self._connected:
235
- raise ConnectionError('not connected to a server')
158
+ @staticmethod
159
+ def transport():
160
+ return client.TRANSPORT_APRSIS
236
161
 
237
- line = b''
162
+ def decode_packet(self, *args, **kwargs):
163
+ """APRS lib already decodes this."""
164
+ return core.factory(args[0])
238
165
 
239
- while True and not self.thread_stop:
166
+ def consumer(self, callback: Callable, raw: bool = False):
167
+ if self._client:
240
168
  try:
241
- for line in self._socket_readlines(blocking):
242
- if line[0:1] != b'#':
243
- self.aprsd_keepalive = datetime.datetime.now()
244
- if raw:
245
- callback(line)
246
- else:
247
- callback(self._parse(line))
248
- else:
249
- self.logger.debug('Server: %s', line.decode('utf8'))
250
- self.aprsd_keepalive = datetime.datetime.now()
251
- except ParseError as exp:
252
- self.logger.log(
253
- 11,
254
- "%s Packet: '%s'",
255
- exp,
256
- exp.packet,
257
- )
258
- except UnknownFormat as exp:
259
- self.logger.log(
260
- 9,
261
- "%s Packet: '%s'",
262
- exp,
263
- exp.packet,
169
+ self._client.consumer(
170
+ callback,
171
+ blocking=False,
172
+ immortal=False,
173
+ raw=raw,
264
174
  )
265
- except LoginError as exp:
266
- self.logger.error('%s: %s', exp.__class__.__name__, exp)
267
- except (KeyboardInterrupt, SystemExit):
268
- raise
269
- except (ConnectionDrop, ConnectionError):
270
- self.close()
271
-
272
- if not immortal:
273
- raise
274
- else:
275
- self.connect(blocking=blocking)
276
- continue
277
- except GenericError:
278
- pass
279
- except StopIteration:
280
- break
281
- except Exception:
282
- self.logger.error('APRS Packet: %s', line)
283
- raise
284
-
285
- if not blocking:
286
- break
175
+ except Exception as e:
176
+ LOG.error(e)
177
+ LOG.info(e.__cause__)
178
+ raise e
179
+ else:
180
+ LOG.warning('client is None, might be resetting.')
181
+ self.connected = False
182
+
183
+ def stats(self, serializable=False) -> dict:
184
+ stats = {}
185
+ if self.is_configured():
186
+ if self._client:
187
+ keepalive = self._client.aprsd_keepalive
188
+ server_string = self._client.server_string
189
+ if serializable:
190
+ keepalive = keepalive.isoformat()
191
+ filter = self.filter
192
+ else:
193
+ keepalive = 'None'
194
+ server_string = 'None'
195
+ filter = 'None'
196
+ stats = {
197
+ 'connected': self.is_alive,
198
+ 'filter': filter,
199
+ 'login_status': self.login_status,
200
+ 'connection_keepalive': keepalive,
201
+ 'server_string': server_string,
202
+ 'transport': self.transport(),
203
+ }
204
+
205
+ return stats