mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -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:
@@ -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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user