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:
Smittix
2026-02-27 11:13:44 +00:00
parent 121ea56519
commit fdb3af34ff
2 changed files with 134 additions and 86 deletions
+21 -3
View File
@@ -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)})
+39 -9
View File
@@ -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}")