From 997dac3b9f63cc2f5aad9975fdae739d0c338435 Mon Sep 17 00:00:00 2001 From: Smittix Date: Fri, 27 Feb 2026 11:13:44 +0000 Subject: [PATCH] fix: ADS-B device release leak and startup performance Move adsb_active_device/sdr_type assignment to immediately after claim_sdr_device so stop_adsb() can always release the device, even during startup. Sync sdr_type_str after SDRType fallback to prevent claim/release key mismatch. Clear active device on all error paths. Replace blind 3s sleep for dump1090 readiness with port-polling loop (100ms intervals, 3s max). Replace subprocess.run() in rtl_test probe with Popen + select-based early termination on success/error detection. Co-Authored-By: Claude Opus 4.6 --- routes/adsb.py | 24 ++++- utils/sdr/detection.py | 196 ++++++++++++++++++++++++----------------- 2 files changed, 134 insertions(+), 86 deletions(-) diff --git a/routes/adsb.py b/routes/adsb.py index 60cc4a9..1c97020 100644 --- a/routes/adsb.py +++ b/routes/adsb.py @@ -758,6 +758,7 @@ def start_adsb(): sdr_type = SDRType(sdr_type_str) except ValueError: sdr_type = SDRType.RTL_SDR + sdr_type_str = sdr_type.value # For RTL-SDR, use dump1090. For other hardware, need readsb with SoapySDR if sdr_type == SDRType.RTL_SDR: @@ -796,6 +797,10 @@ def start_adsb(): 'message': error }), 409 + # Track claimed device immediately so stop_adsb() can always release it + adsb_active_device = device + adsb_active_sdr_type = sdr_type_str + # Create device object and build command via abstraction layer sdr_device = SDRFactory.create_default_device(sdr_type, index=device) builder = SDRFactory.get_builder(sdr_type) @@ -822,11 +827,24 @@ def start_adsb(): ) write_dump1090_pid(app_module.adsb_process.pid) - time.sleep(DUMP1090_START_WAIT) + # Poll for dump1090 readiness instead of blind sleep + dump1090_ready = False + poll_interval = 0.1 + elapsed = 0.0 + while elapsed < DUMP1090_START_WAIT: + if app_module.adsb_process.poll() is not None: + break # Process exited early — handle below + if check_dump1090_service(): + dump1090_ready = True + break + time.sleep(poll_interval) + elapsed += poll_interval if app_module.adsb_process.poll() is not None: # Process exited - release device and get error message app_module.release_sdr_device(device_int, sdr_type_str) + adsb_active_device = None + adsb_active_sdr_type = None stderr_output = '' if app_module.adsb_process.stderr: try: @@ -872,8 +890,6 @@ def start_adsb(): }) adsb_using_service = True - adsb_active_device = device # Track which device is being used - adsb_active_sdr_type = sdr_type_str thread = threading.Thread(target=parse_sbs_stream, args=(f'localhost:{ADSB_SBS_PORT}',), daemon=True) thread.start() @@ -894,6 +910,8 @@ def start_adsb(): except Exception as e: # Release device on failure app_module.release_sdr_device(device_int, sdr_type_str) + adsb_active_device = None + adsb_active_sdr_type = None return jsonify({'status': 'error', 'message': str(e)}) diff --git a/utils/sdr/detection.py b/utils/sdr/detection.py index cec3fc1..7d388aa 100644 --- a/utils/sdr/detection.py +++ b/utils/sdr/detection.py @@ -6,31 +6,31 @@ Detects RTL-SDR devices via rtl_test and other SDR hardware via SoapySDR. from __future__ import annotations -import logging -import re -import shutil -import subprocess -import time -from typing import Optional +import logging +import re +import shutil +import subprocess +import time +from typing import Optional from .base import SDRCapabilities, SDRDevice, SDRType -logger = logging.getLogger(__name__) - -# Cache HackRF detection results so polling endpoints don't repeatedly run -# hackrf_info while the device is actively streaming in SubGHz mode. -_hackrf_cache: list[SDRDevice] = [] -_hackrf_cache_ts: float = 0.0 -_HACKRF_CACHE_TTL_SECONDS = 3.0 - - -def _hackrf_probe_blocked() -> bool: - """Return True when probing HackRF would interfere with an active stream.""" - try: - from utils.subghz import get_subghz_manager - return get_subghz_manager().active_mode in {'rx', 'decode', 'tx', 'sweep'} - except Exception: - return False +logger = logging.getLogger(__name__) + +# Cache HackRF detection results so polling endpoints don't repeatedly run +# hackrf_info while the device is actively streaming in SubGHz mode. +_hackrf_cache: list[SDRDevice] = [] +_hackrf_cache_ts: float = 0.0 +_HACKRF_CACHE_TTL_SECONDS = 3.0 + + +def _hackrf_probe_blocked() -> bool: + """Return True when probing HackRF would interfere with an active stream.""" + try: + from utils.subghz import get_subghz_manager + return get_subghz_manager().active_mode in {'rx', 'decode', 'tx', 'sweep'} + except Exception: + return False def _check_tool(name: str) -> bool: @@ -112,21 +112,21 @@ def detect_rtlsdr_devices() -> list[SDRDevice]: lib_paths = ['/usr/local/lib', '/opt/homebrew/lib'] current_ld = env.get('DYLD_LIBRARY_PATH', '') env['DYLD_LIBRARY_PATH'] = ':'.join(lib_paths + [current_ld] if current_ld else lib_paths) - result = subprocess.run( - ['rtl_test', '-t'], - capture_output=True, - text=True, - encoding='utf-8', - errors='replace', - timeout=5, - env=env - ) - output = result.stderr + result.stdout - - # Parse device info from rtl_test output - # Format: "0: Realtek, RTL2838UHIDIR, SN: 00000001" - # Require a non-empty serial to avoid matching malformed lines like "SN:". - device_pattern = r'(\d+):\s+(.+?),\s*SN:\s*(\S+)\s*$' + result = subprocess.run( + ['rtl_test', '-t'], + capture_output=True, + text=True, + encoding='utf-8', + errors='replace', + timeout=5, + env=env + ) + output = result.stderr + result.stdout + + # Parse device info from rtl_test output + # Format: "0: Realtek, RTL2838UHIDIR, SN: 00000001" + # Require a non-empty serial to avoid matching malformed lines like "SN:". + device_pattern = r'(\d+):\s+(.+?),\s*SN:\s*(\S+)\s*$' from .rtlsdr import RTLSDRCommandBuilder @@ -134,14 +134,14 @@ def detect_rtlsdr_devices() -> list[SDRDevice]: line = line.strip() match = re.match(device_pattern, line) if match: - devices.append(SDRDevice( - sdr_type=SDRType.RTL_SDR, - index=int(match.group(1)), - name=match.group(2).strip().rstrip(','), - serial=match.group(3), - driver='rtlsdr', - capabilities=RTLSDRCommandBuilder.CAPABILITIES - )) + devices.append(SDRDevice( + sdr_type=SDRType.RTL_SDR, + index=int(match.group(1)), + name=match.group(2).strip().rstrip(','), + serial=match.group(3), + driver='rtlsdr', + capabilities=RTLSDRCommandBuilder.CAPABILITIES + )) # Fallback: if we found devices but couldn't parse details if not devices: @@ -314,29 +314,29 @@ def _add_soapy_device( )) -def detect_hackrf_devices() -> list[SDRDevice]: - """ - Detect HackRF devices using native hackrf_info tool. - - Fallback for when SoapySDR is not available. - """ - global _hackrf_cache, _hackrf_cache_ts - now = time.time() - - # While HackRF is actively streaming in SubGHz mode, skip probe calls. - # Re-running hackrf_info during active RX/TX can disrupt the USB stream. - if _hackrf_probe_blocked(): - return list(_hackrf_cache) - - if _hackrf_cache and (now - _hackrf_cache_ts) < _HACKRF_CACHE_TTL_SECONDS: - return list(_hackrf_cache) - - devices: list[SDRDevice] = [] - - if not _check_tool('hackrf_info'): - _hackrf_cache = devices - _hackrf_cache_ts = now - return devices +def detect_hackrf_devices() -> list[SDRDevice]: + """ + Detect HackRF devices using native hackrf_info tool. + + Fallback for when SoapySDR is not available. + """ + global _hackrf_cache, _hackrf_cache_ts + now = time.time() + + # While HackRF is actively streaming in SubGHz mode, skip probe calls. + # Re-running hackrf_info during active RX/TX can disrupt the USB stream. + if _hackrf_probe_blocked(): + return list(_hackrf_cache) + + if _hackrf_cache and (now - _hackrf_cache_ts) < _HACKRF_CACHE_TTL_SECONDS: + return list(_hackrf_cache) + + devices: list[SDRDevice] = [] + + if not _check_tool('hackrf_info'): + _hackrf_cache = devices + _hackrf_cache_ts = now + return devices try: result = subprocess.run( @@ -374,12 +374,12 @@ def detect_hackrf_devices() -> list[SDRDevice]: capabilities=HackRFCommandBuilder.CAPABILITIES )) - except Exception as e: - logger.debug(f"HackRF detection error: {e}") - - _hackrf_cache = list(devices) - _hackrf_cache_ts = now - return devices + except Exception as e: + logger.debug(f"HackRF detection error: {e}") + + _hackrf_cache = list(devices) + _hackrf_cache_ts = now + return devices def probe_rtlsdr_device(device_index: int) -> str | None: @@ -413,16 +413,50 @@ def probe_rtlsdr_device(device_index: int) -> str | None: lib_paths + [current_ld] if current_ld else lib_paths ) - result = subprocess.run( + # Use Popen with early termination instead of run() with full timeout. + # rtl_test prints device info to stderr quickly, then keeps running + # its test loop. We kill it as soon as we see success or failure. + proc = subprocess.Popen( ['rtl_test', '-d', str(device_index), '-t'], - capture_output=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, text=True, - timeout=3, env=env, ) - output = result.stderr + result.stdout - if 'usb_claim_interface' in output or 'Failed to open' in output: + import select + error_found = False + deadline = time.monotonic() + 3.0 + + try: + while time.monotonic() < deadline: + remaining = deadline - time.monotonic() + if remaining <= 0: + break + # Wait for stderr output with timeout + ready, _, _ = select.select( + [proc.stderr], [], [], min(remaining, 0.1) + ) + if ready: + line = proc.stderr.readline() + if not line: + break # EOF — process closed stderr + if 'usb_claim_interface' in line or 'Failed to open' in line: + error_found = True + break + if 'Found' in line and 'device' in line.lower(): + # Device opened successfully — no need to wait longer + break + if proc.poll() is not None: + break # Process exited + finally: + try: + proc.kill() + except OSError: + pass + proc.wait() + + if error_found: logger.warning( f"RTL-SDR device {device_index} USB probe failed: " f"device busy or unavailable" @@ -434,10 +468,6 @@ def probe_rtlsdr_device(device_index: int) -> str | None: f'or try a different device.' ) - except subprocess.TimeoutExpired: - # rtl_test opened the device successfully and is running the - # test — that means the device *is* available. - pass except Exception as e: logger.debug(f"RTL-SDR probe error for device {device_index}: {e}")