mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-06-23 21:25:12 +00:00
171 lines
8.0 KiB
Python
171 lines
8.0 KiB
Python
import asyncio
|
|
import logging
|
|
import os
|
|
|
|
import tornado
|
|
from tornado.web import StaticFileHandler
|
|
|
|
from core.config import SERVER_OWNER_CALLSIGN, ALLOW_SPOTTING
|
|
from core.constants import SOFTWARE_VERSION
|
|
from core.utils import empty_queue
|
|
from server.handlers.api.addspot import APISpotHandler
|
|
from server.handlers.api.dxstats import APIDxStatsHandler
|
|
from server.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler
|
|
from server.handlers.api.lookups import APILookupCallHandler, APILookupSIGRefHandler, APILookupGridHandler
|
|
from server.handlers.api.options import APIOptionsHandler
|
|
from server.handlers.api.solar_conditions import APISolarConditionsHandler
|
|
from server.handlers.api.spots import APISpotsHandler, APISpotsStreamHandler
|
|
from server.handlers.api.status import APIStatusHandler
|
|
from server.handlers.metrics import PrometheusMetricsHandler
|
|
from server.handlers.pagetemplate import PageTemplateHandler
|
|
|
|
|
|
class WebServer:
|
|
"""Provides the public-facing web server."""
|
|
|
|
def __init__(self, spots, alerts, solar_conditions, status_data, solar_condition_providers, port, api_only_mode=False):
|
|
"""Constructor"""
|
|
|
|
self._spots = spots
|
|
self._alerts = alerts
|
|
self._solar_conditions = solar_conditions
|
|
self._sse_spot_queues = []
|
|
self._sse_alert_queues = []
|
|
self._status_data = status_data
|
|
self._solar_condition_providers = solar_condition_providers
|
|
self._port = port
|
|
self._api_only_mode = api_only_mode
|
|
self._shutdown_event = asyncio.Event()
|
|
self.web_server_metrics = {
|
|
"last_page_access_time": None,
|
|
"last_api_access_time": None,
|
|
"page_access_counter": 0,
|
|
"api_access_counter": 0,
|
|
"status": "Starting"
|
|
}
|
|
|
|
def start(self):
|
|
"""Start the web server"""
|
|
|
|
asyncio.run(self._start_inner())
|
|
|
|
def stop(self):
|
|
"""Stop the web server"""
|
|
|
|
self._shutdown_event.set()
|
|
|
|
async def _start_inner(self):
|
|
"""Start method (async). Sets up the Tornado application."""
|
|
|
|
provider_classes = [type(p).__name__ for p in self._solar_condition_providers if p.enabled]
|
|
has_hamqsl = "HamQSL" in provider_classes
|
|
has_noaa_forecast = "NOAA3dayForecast" in provider_classes
|
|
has_giro_ionosonde = "GIROIonosonde" in provider_classes or "KC2GProp" in provider_classes
|
|
page_opts = {"web_server_metrics": self.web_server_metrics, "has_hamqsl": has_hamqsl,
|
|
"has_noaa_forecast": has_noaa_forecast, "has_giro_ionosonde": has_giro_ionosonde}
|
|
|
|
# API endpoints are always enabled
|
|
api_routes = [
|
|
(r"/api/v1/spots", APISpotsHandler, {"spots": self._spots, "web_server_metrics": self.web_server_metrics}),
|
|
(r"/api/v1/alerts", APIAlertsHandler,
|
|
{"alerts": self._alerts, "web_server_metrics": self.web_server_metrics}),
|
|
(r"/api/v1/spots/stream", APISpotsStreamHandler,
|
|
{"sse_spot_queues": self._sse_spot_queues, "web_server_metrics": self.web_server_metrics}),
|
|
(r"/api/v1/alerts/stream", APIAlertsStreamHandler,
|
|
{"sse_alert_queues": self._sse_alert_queues, "web_server_metrics": self.web_server_metrics}),
|
|
(r"/api/v1/solar", APISolarConditionsHandler,
|
|
{"solar_conditions": self._solar_conditions, "web_server_metrics": self.web_server_metrics}),
|
|
(r"/api/v1/dxstats", APIDxStatsHandler, {"spots": self._spots, "web_server_metrics": self.web_server_metrics}),
|
|
(r"/api/v1/options", APIOptionsHandler,
|
|
{"status_data": self._status_data, "web_server_metrics": self.web_server_metrics}),
|
|
(r"/api/v1/status", APIStatusHandler,
|
|
{"status_data": self._status_data, "web_server_metrics": self.web_server_metrics}),
|
|
(r"/api/v1/lookup/call", APILookupCallHandler, {"web_server_metrics": self.web_server_metrics}),
|
|
(r"/api/v1/lookup/sigref", APILookupSIGRefHandler, {"web_server_metrics": self.web_server_metrics}),
|
|
(r"/api/v1/lookup/grid", APILookupGridHandler, {"web_server_metrics": self.web_server_metrics}),
|
|
(r"/api/v1/spot", APISpotHandler, {"spots": self._spots, "web_server_metrics": self.web_server_metrics}),
|
|
]
|
|
|
|
# If in API-only mode, serve a basic homepage; in normal mode, serve the usual UI routes
|
|
if self._api_only_mode:
|
|
logging.info("API-only mode is enabled. Web UI will not be served.")
|
|
ui_routes = [
|
|
(r"/", PageTemplateHandler, {"template_name": "api_only_home", **page_opts})
|
|
]
|
|
else:
|
|
ui_routes = [
|
|
(r"/", PageTemplateHandler, {"template_name": "spots", **page_opts}),
|
|
(r"/map", PageTemplateHandler, {"template_name": "map", **page_opts}),
|
|
(r"/bands", PageTemplateHandler, {"template_name": "bands", **page_opts}),
|
|
(r"/alerts", PageTemplateHandler, {"template_name": "alerts", **page_opts}),
|
|
(r"/conditions", PageTemplateHandler, {"template_name": "conditions", **page_opts}),
|
|
(r"/status", PageTemplateHandler, {"template_name": "status", **page_opts}),
|
|
(r"/about", PageTemplateHandler, {"template_name": "about", **page_opts})
|
|
]
|
|
# Only allow the Add Spot page if spotting is allowed
|
|
if ALLOW_SPOTTING:
|
|
ui_routes += [(r"/add-spot", PageTemplateHandler, {"template_name": "add_spot", **page_opts})]
|
|
|
|
# API docs, Prometheus metrics, and finally static assets are always available regardless of API-only mode.
|
|
misc_routes = [
|
|
(r"/apidocs", PageTemplateHandler, {"template_name": "apidocs", **page_opts}),
|
|
(r"/metrics", PrometheusMetricsHandler),
|
|
(r"/(.*)", StaticFileHandler, {"path": os.path.join(os.path.dirname(__file__), "../webassets")})
|
|
]
|
|
|
|
app = tornado.web.Application(api_routes + ui_routes + misc_routes,
|
|
template_path=os.path.join(os.path.dirname(__file__), "../templates"),
|
|
debug=False)
|
|
app.listen(self._port)
|
|
await self._shutdown_event.wait()
|
|
|
|
def notify_new_spot(self, spot):
|
|
"""Internal method called when a new spot is added to the system. This is used to ping any SSE clients that are
|
|
awaiting a server-sent message with new spots."""
|
|
|
|
for queue in self._sse_spot_queues:
|
|
try:
|
|
queue.put(spot)
|
|
except:
|
|
# Cleanup thread was probably deleting the queue, that's fine
|
|
pass
|
|
pass
|
|
|
|
def notify_new_alert(self, alert):
|
|
"""Internal method called when a new alert is added to the system. This is used to ping any SSE clients that are
|
|
awaiting a server-sent message with new spots."""
|
|
|
|
for queue in self._sse_alert_queues:
|
|
try:
|
|
queue.put(alert)
|
|
except:
|
|
# Cleanup thread was probably deleting the queue, that's fine
|
|
pass
|
|
pass
|
|
|
|
def clean_up_sse_queues(self):
|
|
"""Clean up any SSE queues that are growing too large; probably their client disconnected and we didn't catch it
|
|
properly for some reason."""
|
|
|
|
for q in self._sse_spot_queues:
|
|
try:
|
|
if q.full():
|
|
logging.warning(
|
|
"A full SSE spot queue was found, presumably because the client disconnected strangely. It has been removed.")
|
|
self._sse_spot_queues.remove(q)
|
|
empty_queue(q)
|
|
except:
|
|
# Probably got deleted already on another thread
|
|
pass
|
|
for q in self._sse_alert_queues:
|
|
try:
|
|
if q.full():
|
|
logging.warning(
|
|
"A full SSE alert queue was found, presumably because the client disconnected strangely. It has been removed.")
|
|
self._sse_alert_queues.remove(q)
|
|
empty_queue(q)
|
|
except:
|
|
# Probably got deleted already on another thread
|
|
pass
|
|
pass
|