mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-22 23:37:43 +00:00
118 lines
5.0 KiB
Python
118 lines
5.0 KiB
Python
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"
|