mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-20 22:37:44 +00:00
Both providers always decoded telnet bytes as Latin-1, which silently mangles nodes that send UTF-8 (Latin-1 decode never raises, so mixed encodings across cluster nodes went unnoticed). Added a shared decode_telnet_bytes() helper that tries UTF-8 first and falls back to Latin-1, since a byte stream that happens to be valid multi-byte UTF-8 is essentially never accidental Latin-1 text. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
91 lines
4.0 KiB
Python
91 lines
4.0 KiB
Python
from datetime import datetime
|
|
|
|
import pytz
|
|
|
|
from core.data_store import DATA_STORE
|
|
|
|
|
|
def decode_telnet_bytes(data: bytes) -> str:
|
|
"""Decode a line of text received from a telnet connection. DX cluster and RBN nodes are inconsistent about the
|
|
character encoding they use for spot comments: most send UTF-8, but some older ones send Latin-1/CP1252. Try
|
|
UTF-8 first, since valid multi-byte UTF-8 sequences are very unlikely to occur by chance in Latin-1 text, then
|
|
fall back to Latin-1, which can decode any byte sequence without raising."""
|
|
|
|
try:
|
|
return data.decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
return data.decode("latin-1")
|
|
|
|
|
|
class SpotProvider:
|
|
"""Generic spot provider class. Subclasses of this query the individual APIs for data."""
|
|
|
|
def __init__(self, name, provider_config):
|
|
"""Constructor"""
|
|
|
|
self.name = name
|
|
self.enabled = provider_config.get("enabled", True)
|
|
self.enabled_by_default_in_web_ui = provider_config.get("enabled_by_default_in_web_ui", True)
|
|
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
|
|
self.last_spot_time = datetime.min.replace(tzinfo=pytz.UTC)
|
|
self.status = "Not Started" if self.enabled else "Disabled"
|
|
self._spots = DATA_STORE.spots
|
|
|
|
def start(self):
|
|
"""Start the provider. This should return immediately after spawning threads to access the remote resources"""
|
|
|
|
raise NotImplementedError("Subclasses must implement this method")
|
|
|
|
def _submit_batch(self, spots):
|
|
"""Submit a batch of spots retrieved from the provider. Only spots that are newer than the last spot retrieved
|
|
by this provider will be added to the spot list, to prevent duplications. Spots passing the check will also have
|
|
their infer_missing() method called to complete their data set. This is called by the API-querying
|
|
subclasses on receiving spots."""
|
|
|
|
# Sort the batch so that earliest ones go in first. This helps keep the ordering correct when spots are fired
|
|
# off to SSE listeners.
|
|
spots = sorted(spots, key=lambda s: s.time if s and s.time else 0)
|
|
for spot in spots:
|
|
if datetime.fromtimestamp(spot.time, pytz.UTC) > self.last_spot_time:
|
|
# Fill in any blanks and add to the list
|
|
spot.infer_missing()
|
|
self._add_spot(spot)
|
|
if spots:
|
|
self.last_spot_time = datetime.fromtimestamp(max(s.time for s in spots), pytz.UTC)
|
|
|
|
def _submit(self, spot):
|
|
"""Submit a single spot retrieved from the provider. This will be added to the list regardless of its age. Spots
|
|
passing the check will also have their infer_missing() method called to complete their data set. This is called by
|
|
the data streaming subclasses, which can be relied upon not to re-provide old spots."""
|
|
|
|
# Fill in any blanks and add to the list
|
|
spot.infer_missing()
|
|
self._add_spot(spot)
|
|
self.last_spot_time = datetime.fromtimestamp(spot.time, pytz.UTC)
|
|
|
|
def _add_spot(self, spot):
|
|
if not spot.expired():
|
|
self._spots.set(spot.id, spot)
|
|
|
|
def stop(self):
|
|
"""Stop any threads and prepare for application shutdown"""
|
|
|
|
raise NotImplementedError("Subclasses must implement this method")
|
|
|
|
def can_submit_spot(self, activity):
|
|
"""Return True if this provider supports submitting spots upstream for the given activity."""
|
|
|
|
return False
|
|
|
|
def submit_spot(self, spot, credentials):
|
|
"""Submit a spot upstream to this provider's API. credentials is a dict with provider-specific keys.
|
|
Raises an exception with a descriptive message on failure."""
|
|
|
|
raise NotImplementedError("This provider does not support spot submission")
|
|
|
|
def force_poll(self):
|
|
"""Trigger an immediate poll without waiting for the normal interval. Default implementation here does nothing
|
|
because not all spot providers have a polling mechanism. Providers that do should override this method."""
|
|
|
|
return
|