Compare commits

...
7 Commits
28 changed files with 95 additions and 46 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ class CleanupTimer:
self._stop_event.set() self._stop_event.set()
if self._thread: if self._thread:
self._thread.join(timeout=15) self._thread.join(timeout=5)
if self._thread.is_alive(): if self._thread.is_alive():
logger.warning("Cleanup worker thread did not exit on time and will be killed.") logger.warning("Cleanup worker thread did not exit on time and will be killed.")
+1 -1
View File
@@ -4,7 +4,7 @@ from data.band import Band
from data.sig import SIG from data.sig import SIG
# General software # General software
SOFTWARE_VERSION = "2.1.1" SOFTWARE_VERSION = "2.1.3"
# HTTP headers used for spot providers that use HTTP # HTTP headers used for spot providers that use HTTP
HTTP_HEADERS = {"User-Agent": f"Spothole v{SOFTWARE_VERSION} (operated by {SERVER_OWNER_CALLSIGN})"} HTTP_HEADERS = {"User-Agent": f"Spothole v{SOFTWARE_VERSION} (operated by {SERVER_OWNER_CALLSIGN})"}
+1 -1
View File
@@ -91,7 +91,7 @@ class DataProviders:
for t in threads: for t in threads:
t.start() t.start()
deadline = time.monotonic() + 40 deadline = time.monotonic() + 15
for t in threads: for t in threads:
t.join(timeout=max(0.0, deadline - time.monotonic())) t.join(timeout=max(0.0, deadline - time.monotonic()))
still_running = [t for t in threads if t.is_alive()] still_running = [t for t in threads if t.is_alive()]
+5
View File
@@ -77,7 +77,12 @@ class LiveDataCache:
logger.exception(f"Failed to write snapshot to {self._snapshot_dir}") logger.exception(f"Failed to write snapshot to {self._snapshot_dir}")
def _load_snapshot(self): def _load_snapshot(self):
try:
data = self._disk_cache.get("snapshot") 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: if not data:
return return
+6
View File
@@ -21,7 +21,13 @@ class SingleObjectDataCache:
# This cache stores a single object, doesn't matter what it's called so "object" will do # This cache stores a single object, doesn't matter what it's called so "object" will do
if "object" not in self._cache: if "object" not in self._cache:
self._cache.add("object", object_if_empty) self._cache.add("object", object_if_empty)
try:
self._obj = self._cache.get("object") 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): def get(self):
"""Get the data object. This can then be manipulated as necessary across multiple threads. Any function """Get the data object. This can then be manipulated as necessary across multiple threads. Any function
+7 -2
View File
@@ -4,7 +4,7 @@ from threading import Event, Thread
import pytz import pytz
import requests import requests
from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout from requests.exceptions import ConnectionError, ConnectTimeout, JSONDecodeError, ReadTimeout
from core.constants import HTTP_HEADERS from core.constants import HTTP_HEADERS
from providers.alert.alert_provider import AlertProvider from providers.alert.alert_provider import AlertProvider
@@ -33,7 +33,7 @@ class HTTPAlertProvider(AlertProvider):
def stop(self): def stop(self):
self._stop_event.set() self._stop_event.set()
if self._thread: if self._thread:
self._thread.join(timeout=35) self._thread.join(timeout=12)
if self._thread.is_alive(): if self._thread.is_alive():
logger.warning(f"{self.name} alert worker thread did not exit on time and will be killed.") 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.") logger.warning(f"HTTP {http_response.status_code} when calling {self.name} alerts API.")
except ConnectionError: except ConnectionError:
self.status = "Error"
logger.warning(f"Connection error when accessing {self.name} alerts API.") logger.warning(f"Connection error when accessing {self.name} alerts API.")
except (ConnectTimeout, ReadTimeout): except (ConnectTimeout, ReadTimeout):
self.status = "Error"
logger.warning(f"Timeout when accessing {self.name} alerts API.") 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: except Exception:
self.status = "Error" self.status = "Error"
logger.exception(f"Exception in HTTP JSON Alert Provider ({self.name})") logger.exception(f"Exception in HTTP JSON Alert Provider ({self.name})")
@@ -38,7 +38,7 @@ class FileDownloadCallsignDataProvider(CallsignDataProvider):
def stop(self): def stop(self):
self._stop_event.set() self._stop_event.set()
if self._thread: if self._thread:
self._thread.join(timeout=35) self._thread.join(timeout=12)
if self._thread.is_alive(): if self._thread.is_alive():
logger.warning(f"{self.name} callsign data worker thread did not exit on time and will be killed.") logger.warning(f"{self.name} callsign data worker thread did not exit on time and will be killed.")
@@ -33,7 +33,7 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
def stop(self): def stop(self):
super().stop() super().stop()
if self._thread: if self._thread:
self._thread.join(timeout=35) self._thread.join(timeout=12)
if self._thread.is_alive(): if self._thread.is_alive():
logger.warning(f"{self.sig_name} SIG ref data worker thread did not exit on time and will be killed.") logger.warning(f"{self.sig_name} SIG ref data worker thread did not exit on time and will be killed.")
+1 -1
View File
@@ -73,7 +73,7 @@ class GIROIonosonde(SolarConditionsProvider):
def stop(self): def stop(self):
self._stop_event.set() self._stop_event.set()
if self._thread: if self._thread:
self._thread.join(timeout=35) self._thread.join(timeout=12)
if self._thread.is_alive(): if self._thread.is_alive():
logger.warning("GIRO ionosonde worker thread did not exit on time and will be killed.") logger.warning("GIRO ionosonde worker thread did not exit on time and will be killed.")
@@ -31,7 +31,7 @@ class HTTPSolarConditionsProvider(SolarConditionsProvider):
def stop(self): def stop(self):
self._stop_event.set() self._stop_event.set()
if self._thread: if self._thread:
self._thread.join(timeout=35) self._thread.join(timeout=12)
if self._thread.is_alive(): if self._thread.is_alive():
logger.warning(f"{self.name} solar conditions worker thread did not exit on time and will be killed.") logger.warning(f"{self.name} solar conditions worker thread did not exit on time and will be killed.")
+1 -1
View File
@@ -38,7 +38,7 @@ class KC2GProp(SolarConditionsProvider):
def stop(self): def stop(self):
self._stop_event.set() self._stop_event.set()
if self._thread: if self._thread:
self._thread.join(timeout=35) self._thread.join(timeout=12)
if self._thread.is_alive(): if self._thread.is_alive():
logger.warning("KC2G ionosonde worker thread did not exit on time and will be killed.") logger.warning("KC2G ionosonde worker thread did not exit on time and will be killed.")
+1 -1
View File
@@ -49,7 +49,7 @@ class APRSIS(SpotProvider):
if self._aprsis: if self._aprsis:
self._aprsis.close() self._aprsis.close()
if self._thread: if self._thread:
self._thread.join(timeout=15) self._thread.join(timeout=5)
if self._thread.is_alive(): if self._thread.is_alive():
logger.warning("APRS-IS worker thread did not exit on time and will be killed.") logger.warning("APRS-IS worker thread did not exit on time and will be killed.")
+17 -3
View File
@@ -1,7 +1,8 @@
import logging import logging
import re import re
import socket
from datetime import datetime from datetime import datetime
from threading import Event, Thread from threading import Event, Lock, Thread
import pytz import pytz
import telnetlib3 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._LINE_PATTERN_ALLOW_RBN if self._allow_rbn_spots else self._LINE_PATTERN_EXCLUDE_RBN
) )
self._telnet = None self._telnet = None
self._telnet_lock = Lock()
self._thread = None self._thread = None
self._stop_event = Event() self._stop_event = Event()
@@ -49,10 +51,15 @@ class DXCluster(SpotProvider):
def stop(self): def stop(self):
self._stop_event.set() self._stop_event.set()
with self._telnet_lock:
if self._telnet: if self._telnet:
try:
self._telnet.sock.shutdown(socket.SHUT_RDWR)
except (AttributeError, OSError):
pass
self._telnet.close() self._telnet.close()
if self._thread: if self._thread:
self._thread.join(timeout=15) self._thread.join(timeout=5)
if self._thread.is_alive(): if self._thread.is_alive():
logger.warning(f"DX Cluster {self._hostname} worker thread did not exit on time and will be killed.") 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: try:
self.status = "Connecting" self.status = "Connecting"
logger.info(f"DX Cluster {self._hostname} 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.read_until(self._login_prompt.encode("latin-1"))
self._telnet.write(f"{self._login_callsign}\n".encode("latin-1")) self._telnet.write(f"{self._login_callsign}\n".encode("latin-1"))
connected = True connected = True
+7 -2
View File
@@ -4,7 +4,7 @@ from threading import Event, Thread
import pytz import pytz
import requests import requests
from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout from requests.exceptions import ConnectionError, ConnectTimeout, JSONDecodeError, ReadTimeout
from core.constants import HTTP_HEADERS from core.constants import HTTP_HEADERS
from providers.spot.spot_provider import SpotProvider from providers.spot.spot_provider import SpotProvider
@@ -35,7 +35,7 @@ class HTTPSpotProvider(SpotProvider):
self._stop_event.set() self._stop_event.set()
self._wakeup_event.set() self._wakeup_event.set()
if self._thread: if self._thread:
self._thread.join(timeout=35) self._thread.join(timeout=12)
if self._thread.is_alive(): if self._thread.is_alive():
logger.warning(f"{self.name} spot worker thread did not exit on time and will be killed.") 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.") logger.warning(f"HTTP {http_response.status_code} when calling {self.name} spot API.")
except ConnectionError: except ConnectionError:
self.status = "Error"
logger.warning(f"Connection error when accessing {self.name} spots API.") logger.warning(f"Connection error when accessing {self.name} spots API.")
except (ConnectTimeout, ReadTimeout): except (ConnectTimeout, ReadTimeout):
self.status = "Error"
logger.warning(f"Timeout when accessing {self.name} spots API.") 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: except Exception:
self.status = "Error" self.status = "Error"
logger.exception(f"Exception in HTTP Spot Provider ({self.name})") logger.exception(f"Exception in HTTP Spot Provider ({self.name})")
+17 -3
View File
@@ -1,7 +1,8 @@
import logging import logging
import re import re
import socket
from datetime import datetime from datetime import datetime
from threading import Event, Thread from threading import Event, Lock, Thread
import pytz import pytz
import telnetlib3 import telnetlib3
@@ -29,6 +30,7 @@ class RBN(SpotProvider):
super().__init__(name, provider_config) super().__init__(name, provider_config)
self._port = provider_config["port"] self._port = provider_config["port"]
self._telnet = None self._telnet = None
self._telnet_lock = Lock()
self._thread = None self._thread = None
self._stop_event = Event() self._stop_event = Event()
@@ -38,10 +40,15 @@ class RBN(SpotProvider):
def stop(self): def stop(self):
self._stop_event.set() self._stop_event.set()
with self._telnet_lock:
if self._telnet: if self._telnet:
try:
self._telnet.sock.shutdown(socket.SHUT_RDWR)
except (AttributeError, OSError):
pass
self._telnet.close() self._telnet.close()
if self._thread: if self._thread:
self._thread.join(timeout=15) self._thread.join(timeout=5)
if self._thread.is_alive(): if self._thread.is_alive():
logger.warning(f"RBN (port {self._port!s}) worker thread did not exit on time and will be killed.") 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: try:
self.status = "Connecting" self.status = "Connecting"
logger.info(f"RBN port {self._port!s} 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.read_until("Please enter your call: ".encode("latin-1"))
self._telnet.write(f"{SERVER_OWNER_CALLSIGN}\n".encode("latin-1")) self._telnet.write(f"{SERVER_OWNER_CALLSIGN}\n".encode("latin-1"))
connected = True connected = True
+1 -1
View File
@@ -42,7 +42,7 @@ class SSESpotProvider(SpotProvider):
logger.exception(f"Exception closing SSE connection for {self.name} during stop()") logger.exception(f"Exception closing SSE connection for {self.name} during stop()")
if self._thread: if self._thread:
self._thread.join(timeout=15) self._thread.join(timeout=5)
if self._thread.is_alive(): if self._thread.is_alive():
logger.warning(f"{self.name} SSE worker thread did not exit on time and will be killed.") logger.warning(f"{self.name} SSE worker thread did not exit on time and will be killed.")
+1 -1
View File
@@ -34,7 +34,7 @@ class WebsocketSpotProvider(SpotProvider):
if self._ws: if self._ws:
self._ws.close() self._ws.close()
if self._thread: if self._thread:
self._thread.join(timeout=15) self._thread.join(timeout=5)
if self._thread.is_alive(): if self._thread.is_alive():
logger.warning(f"{self.name} websocket worker thread did not exit on time and will be killed.") logger.warning(f"{self.name} websocket worker thread did not exit on time and will be killed.")
@@ -35,7 +35,7 @@ class FileDownloadStaticDataProvider(StaticDataProvider):
def stop(self): def stop(self):
self._stop_event.set() self._stop_event.set()
if self._thread: if self._thread:
self._thread.join(timeout=35) self._thread.join(timeout=12)
if self._thread.is_alive(): if self._thread.is_alive():
logger.warning(f"{self.name} static data worker thread did not exit on time and will be killed.") logger.warning(f"{self.name} static data worker thread did not exit on time and will be killed.")
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "spothole" name = "spothole"
version = "2.1.1" version = "2.1.3"
authors = [ authors = [
{ name = "Ian Renton", email = "ian@ianrenton.com" }, { name = "Ian Renton", email = "ian@ianrenton.com" },
] ]
+2 -2
View File
@@ -414,8 +414,8 @@ div.band-spot:hover span.band-spot-info {
/* Make map stretch to horizontal screen edges */ /* Make map stretch to horizontal screen edges */
div#map, div#table-container, div#bands-container { div#map, div#table-container, div#bands-container {
margin-left: -1em; margin-left: -0.75rem;
margin-right: -1em; margin-right: -0.75rem;
} }
/* Avoid map page filters panel being larger than the map itself */ /* Avoid map page filters panel being larger than the map itself */
+1 -1
View File
@@ -77,7 +77,7 @@
</div> </div>
<script src="/static/js/add-spot.js?v=1789763826"></script> <script src="/static/js/add-spot.js?v=1789894090"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-add-spot").addClass("active"); $("#nav-link-add-spot").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -83,7 +83,7 @@
</div> </div>
<script src="/static/js/alerts.js?v=1789763826"></script> <script src="/static/js/alerts.js?v=1789894090"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-alerts").addClass("active"); $("#nav-link-alerts").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -76,8 +76,8 @@
</div> </div>
<script src="/static/js/spotsbandsandmap.js?v=1789763826"></script> <script src="/static/js/spotsbandsandmap.js?v=1789894090"></script>
<script src="/static/js/bands.js?v=1789763826"></script> <script src="/static/js/bands.js?v=1789894090"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-bands").addClass("active"); $("#nav-link-bands").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+5 -5
View File
@@ -1,6 +1,6 @@
{% extends "skeleton.html" %} {% extends "skeleton.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/static/css/style.css?v=1789763825" type="text/css"> <link rel="stylesheet" href="/static/css/style.css?v=1789894090" type="text/css">
<link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet"> <link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
<link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet"> <link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet"> <link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
@@ -16,10 +16,10 @@
window.fetchEventSource = fetchEventSource; window.fetchEventSource = fetchEventSource;
</script> </script>
<script src="/static/js/utils.js?v=1789763825"></script> <script src="/static/js/utils.js?v=1789894090"></script>
<script src="/static/js/ui-ham.js?v=1789763825"></script> <script src="/static/js/ui-ham.js?v=1789894090"></script>
<script src="/static/js/geo.js?v=1789763825"></script> <script src="/static/js/geo.js?v=1789894090"></script>
<script src="/static/js/common.js?v=1789763825"></script> <script src="/static/js/common.js?v=1789894090"></script>
{% end %} {% end %}
{% block body %} {% block body %}
<div class="container"> <div class="container">
+1 -1
View File
@@ -284,7 +284,7 @@
</div> </div>
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script> <script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
<script src="/static/js/conditions.js?v=1789763825"></script> <script src="/static/js/conditions.js?v=1789894090"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-conditions").addClass("active"); $("#nav-link-conditions").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -113,8 +113,8 @@
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}"; const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
</script> </script>
<script src="/static/js/spotsbandsandmap.js?v=1789763825"></script> <script src="/static/js/spotsbandsandmap.js?v=1789894090"></script>
<script src="/static/js/map.js?v=1789763825"></script> <script src="/static/js/map.js?v=1789894090"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-map").addClass("active"); $("#nav-link-map").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -125,8 +125,8 @@
</div> </div>
<script src="/static/js/spotsbandsandmap.js?v=1789763825"></script> <script src="/static/js/spotsbandsandmap.js?v=1789894090"></script>
<script src="/static/js/spots.js?v=1789763825"></script> <script src="/static/js/spots.js?v=1789894090"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-spots").addClass("active"); $("#nav-link-spots").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -96,7 +96,7 @@
</div> </div>
</div> </div>
<script src="/static/js/status.js?v=1789763826"></script> <script src="/static/js/status.js?v=1789894090"></script>
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$("#nav-link-status").addClass("active"); $("#nav-link-status").addClass("active");