mirror of
https://github.com/smittix/intercept.git
synced 2026-06-18 02:19:46 -07:00
fix: close leaked file descriptors on mode switch (#169)
SSE EventSource connections for AIS, ACARS, VDL2, and radiosonde were not closed when switching modes, causing fd exhaustion after repeated switches. Also fixes socket leaks on exception paths in AIS/ADS-B stream parsers, closes subprocess pipes in safe_terminate/cleanup, and caches skyfield timescale at module level to avoid per-request fd churn. Closes #169 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user