mirror of
https://github.com/smittix/intercept.git
synced 2026-07-05 16:18:12 -07:00
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 <noreply@anthropic.com>
This commit is contained in:
+21
-3
@@ -758,6 +758,7 @@ def start_adsb():
|
|||||||
sdr_type = SDRType(sdr_type_str)
|
sdr_type = SDRType(sdr_type_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
sdr_type = SDRType.RTL_SDR
|
sdr_type = SDRType.RTL_SDR
|
||||||
|
sdr_type_str = sdr_type.value
|
||||||
|
|
||||||
# For RTL-SDR, use dump1090. For other hardware, need readsb with SoapySDR
|
# For RTL-SDR, use dump1090. For other hardware, need readsb with SoapySDR
|
||||||
if sdr_type == SDRType.RTL_SDR:
|
if sdr_type == SDRType.RTL_SDR:
|
||||||
@@ -796,6 +797,10 @@ def start_adsb():
|
|||||||
'message': error
|
'message': error
|
||||||
}), 409
|
}), 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
|
# Create device object and build command via abstraction layer
|
||||||
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
|
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
|
||||||
builder = SDRFactory.get_builder(sdr_type)
|
builder = SDRFactory.get_builder(sdr_type)
|
||||||
@@ -822,11 +827,24 @@ def start_adsb():
|
|||||||
)
|
)
|
||||||
write_dump1090_pid(app_module.adsb_process.pid)
|
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:
|
if app_module.adsb_process.poll() is not None:
|
||||||
# Process exited - release device and get error message
|
# Process exited - release device and get error message
|
||||||
app_module.release_sdr_device(device_int, sdr_type_str)
|
app_module.release_sdr_device(device_int, sdr_type_str)
|
||||||
|
adsb_active_device = None
|
||||||
|
adsb_active_sdr_type = None
|
||||||
stderr_output = ''
|
stderr_output = ''
|
||||||
if app_module.adsb_process.stderr:
|
if app_module.adsb_process.stderr:
|
||||||
try:
|
try:
|
||||||
@@ -872,8 +890,6 @@ def start_adsb():
|
|||||||
})
|
})
|
||||||
|
|
||||||
adsb_using_service = True
|
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 = threading.Thread(target=parse_sbs_stream, args=(f'localhost:{ADSB_SBS_PORT}',), daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
@@ -894,6 +910,8 @@ def start_adsb():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Release device on failure
|
# Release device on failure
|
||||||
app_module.release_sdr_device(device_int, sdr_type_str)
|
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)})
|
return jsonify({'status': 'error', 'message': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+113
-83
@@ -6,31 +6,31 @@ Detects RTL-SDR devices via rtl_test and other SDR hardware via SoapySDR.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from .base import SDRCapabilities, SDRDevice, SDRType
|
from .base import SDRCapabilities, SDRDevice, SDRType
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Cache HackRF detection results so polling endpoints don't repeatedly run
|
# Cache HackRF detection results so polling endpoints don't repeatedly run
|
||||||
# hackrf_info while the device is actively streaming in SubGHz mode.
|
# hackrf_info while the device is actively streaming in SubGHz mode.
|
||||||
_hackrf_cache: list[SDRDevice] = []
|
_hackrf_cache: list[SDRDevice] = []
|
||||||
_hackrf_cache_ts: float = 0.0
|
_hackrf_cache_ts: float = 0.0
|
||||||
_HACKRF_CACHE_TTL_SECONDS = 3.0
|
_HACKRF_CACHE_TTL_SECONDS = 3.0
|
||||||
|
|
||||||
|
|
||||||
def _hackrf_probe_blocked() -> bool:
|
def _hackrf_probe_blocked() -> bool:
|
||||||
"""Return True when probing HackRF would interfere with an active stream."""
|
"""Return True when probing HackRF would interfere with an active stream."""
|
||||||
try:
|
try:
|
||||||
from utils.subghz import get_subghz_manager
|
from utils.subghz import get_subghz_manager
|
||||||
return get_subghz_manager().active_mode in {'rx', 'decode', 'tx', 'sweep'}
|
return get_subghz_manager().active_mode in {'rx', 'decode', 'tx', 'sweep'}
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _check_tool(name: str) -> bool:
|
def _check_tool(name: str) -> bool:
|
||||||
@@ -112,21 +112,21 @@ def detect_rtlsdr_devices() -> list[SDRDevice]:
|
|||||||
lib_paths = ['/usr/local/lib', '/opt/homebrew/lib']
|
lib_paths = ['/usr/local/lib', '/opt/homebrew/lib']
|
||||||
current_ld = env.get('DYLD_LIBRARY_PATH', '')
|
current_ld = env.get('DYLD_LIBRARY_PATH', '')
|
||||||
env['DYLD_LIBRARY_PATH'] = ':'.join(lib_paths + [current_ld] if current_ld else lib_paths)
|
env['DYLD_LIBRARY_PATH'] = ':'.join(lib_paths + [current_ld] if current_ld else lib_paths)
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['rtl_test', '-t'],
|
['rtl_test', '-t'],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
encoding='utf-8',
|
encoding='utf-8',
|
||||||
errors='replace',
|
errors='replace',
|
||||||
timeout=5,
|
timeout=5,
|
||||||
env=env
|
env=env
|
||||||
)
|
)
|
||||||
output = result.stderr + result.stdout
|
output = result.stderr + result.stdout
|
||||||
|
|
||||||
# Parse device info from rtl_test output
|
# Parse device info from rtl_test output
|
||||||
# Format: "0: Realtek, RTL2838UHIDIR, SN: 00000001"
|
# Format: "0: Realtek, RTL2838UHIDIR, SN: 00000001"
|
||||||
# Require a non-empty serial to avoid matching malformed lines like "SN:".
|
# Require a non-empty serial to avoid matching malformed lines like "SN:".
|
||||||
device_pattern = r'(\d+):\s+(.+?),\s*SN:\s*(\S+)\s*$'
|
device_pattern = r'(\d+):\s+(.+?),\s*SN:\s*(\S+)\s*$'
|
||||||
|
|
||||||
from .rtlsdr import RTLSDRCommandBuilder
|
from .rtlsdr import RTLSDRCommandBuilder
|
||||||
|
|
||||||
@@ -134,14 +134,14 @@ def detect_rtlsdr_devices() -> list[SDRDevice]:
|
|||||||
line = line.strip()
|
line = line.strip()
|
||||||
match = re.match(device_pattern, line)
|
match = re.match(device_pattern, line)
|
||||||
if match:
|
if match:
|
||||||
devices.append(SDRDevice(
|
devices.append(SDRDevice(
|
||||||
sdr_type=SDRType.RTL_SDR,
|
sdr_type=SDRType.RTL_SDR,
|
||||||
index=int(match.group(1)),
|
index=int(match.group(1)),
|
||||||
name=match.group(2).strip().rstrip(','),
|
name=match.group(2).strip().rstrip(','),
|
||||||
serial=match.group(3),
|
serial=match.group(3),
|
||||||
driver='rtlsdr',
|
driver='rtlsdr',
|
||||||
capabilities=RTLSDRCommandBuilder.CAPABILITIES
|
capabilities=RTLSDRCommandBuilder.CAPABILITIES
|
||||||
))
|
))
|
||||||
|
|
||||||
# Fallback: if we found devices but couldn't parse details
|
# Fallback: if we found devices but couldn't parse details
|
||||||
if not devices:
|
if not devices:
|
||||||
@@ -314,29 +314,29 @@ def _add_soapy_device(
|
|||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
def detect_hackrf_devices() -> list[SDRDevice]:
|
def detect_hackrf_devices() -> list[SDRDevice]:
|
||||||
"""
|
"""
|
||||||
Detect HackRF devices using native hackrf_info tool.
|
Detect HackRF devices using native hackrf_info tool.
|
||||||
|
|
||||||
Fallback for when SoapySDR is not available.
|
Fallback for when SoapySDR is not available.
|
||||||
"""
|
"""
|
||||||
global _hackrf_cache, _hackrf_cache_ts
|
global _hackrf_cache, _hackrf_cache_ts
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
# While HackRF is actively streaming in SubGHz mode, skip probe calls.
|
# While HackRF is actively streaming in SubGHz mode, skip probe calls.
|
||||||
# Re-running hackrf_info during active RX/TX can disrupt the USB stream.
|
# Re-running hackrf_info during active RX/TX can disrupt the USB stream.
|
||||||
if _hackrf_probe_blocked():
|
if _hackrf_probe_blocked():
|
||||||
return list(_hackrf_cache)
|
return list(_hackrf_cache)
|
||||||
|
|
||||||
if _hackrf_cache and (now - _hackrf_cache_ts) < _HACKRF_CACHE_TTL_SECONDS:
|
if _hackrf_cache and (now - _hackrf_cache_ts) < _HACKRF_CACHE_TTL_SECONDS:
|
||||||
return list(_hackrf_cache)
|
return list(_hackrf_cache)
|
||||||
|
|
||||||
devices: list[SDRDevice] = []
|
devices: list[SDRDevice] = []
|
||||||
|
|
||||||
if not _check_tool('hackrf_info'):
|
if not _check_tool('hackrf_info'):
|
||||||
_hackrf_cache = devices
|
_hackrf_cache = devices
|
||||||
_hackrf_cache_ts = now
|
_hackrf_cache_ts = now
|
||||||
return devices
|
return devices
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
@@ -374,12 +374,12 @@ def detect_hackrf_devices() -> list[SDRDevice]:
|
|||||||
capabilities=HackRFCommandBuilder.CAPABILITIES
|
capabilities=HackRFCommandBuilder.CAPABILITIES
|
||||||
))
|
))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"HackRF detection error: {e}")
|
logger.debug(f"HackRF detection error: {e}")
|
||||||
|
|
||||||
_hackrf_cache = list(devices)
|
_hackrf_cache = list(devices)
|
||||||
_hackrf_cache_ts = now
|
_hackrf_cache_ts = now
|
||||||
return devices
|
return devices
|
||||||
|
|
||||||
|
|
||||||
def probe_rtlsdr_device(device_index: int) -> str | None:
|
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
|
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'],
|
['rtl_test', '-d', str(device_index), '-t'],
|
||||||
capture_output=True,
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=3,
|
|
||||||
env=env,
|
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(
|
logger.warning(
|
||||||
f"RTL-SDR device {device_index} USB probe failed: "
|
f"RTL-SDR device {device_index} USB probe failed: "
|
||||||
f"device busy or unavailable"
|
f"device busy or unavailable"
|
||||||
@@ -434,10 +468,6 @@ def probe_rtlsdr_device(device_index: int) -> str | None:
|
|||||||
f'or try a different device.'
|
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:
|
except Exception as e:
|
||||||
logger.debug(f"RTL-SDR probe error for device {device_index}: {e}")
|
logger.debug(f"RTL-SDR probe error for device {device_index}: {e}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user