Improve mode stop responsiveness and timeout handling

This commit is contained in:
Smittix
2026-02-23 17:53:50 +00:00
parent ddb9f06550
commit e20671de5e
5 changed files with 371 additions and 238 deletions
+19 -4
View File
@@ -946,17 +946,32 @@ const BluetoothMode = (function() {
async function stopScan() { async function stopScan() {
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local'; const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
const timeoutMs = isAgentMode ? 8000 : 2200;
const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
const timeoutId = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
// Optimistic UI teardown keeps mode changes responsive.
setScanning(false);
stopEventStream();
try { try {
if (isAgentMode) { if (isAgentMode) {
await fetch(`/controller/agents/${currentAgent}/bluetooth/stop`, { method: 'POST' }); await fetch(`/controller/agents/${currentAgent}/bluetooth/stop`, {
method: 'POST',
...(controller ? { signal: controller.signal } : {}),
});
} else { } else {
await fetch('/api/bluetooth/scan/stop', { method: 'POST' }); await fetch('/api/bluetooth/scan/stop', {
method: 'POST',
...(controller ? { signal: controller.signal } : {}),
});
} }
setScanning(false);
stopEventStream();
} catch (err) { } catch (err) {
console.error('Failed to stop scan:', err); console.error('Failed to stop scan:', err);
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
} }
} }
+19 -4
View File
@@ -590,20 +590,35 @@ const WiFiMode = (function() {
eventSource = null; eventSource = null;
} }
// Update UI immediately so mode transitions are responsive even if the
// backend needs extra time to terminate subprocesses.
setScanning(false);
// Stop scan on server (local or agent) // Stop scan on server (local or agent)
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local'; const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
const timeoutMs = isAgentMode ? 8000 : 2200;
const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
const timeoutId = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
try { try {
if (isAgentMode) { if (isAgentMode) {
await fetch(`/controller/agents/${currentAgent}/wifi/stop`, { method: 'POST' }); await fetch(`/controller/agents/${currentAgent}/wifi/stop`, {
method: 'POST',
...(controller ? { signal: controller.signal } : {}),
});
} else if (scanMode === 'deep') { } else if (scanMode === 'deep') {
await fetch(`${CONFIG.apiBase}/scan/stop`, { method: 'POST' }); await fetch(`${CONFIG.apiBase}/scan/stop`, {
method: 'POST',
...(controller ? { signal: controller.signal } : {}),
});
} }
} catch (error) { } catch (error) {
console.warn('[WiFiMode] Error stopping scan:', error); console.warn('[WiFiMode] Error stopping scan:', error);
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
} }
setScanning(false);
} }
function setScanning(scanning, mode = null) { function setScanning(scanning, mode = null) {
+177 -125
View File
@@ -3602,26 +3602,89 @@
} }
} }
const LOCAL_STOP_TIMEOUT_MS = 2200;
const REMOTE_STOP_TIMEOUT_MS = 8000;
function postStopRequest(url, timeoutMs = LOCAL_STOP_TIMEOUT_MS) {
const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
const timeoutId = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
const started = performance.now();
return fetch(url, {
method: 'POST',
...(controller ? { signal: controller.signal } : {}),
})
.then((response) => response.json().catch(() => ({ status: response.ok ? 'ok' : 'error' })))
.catch((err) => {
if (err && err.name === 'AbortError') {
console.warn(`[Stop] ${url} timed out after ${timeoutMs}ms`);
return { status: 'timeout', timed_out: true };
}
console.warn(`[Stop] ${url} failed: ${err?.message || err}`);
return { status: 'error', message: err?.message || String(err) };
})
.finally(() => {
if (timeoutId) clearTimeout(timeoutId);
const elapsedMs = Math.round(performance.now() - started);
console.debug(`[Stop] ${url} finished in ${elapsedMs}ms`);
});
}
async function awaitStopAction(name, action, timeoutMs = LOCAL_STOP_TIMEOUT_MS) {
const started = performance.now();
try {
const result = action();
const promise = (result && typeof result.then === 'function')
? result
: Promise.resolve(result);
await Promise.race([
promise,
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
]);
} catch (err) {
console.warn(`[ModeSwitch] stop ${name} failed: ${err?.message || err}`);
} finally {
const elapsedMs = Math.round(performance.now() - started);
console.debug(`[ModeSwitch] stop ${name} finished in ${elapsedMs}ms`);
}
}
// Mode switching // Mode switching
function switchMode(mode, options = {}) { async function switchMode(mode, options = {}) {
const { updateUrl = true } = options; const { updateUrl = true } = options;
if (mode === 'listening') mode = 'waterfall'; if (mode === 'listening') mode = 'waterfall';
if (!validModes.has(mode)) mode = 'pager'; if (!validModes.has(mode)) mode = 'pager';
// Only stop local scans if in local mode (not agent mode) // Only stop local scans if in local mode (not agent mode)
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local'; const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
if (!isAgentMode) { if (!isAgentMode) {
if (isRunning) stopDecoding(); if (isRunning) {
if (isSensorRunning) stopSensorDecoding(); await awaitStopAction('pager', () => stopDecoding(), LOCAL_STOP_TIMEOUT_MS);
if (isWifiRunning) stopWifiScan(); }
if (isSensorRunning) {
await awaitStopAction('sensor', () => stopSensorDecoding(), LOCAL_STOP_TIMEOUT_MS);
}
const wifiScanActive = (
typeof WiFiMode !== 'undefined'
&& typeof WiFiMode.isScanning === 'function'
&& WiFiMode.isScanning()
) || isWifiRunning;
if (wifiScanActive) {
await awaitStopAction('wifi', () => stopWifiScan(), LOCAL_STOP_TIMEOUT_MS);
}
const btScanActive = (typeof BluetoothMode !== 'undefined' && const btScanActive = (typeof BluetoothMode !== 'undefined' &&
typeof BluetoothMode.isScanning === 'function' && typeof BluetoothMode.isScanning === 'function' &&
BluetoothMode.isScanning()) || isBtRunning; BluetoothMode.isScanning()) || isBtRunning;
const isBtModeTransition = const isBtModeTransition =
(currentMode === 'bluetooth' && mode === 'bt_locate') || (currentMode === 'bluetooth' && mode === 'bt_locate') ||
(currentMode === 'bt_locate' && mode === 'bluetooth'); (currentMode === 'bt_locate' && mode === 'bluetooth');
if (btScanActive && !isBtModeTransition && typeof stopBtScan === 'function') stopBtScan(); if (btScanActive && !isBtModeTransition && typeof stopBtScan === 'function') {
if (isAprsRunning) stopAprs(); await awaitStopAction('bluetooth', () => stopBtScan(), LOCAL_STOP_TIMEOUT_MS);
if (isTscmRunning) stopTscmSweep(); }
if (isAprsRunning) {
await awaitStopAction('aprs', () => stopAprs(), LOCAL_STOP_TIMEOUT_MS);
}
if (isTscmRunning) {
await awaitStopAction('tscm', () => stopTscmSweep(), LOCAL_STOP_TIMEOUT_MS);
}
} }
// Clean up SubGHz SSE connection when leaving the mode // Clean up SubGHz SSE connection when leaving the mode
@@ -4343,35 +4406,31 @@
// Stop sensor decoding // Stop sensor decoding
function stopSensorDecoding() { function stopSensorDecoding() {
// Check if using remote agent const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
if (typeof currentAgent !== 'undefined' && currentAgent !== 'local') { const endpoint = isAgentMode
fetch(`/controller/agents/${currentAgent}/sensor/stop`, { method: 'POST' }) ? `/controller/agents/${currentAgent}/sensor/stop`
.then(r => r.json()) : '/stop_sensor';
.then(data => { const timeoutMs = isAgentMode ? REMOTE_STOP_TIMEOUT_MS : LOCAL_STOP_TIMEOUT_MS;
setSensorRunning(false);
if (eventSource) { setSensorRunning(false);
eventSource.close(); if (eventSource) {
eventSource = null; eventSource.close();
} eventSource = null;
if (agentPollInterval) { }
clearInterval(agentPollInterval); if (agentPollInterval) {
agentPollInterval = null; clearInterval(agentPollInterval);
} agentPollInterval = null;
showInfo('Sensor stopped on remote agent'); }
}); if (!isAgentMode) {
return; releaseDevice('sensor');
} }
fetch('/stop_sensor', { method: 'POST' }) return postStopRequest(endpoint, timeoutMs).then((data) => {
.then(r => r.json()) if (isAgentMode && data && data.status !== 'error' && data.status !== 'timeout') {
.then(data => { showInfo('Sensor stopped on remote agent');
releaseDevice('sensor'); }
setSensorRunning(false); return data;
if (eventSource) { });
eventSource.close();
eventSource = null;
}
});
} }
// Polling interval for agent data // Polling interval for agent data
@@ -4685,25 +4744,23 @@
const endpoint = isAgentMode const endpoint = isAgentMode
? `/controller/agents/${rtlamrCurrentAgent}/rtlamr/stop` ? `/controller/agents/${rtlamrCurrentAgent}/rtlamr/stop`
: '/stop_rtlamr'; : '/stop_rtlamr';
const timeoutMs = isAgentMode ? REMOTE_STOP_TIMEOUT_MS : LOCAL_STOP_TIMEOUT_MS;
fetch(endpoint, { method: 'POST' }) rtlamrCurrentAgent = null;
.then(r => r.json()) setRtlamrRunning(false);
.then(data => { if (eventSource) {
if (!isAgentMode) { eventSource.close();
releaseDevice('rtlamr'); eventSource = null;
} }
rtlamrCurrentAgent = null; if (rtlamrPollTimer) {
setRtlamrRunning(false); clearInterval(rtlamrPollTimer);
if (eventSource) { rtlamrPollTimer = null;
eventSource.close(); }
eventSource = null; if (!isAgentMode) {
} releaseDevice('rtlamr');
// Clear polling timer }
if (rtlamrPollTimer) {
clearInterval(rtlamrPollTimer); return postStopRequest(endpoint, timeoutMs);
rtlamrPollTimer = null;
}
});
} }
function setRtlamrRunning(running) { function setRtlamrRunning(running) {
@@ -5727,24 +5784,22 @@
const endpoint = isAgentMode const endpoint = isAgentMode
? `/controller/agents/${currentAgent}/pager/stop` ? `/controller/agents/${currentAgent}/pager/stop`
: '/stop'; : '/stop';
const timeoutMs = isAgentMode ? REMOTE_STOP_TIMEOUT_MS : LOCAL_STOP_TIMEOUT_MS;
fetch(endpoint, { method: 'POST' }) if (!isAgentMode) {
.then(r => r.json()) releaseDevice('pager');
.then(data => { }
if (!isAgentMode) { setRunning(false);
releaseDevice('pager'); if (eventSource) {
} eventSource.close();
setRunning(false); eventSource = null;
if (eventSource) { }
eventSource.close(); if (pagerPollTimer) {
eventSource = null; clearInterval(pagerPollTimer);
} pagerPollTimer = null;
// Clear polling timer if active }
if (pagerPollTimer) {
clearInterval(pagerPollTimer); return postStopRequest(endpoint, timeoutMs);
pagerPollTimer = null;
}
});
} }
function killAll() { function killAll() {
@@ -7642,15 +7697,19 @@
// Stop WiFi scan // Stop WiFi scan
function stopWifiScan() { function stopWifiScan() {
fetch('/wifi/scan/stop', { method: 'POST' }) setWifiRunning(false);
.then(r => r.json()) if (wifiEventSource) {
.then(data => { wifiEventSource.close();
setWifiRunning(false); wifiEventSource = null;
if (wifiEventSource) { }
wifiEventSource.close();
wifiEventSource = null; if (typeof WiFiMode !== 'undefined' && typeof WiFiMode.stopScan === 'function') {
} return Promise.resolve(WiFiMode.stopScan()).catch((err) => {
console.warn('[WiFi] stop via WiFiMode failed:', err);
}); });
}
return postStopRequest('/wifi/scan/stop', LOCAL_STOP_TIMEOUT_MS);
} }
function setWifiRunning(running) { function setWifiRunning(running) {
@@ -8856,10 +8915,14 @@
function stopBtScan() { function stopBtScan() {
const bt = getBluetoothModeApi(); const bt = getBluetoothModeApi();
let stopPromise = Promise.resolve();
if (bt && typeof bt.stopScan === 'function') { if (bt && typeof bt.stopScan === 'function') {
bt.stopScan(); stopPromise = Promise.resolve(bt.stopScan()).catch((err) => {
console.warn('[BT] stop failed:', err);
});
} }
setTimeout(syncBtRunningState, 0); setTimeout(syncBtRunningState, 0);
return stopPromise;
} }
function setBtRunning(running) { function setBtRunning(running) {
@@ -9175,44 +9238,36 @@
const endpoint = isAgentMode const endpoint = isAgentMode
? `/controller/agents/${aprsCurrentAgent}/aprs/stop` ? `/controller/agents/${aprsCurrentAgent}/aprs/stop`
: '/aprs/stop'; : '/aprs/stop';
const timeoutMs = isAgentMode ? REMOTE_STOP_TIMEOUT_MS : LOCAL_STOP_TIMEOUT_MS;
fetch(endpoint, { method: 'POST' }) isAprsRunning = false;
.then(r => r.json()) aprsCurrentAgent = null;
.then(data => { document.getElementById('aprsStripStartBtn').style.display = 'inline-block';
isAprsRunning = false; document.getElementById('aprsStripStopBtn').style.display = 'none';
aprsCurrentAgent = null; document.getElementById('aprsMapStatus').textContent = 'STANDBY';
// Update function bar buttons document.getElementById('aprsMapStatus').style.color = '';
document.getElementById('aprsStripStartBtn').style.display = 'inline-block'; updateAprsStatus('standby');
document.getElementById('aprsStripStopBtn').style.display = 'none'; document.getElementById('aprsStripFreq').textContent = '--';
// Update map status document.getElementById('aprsStripSignal').textContent = '--';
document.getElementById('aprsMapStatus').textContent = 'STANDBY'; document.getElementById('aprsStripRegion').disabled = false;
document.getElementById('aprsMapStatus').style.color = ''; document.getElementById('aprsStripGain').disabled = false;
// Reset function bar status const customFreqInput = document.getElementById('aprsStripCustomFreq');
updateAprsStatus('standby'); if (customFreqInput) customFreqInput.disabled = false;
document.getElementById('aprsStripFreq').textContent = '--'; const signalStat = document.getElementById('aprsStripSignalStat');
document.getElementById('aprsStripSignal').textContent = '--'; if (signalStat) {
// Re-enable controls signalStat.classList.remove('good', 'warning', 'poor');
document.getElementById('aprsStripRegion').disabled = false; }
document.getElementById('aprsStripGain').disabled = false; stopAprsMeterCheck();
const customFreqInput = document.getElementById('aprsStripCustomFreq'); if (aprsEventSource) {
if (customFreqInput) customFreqInput.disabled = false; aprsEventSource.close();
// Remove signal quality class aprsEventSource = null;
const signalStat = document.getElementById('aprsStripSignalStat'); }
if (signalStat) { if (aprsPollTimer) {
signalStat.classList.remove('good', 'warning', 'poor'); clearInterval(aprsPollTimer);
} aprsPollTimer = null;
// Stop meter check interval }
stopAprsMeterCheck();
if (aprsEventSource) { return postStopRequest(endpoint, timeoutMs);
aprsEventSource.close();
aprsEventSource = null;
}
// Clear polling timer
if (aprsPollTimer) {
clearInterval(aprsPollTimer);
aprsPollTimer = null;
}
});
} }
function startAprsStream(isAgentMode = false) { function startAprsStream(isAgentMode = false) {
@@ -10902,16 +10957,11 @@
} }
async function stopTscmSweep() { async function stopTscmSweep() {
try { const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
// Route to agent or local based on selection const endpoint = isAgentMode
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local'; ? `/controller/agents/${currentAgent}/tscm/stop`
const endpoint = isAgentMode : '/tscm/sweep/stop';
? `/controller/agents/${currentAgent}/tscm/stop` const timeoutMs = isAgentMode ? REMOTE_STOP_TIMEOUT_MS : LOCAL_STOP_TIMEOUT_MS;
: '/tscm/sweep/stop';
await fetch(endpoint, { method: 'POST' });
} catch (e) {
console.error('Error stopping sweep:', e);
}
isTscmRunning = false; isTscmRunning = false;
tscmSweepEndTime = new Date(); tscmSweepEndTime = new Date();
@@ -10931,6 +10981,8 @@
// Show report button if we have any data // Show report button if we have any data
const hasData = tscmWifiDevices.length > 0 || tscmBtDevices.length > 0 || tscmRfSignals.length > 0; const hasData = tscmWifiDevices.length > 0 || tscmBtDevices.length > 0 || tscmRfSignals.length > 0;
document.getElementById('tscmReportBtn').style.display = hasData ? 'block' : 'none'; document.getElementById('tscmReportBtn').style.display = hasData ? 'block' : 'none';
return postStopRequest(endpoint, timeoutMs);
} }
function generateTscmReport() { function generateTscmReport() {
+8 -7
View File
@@ -97,7 +97,7 @@ class AgentClient:
except requests.RequestException as e: except requests.RequestException as e:
raise AgentHTTPError(f"Request failed: {e}") raise AgentHTTPError(f"Request failed: {e}")
def _post(self, path: str, data: dict | None = None) -> dict: def _post(self, path: str, data: dict | None = None, timeout: float | None = None) -> dict:
""" """
Perform POST request to agent. Perform POST request to agent.
@@ -113,19 +113,20 @@ class AgentClient:
AgentConnectionError: If agent is unreachable AgentConnectionError: If agent is unreachable
""" """
url = f"{self.base_url}{path}" url = f"{self.base_url}{path}"
request_timeout = self.timeout if timeout is None else timeout
try: try:
response = requests.post( response = requests.post(
url, url,
json=data or {}, json=data or {},
headers=self._headers(), headers=self._headers(),
timeout=self.timeout timeout=request_timeout
) )
response.raise_for_status() response.raise_for_status()
return response.json() if response.content else {} return response.json() if response.content else {}
except requests.ConnectionError as e: except requests.ConnectionError as e:
raise AgentConnectionError(f"Cannot connect to agent at {self.base_url}: {e}") raise AgentConnectionError(f"Cannot connect to agent at {self.base_url}: {e}")
except requests.Timeout: except requests.Timeout:
raise AgentConnectionError(f"Request to agent timed out after {self.timeout}s") raise AgentConnectionError(f"Request to agent timed out after {request_timeout}s")
except requests.HTTPError as e: except requests.HTTPError as e:
# Try to extract error message from response body # Try to extract error message from response body
error_msg = f"Agent returned error: {e.response.status_code}" error_msg = f"Agent returned error: {e.response.status_code}"
@@ -141,9 +142,9 @@ class AgentClient:
except requests.RequestException as e: except requests.RequestException as e:
raise AgentHTTPError(f"Request failed: {e}") raise AgentHTTPError(f"Request failed: {e}")
def post(self, path: str, data: dict | None = None) -> dict: def post(self, path: str, data: dict | None = None, timeout: float | None = None) -> dict:
"""Public POST method for arbitrary endpoints.""" """Public POST method for arbitrary endpoints."""
return self._post(path, data) return self._post(path, data, timeout=timeout)
# ========================================================================= # =========================================================================
# Capability & Status # Capability & Status
@@ -214,7 +215,7 @@ class AgentClient:
""" """
return self._post(f'/{mode}/start', params or {}) return self._post(f'/{mode}/start', params or {})
def stop_mode(self, mode: str) -> dict: def stop_mode(self, mode: str, timeout: float = 8.0) -> dict:
""" """
Stop a running mode on the agent. Stop a running mode on the agent.
@@ -224,7 +225,7 @@ class AgentClient:
Returns: Returns:
Stop result with 'status' field Stop result with 'status' field
""" """
return self._post(f'/{mode}/stop') return self._post(f'/{mode}/stop', timeout=timeout)
def get_mode_status(self, mode: str) -> dict: def get_mode_status(self, mode: str) -> dict:
""" """
+72 -22
View File
@@ -733,39 +733,69 @@ class UnifiedWiFiScanner:
Returns: Returns:
True if scan was stopped. True if scan was stopped.
""" """
cleanup_process: Optional[subprocess.Popen] = None
cleanup_thread: Optional[threading.Thread] = None
cleanup_detector = None
with self._lock: with self._lock:
if not self._status.is_scanning: if not self._status.is_scanning:
return True return True
# Stop deauth detector first
self._stop_deauth_detector()
self._deep_scan_stop_event.set() self._deep_scan_stop_event.set()
cleanup_process = self._deep_scan_process
if self._deep_scan_process: cleanup_thread = self._deep_scan_thread
try: cleanup_detector = self._deauth_detector
self._deep_scan_process.terminate() self._deauth_detector = None
self._deep_scan_process.wait(timeout=5) self._deep_scan_process = None
except Exception as e: self._deep_scan_thread = None
logger.warning(f"Error terminating airodump-ng: {e}")
try:
self._deep_scan_process.kill()
except Exception:
pass
self._deep_scan_process = None
if self._deep_scan_thread:
self._deep_scan_thread.join(timeout=5)
self._deep_scan_thread = None
self._status.is_scanning = False self._status.is_scanning = False
self._status.error = None
self._queue_event({ self._queue_event({
'type': 'scan_stopped', 'type': 'scan_stopped',
'mode': SCAN_MODE_DEEP, 'mode': SCAN_MODE_DEEP,
}) })
return True cleanup_start = time.perf_counter()
def _finalize_stop(
process: Optional[subprocess.Popen],
scan_thread: Optional[threading.Thread],
detector,
) -> None:
if detector:
try:
detector.stop()
logger.info("Deauth detector stopped")
self._queue_event({'type': 'deauth_detector_stopped'})
except Exception as exc:
logger.error(f"Error stopping deauth detector: {exc}")
if process and process.poll() is None:
try:
process.terminate()
process.wait(timeout=1.5)
except Exception:
try:
process.kill()
except Exception:
pass
if scan_thread and scan_thread.is_alive():
scan_thread.join(timeout=1.5)
elapsed_ms = (time.perf_counter() - cleanup_start) * 1000.0
logger.info(f"Deep scan stop finalized in {elapsed_ms:.1f}ms")
threading.Thread(
target=_finalize_stop,
args=(cleanup_process, cleanup_thread, cleanup_detector),
daemon=True,
name='wifi-deep-stop',
).start()
return True
def _run_deep_scan( def _run_deep_scan(
self, self,
@@ -799,12 +829,30 @@ class UnifiedWiFiScanner:
logger.info(f"Starting airodump-ng: {' '.join(cmd)}") logger.info(f"Starting airodump-ng: {' '.join(cmd)}")
process: Optional[subprocess.Popen] = None
try: try:
self._deep_scan_process = subprocess.Popen( process = subprocess.Popen(
cmd, cmd,
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
) )
should_track_process = False
with self._lock:
# Only expose the process handle if this run has not been
# replaced by a newer deep scan session.
if self._status.is_scanning and not self._deep_scan_stop_event.is_set():
should_track_process = True
self._deep_scan_process = process
if not should_track_process:
try:
process.terminate()
process.wait(timeout=1.0)
except Exception:
try:
process.kill()
except Exception:
pass
return
csv_file = f"{output_prefix}-01.csv" csv_file = f"{output_prefix}-01.csv"
@@ -837,7 +885,9 @@ class UnifiedWiFiScanner:
'error': str(e), 'error': str(e),
}) })
finally: finally:
self._deep_scan_process = None with self._lock:
if process is not None and self._deep_scan_process is process:
self._deep_scan_process = None
# ========================================================================= # =========================================================================
# Observation Processing # Observation Processing