From 1606585d41f69133192199d139b53344bb320fa9 Mon Sep 17 00:00:00 2001 From: Hemna Date: Thu, 30 Jan 2025 10:03:14 -0800 Subject: [PATCH] Updated APRSIS driver This patch adds an override to _connect to set some more socket keepalive options. --- aprsd/client/drivers/aprsis.py | 122 +++++++++++++++++++++++---------- 1 file changed, 87 insertions(+), 35 deletions(-) diff --git a/aprsd/client/drivers/aprsis.py b/aprsd/client/drivers/aprsis.py index 61953de..95a403d 100644 --- a/aprsd/client/drivers/aprsis.py +++ b/aprsd/client/drivers/aprsis.py @@ -1,6 +1,7 @@ import datetime import logging import select +import socket import threading import aprslib @@ -18,7 +19,7 @@ from aprslib.exceptions import ( import aprsd from aprsd.packets import core -LOG = logging.getLogger("APRSD") +LOG = logging.getLogger('APRSD') class Aprsdis(aprslib.IS): @@ -31,7 +32,7 @@ class Aprsdis(aprslib.IS): aprsd_keepalive = datetime.datetime.now() # Which server we are connected to? - server_string = "None" + server_string = 'None' # timeout in seconds select_timeout = 1 @@ -39,10 +40,10 @@ class Aprsdis(aprslib.IS): def stop(self): self.thread_stop = True - LOG.warning("Shutdown Aprsdis client.") + LOG.warning('Shutdown Aprsdis client.') def close(self): - LOG.warning("Closing Aprsdis client.") + LOG.warning('Closing Aprsdis client.') super().close() @wrapt.synchronized(lock) @@ -54,6 +55,57 @@ class Aprsdis(aprslib.IS): """If the connection is alive or not.""" return self._connected + def _connect(self): + """ + Attemps connection to the server + """ + + self.logger.info( + 'Attempting connection to %s:%s', self.server[0], self.server[1] + ) + + try: + self._open_socket() + + peer = self.sock.getpeername() + + self.logger.info('Connected to %s', str(peer)) + + # 5 second timeout to receive server banner + self.sock.setblocking(1) + self.sock.settimeout(5) + + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + # MACOS doesn't have TCP_KEEPIDLE + if hasattr(socket, 'TCP_KEEPIDLE'): + self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1) + self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 3) + self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5) + + banner = self.sock.recv(512) + if is_py3: + banner = banner.decode('latin-1') + + if banner[0] == '#': + self.logger.debug('Banner: %s', banner.rstrip()) + else: + raise ConnectionError('invalid banner from server') + + except ConnectionError as e: + self.logger.error(str(e)) + self.close() + raise + except (socket.error, socket.timeout) as e: + self.close() + + self.logger.error('Socket error: %s' % str(e)) + if str(e) == 'timed out': + raise ConnectionError('no banner from server') from e + else: + raise ConnectionError(e) from e + + self._connected = True + def _socket_readlines(self, blocking=False): """ Generator for complete lines, received from the server @@ -61,12 +113,12 @@ class Aprsdis(aprslib.IS): try: self.sock.setblocking(0) except OSError as e: - self.logger.error(f"socket error when setblocking(0): {str(e)}") - raise aprslib.ConnectionDrop("connection dropped") + self.logger.error(f'socket error when setblocking(0): {str(e)}') + raise aprslib.ConnectionDrop('connection dropped') from e while not self.thread_stop: - short_buf = b"" - newline = b"\r\n" + short_buf = b'' + newline = b'\r\n' # set a select timeout, so we get a chance to exit # when user hits CTRL-C @@ -91,11 +143,11 @@ class Aprsdis(aprslib.IS): # We could just not be blocking, so empty is expected continue else: - self.logger.error("socket.recv(): returned empty") - raise aprslib.ConnectionDrop("connection dropped") + self.logger.error('socket.recv(): returned empty') + raise aprslib.ConnectionDrop('connection dropped') except OSError as e: # self.logger.error("socket error on recv(): %s" % str(e)) - if "Resource temporarily unavailable" in str(e): + if 'Resource temporarily unavailable' in str(e): if not blocking: if len(self.buf) == 0: break @@ -111,22 +163,22 @@ class Aprsdis(aprslib.IS): """ Sends login string to server """ - login_str = "user {0} pass {1} vers github.com/craigerl/aprsd {3}{2}\r\n" + login_str = 'user {0} pass {1} vers Python-APRSD {3}{2}\r\n' login_str = login_str.format( self.callsign, self.passwd, - (" filter " + self.filter) if self.filter != "" else "", + (' filter ' + self.filter) if self.filter != '' else '', aprsd.__version__, ) - self.logger.debug("Sending login information") + self.logger.debug('Sending login information') try: self._sendall(login_str) self.sock.settimeout(5) test = self.sock.recv(len(login_str) + 100) if is_py3: - test = test.decode("latin-1") + test = test.decode('latin-1') test = test.rstrip() self.logger.debug("Server: '%s'", test) @@ -134,26 +186,26 @@ class Aprsdis(aprslib.IS): if not test: raise LoginError(f"Server Response Empty: '{test}'") - _, _, callsign, status, e = test.split(" ", 4) - s = e.split(",") + _, _, callsign, status, e = test.split(' ', 4) + s = e.split(',') if len(s): - server_string = s[0].replace("server ", "") + server_string = s[0].replace('server ', '') else: - server_string = e.replace("server ", "") + server_string = e.replace('server ', '') - if callsign == "": - raise LoginError("Server responded with empty callsign???") + if callsign == '': + raise LoginError('Server responded with empty callsign???') if callsign != self.callsign: - raise LoginError(f"Server: {test}") - if status != "verified," and self.passwd != "-1": - raise LoginError("Password is incorrect") + raise LoginError(f'Server: {test}') + if status != 'verified,' and self.passwd != '-1': + raise LoginError('Password is incorrect') - if self.passwd == "-1": - self.logger.info("Login successful (receive only)") + if self.passwd == '-1': + self.logger.info('Login successful (receive only)') else: - self.logger.info("Login successful") + self.logger.info('Login successful') - self.logger.info(f"Connected to {server_string}") + self.logger.info(f'Connected to {server_string}') self.server_string = server_string except LoginError as e: @@ -164,7 +216,7 @@ class Aprsdis(aprslib.IS): self.close() self.logger.error(f"Failed to login '{e}'") self.logger.exception(e) - raise LoginError("Failed to login") + raise LoginError('Failed to login') from e def consumer(self, callback, blocking=True, immortal=False, raw=False): """ @@ -180,21 +232,21 @@ class Aprsdis(aprslib.IS): """ if not self._connected: - raise ConnectionError("not connected to a server") + raise ConnectionError('not connected to a server') - line = b"" + line = b'' while True and not self.thread_stop: try: for line in self._socket_readlines(blocking): - if line[0:1] != b"#": + 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")) + self.logger.debug('Server: %s', line.decode('utf8')) self.aprsd_keepalive = datetime.datetime.now() except ParseError as exp: self.logger.log( @@ -211,7 +263,7 @@ class Aprsdis(aprslib.IS): exp.packet, ) except LoginError as exp: - self.logger.error("%s: %s", exp.__class__.__name__, exp) + self.logger.error('%s: %s', exp.__class__.__name__, exp) except (KeyboardInterrupt, SystemExit): raise except (ConnectionDrop, ConnectionError): @@ -227,7 +279,7 @@ class Aprsdis(aprslib.IS): except StopIteration: break except Exception: - self.logger.error("APRS Packet: %s", line) + self.logger.error('APRS Packet: %s', line) raise if not blocking: