mirror of
https://github.com/smittix/intercept.git
synced 2026-06-08 22:21:55 -07:00
Fix waterfall-to-listen SDR busy race condition
Wait for server-side WebSocket stop confirmation before closing the connection, ensuring the IQ process is fully terminated and the USB device released. Add retry logic with back-off in the audio start endpoint as defense-in-depth for any remaining timing gaps. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+26
-11
@@ -1310,20 +1310,35 @@ def start_audio() -> Response:
|
||||
_stop_waterfall_internal()
|
||||
time.sleep(0.2)
|
||||
|
||||
# Release waterfall device claim if the WebSocket waterfall is still
|
||||
# holding it. The JS client sends a stop command and closes the
|
||||
# WebSocket before requesting audio, but the backend handler may not
|
||||
# have finished its cleanup yet.
|
||||
device_status = app_module.get_sdr_device_status()
|
||||
if device_status.get(device) == 'waterfall':
|
||||
app_module.release_sdr_device(device)
|
||||
time.sleep(0.3)
|
||||
|
||||
# Claim device for listening audio
|
||||
# Claim device for listening audio. The WebSocket waterfall handler
|
||||
# may still be tearing down its IQ capture process (thread join +
|
||||
# safe_terminate can take several seconds), so we retry with back-off
|
||||
# to give the USB device time to be fully released.
|
||||
if listening_active_device is None or listening_active_device != device:
|
||||
if listening_active_device is not None:
|
||||
app_module.release_sdr_device(listening_active_device)
|
||||
error = app_module.claim_sdr_device(device, 'listening')
|
||||
listening_active_device = None
|
||||
|
||||
error = None
|
||||
max_claim_attempts = 6
|
||||
for attempt in range(max_claim_attempts):
|
||||
# Force-release a stale waterfall registry entry on each
|
||||
# attempt — the WebSocket handler may not have finished
|
||||
# cleanup yet.
|
||||
device_status = app_module.get_sdr_device_status()
|
||||
if device_status.get(device) == 'waterfall':
|
||||
app_module.release_sdr_device(device)
|
||||
|
||||
error = app_module.claim_sdr_device(device, 'listening')
|
||||
if not error:
|
||||
break
|
||||
if attempt < max_claim_attempts - 1:
|
||||
logger.debug(
|
||||
f"Device claim attempt {attempt + 1}/{max_claim_attempts} "
|
||||
f"failed, retrying in 0.5s: {error}"
|
||||
)
|
||||
time.sleep(0.5)
|
||||
|
||||
if error:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
|
||||
@@ -3943,11 +3943,31 @@ async function stopWaterfall() {
|
||||
|
||||
// WebSocket path
|
||||
if (waterfallUseWebSocket && waterfallWebSocket) {
|
||||
const ws = waterfallWebSocket;
|
||||
try {
|
||||
if (waterfallWebSocket.readyState === WebSocket.OPEN) {
|
||||
waterfallWebSocket.send(JSON.stringify({ cmd: 'stop' }));
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
// Wait for server to confirm stop (it terminates the IQ
|
||||
// process and releases the USB device before responding).
|
||||
await new Promise((resolve) => {
|
||||
const timeout = setTimeout(resolve, 4000);
|
||||
const prevHandler = ws.onmessage;
|
||||
ws.onmessage = (event) => {
|
||||
if (typeof event.data === 'string') {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.status === 'stopped') {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
if (prevHandler) prevHandler(event);
|
||||
};
|
||||
ws.send(JSON.stringify({ cmd: 'stop' }));
|
||||
});
|
||||
}
|
||||
waterfallWebSocket.close();
|
||||
ws.close();
|
||||
} catch (e) {
|
||||
console.error('[WATERFALL] WebSocket stop error:', e);
|
||||
}
|
||||
@@ -3958,8 +3978,6 @@ async function stopWaterfall() {
|
||||
if (typeof releaseDevice === 'function') {
|
||||
releaseDevice('waterfall');
|
||||
}
|
||||
// Allow backend WebSocket handler to finish cleanup and release SDR
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user