import logging import socket from datetime import datetime from threading import Event, Lock, Thread import pytz import telnetlib3 from core.utils import decode_telnet_bytes from providers.spot.spot_provider import SpotProvider logger = logging.getLogger(__name__) class TelnetSpotProvider(SpotProvider): """Base class for spot providers that connect to a telnet server to receive spots.""" def __init__(self, name, provider_config, host, port, login_prompt, login_response): """Constructor. host/port are the telnet server to connect to. login_prompt is the text to wait for before logging in, and login_response is the callsign to send in response.""" super().__init__(name, provider_config) self._host = host self._port = port self._login_prompt = login_prompt self._login_response = login_response self._telnet = None self._telnet_lock = Lock() self._thread = None self._stop_event = Event() def _parse_line(self, line): """Parse a line of telnet output and return a Spot, or None if the line did not contain a spot. Subclasses must implement this method.""" raise NotImplementedError("Subclasses must implement this method") def start(self): self._thread = Thread(target=self._handle, name=f"{self.name}SpotProvider", daemon=True) self._thread.start() def stop(self): self._stop_event.set() with self._telnet_lock: if self._telnet: try: self._telnet.sock.shutdown(socket.SHUT_RDWR) except (AttributeError, OSError): pass self._telnet.close() if self._thread: self._thread.join(timeout=5) if self._thread.is_alive(): logger.warning(f"{self.name} worker thread did not exit on time and will be killed.") def _handle(self): while not self._stop_event.is_set(): connected = False while not connected and not self._stop_event.is_set(): try: self.status = "Connecting" logger.info(f"{self.name} connecting to {self._host}:{self._port!s}...") new_telnet = telnetlib3.Telnet(self._host, self._port) with self._telnet_lock: self._telnet = new_telnet if self._stop_event.is_set(): # stop() was called while we were connecting, close the connection rather than trying to # read when we know it won't work new_telnet.close() break self._telnet.read_until(self._login_prompt.encode("latin-1")) self._telnet.write(f"{self._login_response}\n".encode("latin-1")) connected = True logger.info(f"{self.name} connected.") except ConnectionRefusedError: self.status = "Error" logger.warning(f"Connection refused to {self.name} ({self._host}:{self._port!s}).") self._stop_event.wait(timeout=300) except Exception: self.status = "Error" logger.exception(f"Exception while connecting to {self.name} ({self._host}:{self._port!s}).") self._stop_event.wait(timeout=5) self.status = "Waiting for Data" while connected and not self._stop_event.is_set(): try: # Check new telnet info against regular expression telnet_output = self._telnet.read_until("\n".encode("latin-1")) spot = self._parse_line(decode_telnet_bytes(telnet_output)) if spot: # Add to our list self._submit(spot) self.status = "OK" self.last_update_time = datetime.now(pytz.UTC) logger.debug(f"Data received from {self.name}.") except EOFError: connected = False if not self._stop_event.is_set(): self.status = "Restarting" logger.warning(f"Disconnected from {self.name}. Reconnecting...") self._stop_event.wait(timeout=5) else: logger.info(f"{self.name} shutting down...") self.status = "Shutting down" except Exception: connected = False if not self._stop_event.is_set(): self.status = "Error" logger.exception(f"Exception in {self.name}") self._stop_event.wait(timeout=5) else: logger.info(f"{self.name} shutting down...") self.status = "Shutting down" self.status = "Disconnected"