diff --git a/routes/adsb.py b/routes/adsb.py index 26575c9..c718415 100644 --- a/routes/adsb.py +++ b/routes/adsb.py @@ -364,6 +364,7 @@ def parse_sbs_stream(service_addr): _sbs_error_logged = False while adsb_using_service: + sock = None try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(SBS_SOCKET_TIMEOUT) @@ -586,7 +587,6 @@ def parse_sbs_stream(service_addr): continue flush_pending_updates(force=True) - sock.close() adsb_connected = False except OSError as e: adsb_connected = False @@ -594,6 +594,12 @@ def parse_sbs_stream(service_addr): logger.warning(f"SBS connection error: {e}, reconnecting...") _sbs_error_logged = True time.sleep(SBS_RECONNECT_DELAY) + finally: + if sock: + try: + sock.close() + except OSError: + pass adsb_connected = False logger.info("SBS stream parser stopped") diff --git a/routes/ais.py b/routes/ais.py index 021fa24..6281172 100644 --- a/routes/ais.py +++ b/routes/ais.py @@ -80,6 +80,7 @@ def parse_ais_stream(port: int): _ais_error_logged = True while ais_running: + sock = None try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(AIS_SOCKET_TIMEOUT) @@ -152,7 +153,6 @@ def parse_ais_stream(port: int): except socket.timeout: continue - sock.close() ais_connected = False except OSError as e: ais_connected = False @@ -160,6 +160,12 @@ def parse_ais_stream(port: int): logger.warning(f"AIS connection error: {e}, reconnecting...") _ais_error_logged = True time.sleep(AIS_RECONNECT_DELAY) + finally: + if sock: + try: + sock.close() + except OSError: + pass ais_connected = False logger.info("AIS stream parser stopped") diff --git a/routes/satellite.py b/routes/satellite.py index f2e998b..36e303e 100644 --- a/routes/satellite.py +++ b/routes/satellite.py @@ -28,6 +28,17 @@ from utils.validation import validate_latitude, validate_longitude, validate_hou satellite_bp = Blueprint('satellite', __name__, url_prefix='/satellite') +# Cache skyfield timescale to avoid re-downloading/re-parsing per request +_cached_timescale = None + + +def _get_timescale(): + global _cached_timescale + if _cached_timescale is None: + from skyfield.api import load + _cached_timescale = load.timescale() + return _cached_timescale + # Maximum response size for external requests (1MB) MAX_RESPONSE_SIZE = 1024 * 1024 @@ -178,7 +189,7 @@ def satellite_dashboard(): def predict_passes(): """Calculate satellite passes using skyfield.""" try: - from skyfield.api import load, wgs84, EarthSatellite + from skyfield.api import wgs84, EarthSatellite from skyfield.almanac import find_discrete except ImportError: return jsonify({ @@ -219,7 +230,7 @@ def predict_passes(): } name_to_norad = {v: k for k, v in norad_to_name.items()} - ts = load.timescale() + ts = _get_timescale() observer = wgs84.latlon(lat, lon) t0 = ts.now() @@ -332,7 +343,7 @@ def predict_passes(): def get_satellite_position(): """Get real-time positions of satellites.""" try: - from skyfield.api import load, wgs84, EarthSatellite + from skyfield.api import wgs84, EarthSatellite except ImportError: return jsonify({'status': 'error', 'message': 'skyfield not installed'}), 503 @@ -361,7 +372,7 @@ def get_satellite_position(): else: satellites.append(sat) - ts = load.timescale() + ts = _get_timescale() observer = wgs84.latlon(lat, lon) now = ts.now() now_dt = now.utc_datetime() diff --git a/templates/index.html b/templates/index.html index 208c6d6..ef6ff78 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4158,6 +4158,10 @@ sstv_general: () => typeof SSTVGeneral !== 'undefined' && SSTVGeneral.destroy?.(), websdr: () => typeof WebSDR !== 'undefined' && WebSDR.destroy?.(), spystations: () => typeof SpyStations !== 'undefined' && SpyStations.destroy?.(), + ais: () => { if (aisEventSource) { aisEventSource.close(); aisEventSource = null; } }, + acars: () => { if (acarsMainEventSource) { acarsMainEventSource.close(); acarsMainEventSource = null; } }, + vdl2: () => { if (vdl2MainEventSource) { vdl2MainEventSource.close(); vdl2MainEventSource = null; } }, + radiosonde: () => { if (radiosondeEventSource) { radiosondeEventSource.close(); radiosondeEventSource = null; } }, }; if (previousMode && previousMode !== mode && moduleDestroyMap[previousMode]) { try { moduleDestroyMap[previousMode](); } catch(e) { console.warn(`[switchMode] destroy ${previousMode} failed:`, e); } diff --git a/templates/partials/modes/acars.html b/templates/partials/modes/acars.html index 64de896..ef722e9 100644 --- a/templates/partials/modes/acars.html +++ b/templates/partials/modes/acars.html @@ -254,7 +254,9 @@ acarsMainEventSource.onerror = function() { setTimeout(() => { - if (document.getElementById('stopAcarsBtn').style.display === 'block') { + const panel = document.getElementById('acarsMode'); + if (panel && panel.classList.contains('active') && + document.getElementById('stopAcarsBtn').style.display === 'block') { startAcarsMainSSE(); } }, 2000); diff --git a/templates/partials/modes/ais.html b/templates/partials/modes/ais.html index a2155c0..dacab19 100644 --- a/templates/partials/modes/ais.html +++ b/templates/partials/modes/ais.html @@ -164,7 +164,9 @@ aisEventSource.onerror = function() { setTimeout(() => { - if (document.getElementById('stopAisBtn').style.display === 'block') { + const panel = document.getElementById('aisMode'); + if (panel && panel.classList.contains('active') && + document.getElementById('stopAisBtn').style.display === 'block') { startAisSSE(); } }, 2000); diff --git a/templates/partials/modes/radiosonde.html b/templates/partials/modes/radiosonde.html index 1809868..fa3c4e3 100644 --- a/templates/partials/modes/radiosonde.html +++ b/templates/partials/modes/radiosonde.html @@ -220,7 +220,9 @@ radiosondeEventSource.onerror = function() { setTimeout(() => { - if (document.getElementById('stopRadiosondeBtn').style.display === 'block') { + const panel = document.getElementById('radiosondeMode'); + if (panel && panel.classList.contains('active') && + document.getElementById('stopRadiosondeBtn').style.display === 'block') { startRadiosondeSSE(); } }, 2000); diff --git a/templates/partials/modes/vdl2.html b/templates/partials/modes/vdl2.html index 768f742..8a12eb6 100644 --- a/templates/partials/modes/vdl2.html +++ b/templates/partials/modes/vdl2.html @@ -169,32 +169,32 @@ .catch(err => alert('Error: ' + err.message)); } - function stopVdl2Mode() { - fetch('/vdl2/stop', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ source: 'vdl2_mode' }) - }) - .then(async (r) => { - const text = await r.text(); - const data = text ? JSON.parse(text) : {}; - if (!r.ok || (data.status !== 'stopped' && data.status !== 'success')) { - throw new Error(data.message || `HTTP ${r.status}`); - } - return data; - }) - .then(() => { - document.getElementById('startVdl2Btn').style.display = 'block'; - document.getElementById('stopVdl2Btn').style.display = 'none'; - document.getElementById('vdl2StatusText').textContent = 'Standby'; - document.getElementById('vdl2StatusText').style.color = 'var(--accent-yellow)'; - if (vdl2MainEventSource) { - vdl2MainEventSource.close(); - vdl2MainEventSource = null; - } - }) - .catch(err => alert('Failed to stop VDL2: ' + err.message)); - } + function stopVdl2Mode() { + fetch('/vdl2/stop', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ source: 'vdl2_mode' }) + }) + .then(async (r) => { + const text = await r.text(); + const data = text ? JSON.parse(text) : {}; + if (!r.ok || (data.status !== 'stopped' && data.status !== 'success')) { + throw new Error(data.message || `HTTP ${r.status}`); + } + return data; + }) + .then(() => { + document.getElementById('startVdl2Btn').style.display = 'block'; + document.getElementById('stopVdl2Btn').style.display = 'none'; + document.getElementById('vdl2StatusText').textContent = 'Standby'; + document.getElementById('vdl2StatusText').style.color = 'var(--accent-yellow)'; + if (vdl2MainEventSource) { + vdl2MainEventSource.close(); + vdl2MainEventSource = null; + } + }) + .catch(err => alert('Failed to stop VDL2: ' + err.message)); + } function startVdl2MainSSE() { if (vdl2MainEventSource) vdl2MainEventSource.close(); @@ -212,7 +212,9 @@ vdl2MainEventSource.onerror = function() { setTimeout(() => { - if (document.getElementById('stopVdl2Btn').style.display === 'block') { + const panel = document.getElementById('vdl2Mode'); + if (panel && panel.classList.contains('active') && + document.getElementById('stopVdl2Btn').style.display === 'block') { startVdl2MainSSE(); } }, 2000); diff --git a/utils/process.py b/utils/process.py index 3df7d68..f3d72dc 100644 --- a/utils/process.py +++ b/utils/process.py @@ -34,6 +34,16 @@ def unregister_process(process: subprocess.Popen) -> None: _spawned_processes.remove(process) +def close_process_pipes(process: subprocess.Popen) -> None: + """Close stdin/stdout/stderr pipes on a subprocess to free file descriptors.""" + for pipe in (process.stdin, process.stdout, process.stderr): + if pipe: + try: + pipe.close() + except OSError: + pass + + def cleanup_all_processes() -> None: """Clean up all registered processes on exit.""" logger.info("Cleaning up all spawned processes...") @@ -47,6 +57,7 @@ def cleanup_all_processes() -> None: process.kill() except Exception as e: logger.warning(f"Error cleaning up process: {e}") + close_process_pipes(process) _spawned_processes.clear() @@ -66,12 +77,14 @@ def safe_terminate(process: subprocess.Popen | None, timeout: float = 2.0) -> bo if process.poll() is not None: # Already dead + close_process_pipes(process) unregister_process(process) return False try: process.terminate() process.wait(timeout=timeout) + close_process_pipes(process) unregister_process(process) return True except subprocess.TimeoutExpired: @@ -80,10 +93,12 @@ def safe_terminate(process: subprocess.Popen | None, timeout: float = 2.0) -> bo process.wait(timeout=3) except subprocess.TimeoutExpired: pass + close_process_pipes(process) unregister_process(process) return True except Exception as e: logger.warning(f"Error terminating process: {e}") + close_process_pipes(process) return False diff --git a/utils/weather_sat_predict.py b/utils/weather_sat_predict.py index de8f538..6ed824f 100644 --- a/utils/weather_sat_predict.py +++ b/utils/weather_sat_predict.py @@ -13,6 +13,17 @@ from utils.weather_sat import WEATHER_SATELLITES logger = get_logger('intercept.weather_sat_predict') +# Cache skyfield timescale to avoid re-downloading/re-parsing per request +_cached_timescale = None + + +def _get_timescale(): + global _cached_timescale + if _cached_timescale is None: + from skyfield.api import load + _cached_timescale = load.timescale() + return _cached_timescale + def _format_utc_iso(dt: datetime.datetime) -> str: """Return an ISO8601 UTC timestamp with a single timezone designator.""" @@ -48,7 +59,7 @@ def predict_passes( ImportError: If skyfield is not installed. """ from skyfield.almanac import find_discrete - from skyfield.api import EarthSatellite, load, wgs84 + from skyfield.api import EarthSatellite, wgs84 from data.satellites import TLE_SATELLITES @@ -61,7 +72,7 @@ def predict_passes( except ImportError: pass - ts = load.timescale() + ts = _get_timescale() observer = wgs84.latlon(lat, lon) t0 = ts.now() t1 = ts.utc(t0.utc_datetime() + datetime.timedelta(hours=hours))