From 9141539d611a0847dfa6362292047cf7bfb8ab31 Mon Sep 17 00:00:00 2001 From: Sean Dolbec Date: Sat, 22 Aug 2026 23:12:35 -0400 Subject: [PATCH] fix: zombie scan jobs disabled the Quick Scan button for every new page (#237 follow-up) Two stacked bugs produced 'scan does nothing / no open ports': 1. Function name collision: scan_runtime.js and discovery_ui.js both defined initializeDiscoveryUI; discovery_ui loads second and overrode the version that wires the Quick Scan button. The surviving click listener emitted with a stale socket reference, so clicks did nothing. Renamed scan_runtime's copy to initializeScanButtonWiring and call both from both bootstraps. 2. Zombie jobs: a page closed mid-scan left its job marked 'running' forever. On every future connect, sync_state reported isScanning=true, disabling all scan buttons. Connect/get_initial_data now verify the owner sid is still in broadcaster._connected_sids before reporting an active scan; disconnected owners' jobs are completed and torn down. Also: reload the page on socket.io transport reconnect (module closures hold the dead session's socket otherwise). Verified end-to-end: click -> quick scan -> deep scan -> 16 open ports + CVEs rendered in the discovery table. Suite back to the 16 known-red baseline. --- nmapui/handlers/connections.py | 15 +++++++++++++++ static/js/app_bootstrap.js | 8 ++++++++ static/js/scan_runtime.js | 2 +- templates/index.html | 7 +++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/nmapui/handlers/connections.py b/nmapui/handlers/connections.py index 9b9f4fe..eaf0f73 100644 --- a/nmapui/handlers/connections.py +++ b/nmapui/handlers/connections.py @@ -129,6 +129,13 @@ def on_connect(auth=None): if auto_scan_config is not None: emit_to_client(new_sid, "auto_scan_status", build_auto_scan_status_payload(auto_scan_config)) + # #237: ignore zombie jobs whose owner tab has disconnected. + if owner_sid and owner_sid not in getattr(broadcaster, "_connected_sids", set()): + _complete = getattr(job_registry, "complete", None) + if callable(_complete): + _complete(owner_sid, active_job_type or "scan", status="completed") + broadcaster.end_job(owner_sid, job_type=active_job_type or "scan") + job = None job = job_registry.get(owner_sid, active_job_type) if owner_sid else None is_scanning = bool(job and job.get("status") in ("running", "cancelling")) last_scan_target = source_state.get("last_scan_target") or "" @@ -197,6 +204,14 @@ def on_get_initial_data(): ) job = job_registry.get(owner_sid, active_job_type) if owner_sid else None + # #237 follow-up: a job whose owner tab is gone is a zombie - it would + # otherwise disable scan buttons for every future page load. + if owner_sid and owner_sid not in getattr(broadcaster, "_connected_sids", set()): + job = None + _complete = getattr(job_registry, "complete", None) + if callable(_complete): + _complete(owner_sid, active_job_type or "scan", status="completed") + broadcaster.end_job(owner_sid, job_type=active_job_type or "scan") is_scanning = bool(job and job.get("status") in ("running", "cancelling")) last_scan_target = source_state.get("last_scan_target") or "" network_key = source_state.get("network_key") or {} diff --git a/static/js/app_bootstrap.js b/static/js/app_bootstrap.js index 2b785bb..8c5b56e 100644 --- a/static/js/app_bootstrap.js +++ b/static/js/app_bootstrap.js @@ -57,6 +57,14 @@ async function bootstrapApp() { } // Request the legacy sync snapshot now that all listeners are wired (#230 bridge). socket.emit('get_initial_data'); + // Transport-level reconnect = new server session; closures hold the dead socket. + // Reload to rebuild everything against the live session (#237 follow-up). + if (socket.io) { + socket.io.on('reconnect', () => { location.reload(); }); + } + if (typeof initializeScanButtonWiring === 'function') { + initializeScanButtonWiring(socket); + } if (typeof initializeDiscoveryUI === 'function') { initializeDiscoveryUI(socket); } diff --git a/static/js/scan_runtime.js b/static/js/scan_runtime.js index 3aeedcb..ec96075 100644 --- a/static/js/scan_runtime.js +++ b/static/js/scan_runtime.js @@ -697,7 +697,7 @@ function updateHostRow(data) { updateCVESummaries(); } -function initializeDiscoveryUI(socket) { +function initializeScanButtonWiring(socket) { const startScanBtn = document.getElementById('start-scan-btn'); const completeScanBtn = document.getElementById('generate-report-btn'); const dragnetScanBtn = document.getElementById('dragnet-scan-btn'); diff --git a/templates/index.html b/templates/index.html index 5c0f716..5076045 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1650,6 +1650,13 @@

Scan Complete!

} // Request the legacy sync snapshot now that all listeners are wired (#230 bridge). socket.emit('get_initial_data'); + // A transport-level reconnect creates a new server session; module + // closures still hold the dead socket, so events would vanish. Reload + // to rebuild everything against the live session (#237 follow-up). + socket.io.on('reconnect', () => { location.reload(); }); + if (typeof initializeScanButtonWiring === 'function') { + initializeScanButtonWiring(socket); + } if (typeof initializeDiscoveryUI === 'function') { initializeDiscoveryUI(socket); }