diff --git a/core/cleanup.py b/core/cleanup.py index 3a82fa8..6610d5d 100644 --- a/core/cleanup.py +++ b/core/cleanup.py @@ -35,7 +35,7 @@ class CleanupTimer: self._stop_event.set() if self._thread: - self._thread.join(timeout=15) + self._thread.join(timeout=5) if self._thread.is_alive(): logger.warning("Cleanup worker thread did not exit on time and will be killed.") diff --git a/core/data_providers.py b/core/data_providers.py index 71a5343..fe65a74 100644 --- a/core/data_providers.py +++ b/core/data_providers.py @@ -91,7 +91,7 @@ class DataProviders: for t in threads: t.start() - deadline = time.monotonic() + 40 + deadline = time.monotonic() + 15 for t in threads: t.join(timeout=max(0.0, deadline - time.monotonic())) still_running = [t for t in threads if t.is_alive()] diff --git a/core/live_data_cache.py b/core/live_data_cache.py index 6fb61e4..34e35d8 100644 --- a/core/live_data_cache.py +++ b/core/live_data_cache.py @@ -77,7 +77,12 @@ class LiveDataCache: logger.exception(f"Failed to write snapshot to {self._snapshot_dir}") def _load_snapshot(self): - data = self._disk_cache.get("snapshot") + try: + data = self._disk_cache.get("snapshot") + except Exception: + logger.warning(f"Failed to load snapshot from {self._snapshot_dir}, clearing it.") + self._disk_cache.clear() + return if not data: return diff --git a/core/single_object_data_cache.py b/core/single_object_data_cache.py index 5993060..2bb7fa9 100644 --- a/core/single_object_data_cache.py +++ b/core/single_object_data_cache.py @@ -21,7 +21,13 @@ class SingleObjectDataCache: # This cache stores a single object, doesn't matter what it's called so "object" will do if "object" not in self._cache: self._cache.add("object", object_if_empty) - self._obj = self._cache.get("object") + try: + self._obj = self._cache.get("object") + except Exception: + logger.warning(f"Failed to load cache from {cache_dir}, clearing it.") + self._cache.clear() + self._cache.add("object", object_if_empty) + self._obj = object_if_empty def get(self): """Get the data object. This can then be manipulated as necessary across multiple threads. Any function diff --git a/providers/activityrefdata/file_download_activity_ref_data_provider.py b/providers/activityrefdata/file_download_activity_ref_data_provider.py index cfbc3de..3d8e865 100644 --- a/providers/activityrefdata/file_download_activity_ref_data_provider.py +++ b/providers/activityrefdata/file_download_activity_ref_data_provider.py @@ -34,7 +34,7 @@ class FileDownloadActivityRefDataProvider(ActivityRefDataProvider): def stop(self): super().stop() if self._thread: - self._thread.join(timeout=35) + self._thread.join(timeout=12) if self._thread.is_alive(): logger.warning(f"{self.sig_name} activity ref data worker thread did not exit on time and will be killed.") diff --git a/providers/alert/http_alert_provider.py b/providers/alert/http_alert_provider.py index 83d1c6b..eee3b52 100644 --- a/providers/alert/http_alert_provider.py +++ b/providers/alert/http_alert_provider.py @@ -4,7 +4,7 @@ from threading import Event, Thread import pytz import requests -from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout +from requests.exceptions import ConnectionError, ConnectTimeout, JSONDecodeError, ReadTimeout from core.constants import HTTP_HEADERS from providers.alert.alert_provider import AlertProvider @@ -33,7 +33,7 @@ class HTTPAlertProvider(AlertProvider): def stop(self): self._stop_event.set() if self._thread: - self._thread.join(timeout=35) + self._thread.join(timeout=12) if self._thread.is_alive(): logger.warning(f"{self.name} alert worker thread did not exit on time and will be killed.") @@ -64,9 +64,14 @@ class HTTPAlertProvider(AlertProvider): logger.warning(f"HTTP {http_response.status_code} when calling {self.name} alerts API.") except ConnectionError: + self.status = "Error" logger.warning(f"Connection error when accessing {self.name} alerts API.") except (ConnectTimeout, ReadTimeout): + self.status = "Error" logger.warning(f"Timeout when accessing {self.name} alerts API.") + except JSONDecodeError: + self.status = "Error" + logger.warning(f"Invalid or empty JSON response from {self.name} alert API.") except Exception: self.status = "Error" logger.exception(f"Exception in HTTP JSON Alert Provider ({self.name})") diff --git a/providers/callsigndata/file_download_callsign_data_provider.py b/providers/callsigndata/file_download_callsign_data_provider.py index 0663ea9..6016ce2 100644 --- a/providers/callsigndata/file_download_callsign_data_provider.py +++ b/providers/callsigndata/file_download_callsign_data_provider.py @@ -38,7 +38,7 @@ class FileDownloadCallsignDataProvider(CallsignDataProvider): def stop(self): self._stop_event.set() if self._thread: - self._thread.join(timeout=35) + self._thread.join(timeout=12) if self._thread.is_alive(): logger.warning(f"{self.name} callsign data worker thread did not exit on time and will be killed.") diff --git a/providers/solarconditions/giroionosonde.py b/providers/solarconditions/giroionosonde.py index 4067920..1c08618 100644 --- a/providers/solarconditions/giroionosonde.py +++ b/providers/solarconditions/giroionosonde.py @@ -73,7 +73,7 @@ class GIROIonosonde(SolarConditionsProvider): def stop(self): self._stop_event.set() if self._thread: - self._thread.join(timeout=35) + self._thread.join(timeout=12) if self._thread.is_alive(): logger.warning("GIRO ionosonde worker thread did not exit on time and will be killed.") diff --git a/providers/solarconditions/http_solar_conditions_provider.py b/providers/solarconditions/http_solar_conditions_provider.py index a5feeb9..f889fc1 100644 --- a/providers/solarconditions/http_solar_conditions_provider.py +++ b/providers/solarconditions/http_solar_conditions_provider.py @@ -31,7 +31,7 @@ class HTTPSolarConditionsProvider(SolarConditionsProvider): def stop(self): self._stop_event.set() if self._thread: - self._thread.join(timeout=35) + self._thread.join(timeout=12) if self._thread.is_alive(): logger.warning(f"{self.name} solar conditions worker thread did not exit on time and will be killed.") diff --git a/providers/solarconditions/kc2gprop.py b/providers/solarconditions/kc2gprop.py index 8f9cd81..a678bbe 100644 --- a/providers/solarconditions/kc2gprop.py +++ b/providers/solarconditions/kc2gprop.py @@ -38,7 +38,7 @@ class KC2GProp(SolarConditionsProvider): def stop(self): self._stop_event.set() if self._thread: - self._thread.join(timeout=35) + self._thread.join(timeout=12) if self._thread.is_alive(): logger.warning("KC2G ionosonde worker thread did not exit on time and will be killed.") diff --git a/providers/spot/aprsis.py b/providers/spot/aprsis.py index cc4be2a..8be74b7 100644 --- a/providers/spot/aprsis.py +++ b/providers/spot/aprsis.py @@ -49,7 +49,7 @@ class APRSIS(SpotProvider): if self._aprsis: self._aprsis.close() if self._thread: - self._thread.join(timeout=15) + self._thread.join(timeout=5) if self._thread.is_alive(): logger.warning("APRS-IS worker thread did not exit on time and will be killed.") diff --git a/providers/spot/dxcluster.py b/providers/spot/dxcluster.py index 48b5bed..4413207 100644 --- a/providers/spot/dxcluster.py +++ b/providers/spot/dxcluster.py @@ -1,7 +1,8 @@ import logging import re +import socket from datetime import datetime -from threading import Event, Thread +from threading import Event, Lock, Thread import pytz import telnetlib3 @@ -40,6 +41,7 @@ class DXCluster(SpotProvider): self._LINE_PATTERN_ALLOW_RBN if self._allow_rbn_spots else self._LINE_PATTERN_EXCLUDE_RBN ) self._telnet = None + self._telnet_lock = Lock() self._thread = None self._stop_event = Event() @@ -49,10 +51,15 @@ class DXCluster(SpotProvider): def stop(self): self._stop_event.set() - if self._telnet: - self._telnet.close() + 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=15) + self._thread.join(timeout=5) if self._thread.is_alive(): logger.warning(f"DX Cluster {self._hostname} worker thread did not exit on time and will be killed.") @@ -63,7 +70,14 @@ class DXCluster(SpotProvider): try: self.status = "Connecting" logger.info(f"DX Cluster {self._hostname} connecting...") - self._telnet = telnetlib3.Telnet(self._hostname, self._port) + new_telnet = telnetlib3.Telnet(self._hostname, 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_callsign}\n".encode("latin-1")) connected = True diff --git a/providers/spot/http_spot_provider.py b/providers/spot/http_spot_provider.py index 124742a..88f7dde 100644 --- a/providers/spot/http_spot_provider.py +++ b/providers/spot/http_spot_provider.py @@ -4,7 +4,7 @@ from threading import Event, Thread import pytz import requests -from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout +from requests.exceptions import ConnectionError, ConnectTimeout, JSONDecodeError, ReadTimeout from core.constants import HTTP_HEADERS from providers.spot.spot_provider import SpotProvider @@ -35,7 +35,7 @@ class HTTPSpotProvider(SpotProvider): self._stop_event.set() self._wakeup_event.set() if self._thread: - self._thread.join(timeout=35) + self._thread.join(timeout=12) if self._thread.is_alive(): logger.warning(f"{self.name} spot worker thread did not exit on time and will be killed.") @@ -73,9 +73,14 @@ class HTTPSpotProvider(SpotProvider): logger.warning(f"HTTP {http_response.status_code} when calling {self.name} spot API.") except ConnectionError: + self.status = "Error" logger.warning(f"Connection error when accessing {self.name} spots API.") except (ConnectTimeout, ReadTimeout): + self.status = "Error" logger.warning(f"Timeout when accessing {self.name} spots API.") + except JSONDecodeError: + self.status = "Error" + logger.warning(f"Invalid or empty JSON response from {self.name} spots API.") except Exception: self.status = "Error" logger.exception(f"Exception in HTTP Spot Provider ({self.name})") diff --git a/providers/spot/rbn.py b/providers/spot/rbn.py index eee0773..b4f7617 100644 --- a/providers/spot/rbn.py +++ b/providers/spot/rbn.py @@ -1,7 +1,8 @@ import logging import re +import socket from datetime import datetime -from threading import Event, Thread +from threading import Event, Lock, Thread import pytz import telnetlib3 @@ -29,6 +30,7 @@ class RBN(SpotProvider): super().__init__(name, provider_config) self._port = provider_config["port"] self._telnet = None + self._telnet_lock = Lock() self._thread = None self._stop_event = Event() @@ -38,10 +40,15 @@ class RBN(SpotProvider): def stop(self): self._stop_event.set() - if self._telnet: - self._telnet.close() + 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=15) + self._thread.join(timeout=5) if self._thread.is_alive(): logger.warning(f"RBN (port {self._port!s}) worker thread did not exit on time and will be killed.") @@ -52,7 +59,14 @@ class RBN(SpotProvider): try: self.status = "Connecting" logger.info(f"RBN port {self._port!s} connecting...") - self._telnet = telnetlib3.Telnet("telnet.reversebeacon.net", self._port) + new_telnet = telnetlib3.Telnet("telnet.reversebeacon.net", 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("Please enter your call: ".encode("latin-1")) self._telnet.write(f"{SERVER_OWNER_CALLSIGN}\n".encode("latin-1")) connected = True diff --git a/providers/spot/sse_spot_provider.py b/providers/spot/sse_spot_provider.py index 1b3f93b..a83f9a3 100644 --- a/providers/spot/sse_spot_provider.py +++ b/providers/spot/sse_spot_provider.py @@ -42,7 +42,7 @@ class SSESpotProvider(SpotProvider): logger.exception(f"Exception closing SSE connection for {self.name} during stop()") if self._thread: - self._thread.join(timeout=15) + self._thread.join(timeout=5) if self._thread.is_alive(): logger.warning(f"{self.name} SSE worker thread did not exit on time and will be killed.") diff --git a/providers/spot/websocket_spot_provider.py b/providers/spot/websocket_spot_provider.py index 13e460c..d3053bd 100644 --- a/providers/spot/websocket_spot_provider.py +++ b/providers/spot/websocket_spot_provider.py @@ -34,7 +34,7 @@ class WebsocketSpotProvider(SpotProvider): if self._ws: self._ws.close() if self._thread: - self._thread.join(timeout=15) + self._thread.join(timeout=5) if self._thread.is_alive(): logger.warning(f"{self.name} websocket worker thread did not exit on time and will be killed.") diff --git a/providers/staticdata/file_download_static_data_provider.py b/providers/staticdata/file_download_static_data_provider.py index 6b733f6..4b31b3d 100644 --- a/providers/staticdata/file_download_static_data_provider.py +++ b/providers/staticdata/file_download_static_data_provider.py @@ -35,7 +35,7 @@ class FileDownloadStaticDataProvider(StaticDataProvider): def stop(self): self._stop_event.set() if self._thread: - self._thread.join(timeout=35) + self._thread.join(timeout=12) if self._thread.is_alive(): logger.warning(f"{self.name} static data worker thread did not exit on time and will be killed.") diff --git a/static/css/style.css b/static/css/style.css index fc03f35..8ac315c 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -459,8 +459,8 @@ div.band-spot:hover span.band-spot-info { /* Make map stretch to horizontal screen edges */ div#map, div#table-container, div#bands-container { - margin-left: -1em; - margin-right: -1em; + margin-left: -0.75rem; + margin-right: -0.75rem; } /* Avoid map page filters panel being larger than the map itself */ diff --git a/templates/add_spot.html b/templates/add_spot.html index a5c097a..0de4bfe 100644 --- a/templates/add_spot.html +++ b/templates/add_spot.html @@ -77,7 +77,7 @@ - + diff --git a/templates/alerts.html b/templates/alerts.html index 666bf7e..7baf852 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -85,7 +85,7 @@ - + diff --git a/templates/bands.html b/templates/bands.html index 9117b9d..0a37e54 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -76,8 +76,8 @@ - - + + diff --git a/templates/base.html b/templates/base.html index 0061012..099c30d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,6 @@ {% extends "skeleton.html" %} {% block head_extra %} - + @@ -16,10 +16,10 @@ window.fetchEventSource = fetchEventSource; - - - - + + + + {% end %} {% block body %}