Build — Windows, Linux & macOS / GDPRScanner / linux (push) Has been cancelled
Build — Windows, Linux & macOS / GDPRScanner / macos (push) Has been cancelled
Build — Windows, Linux & macOS / GDPRScanner / windows (push) Has been cancelled
Build — Windows, Linux & macOS / Create GitHub Release (push) Has been cancelled
Pressing Stop could look like the scan restarting, and for scheduled jobs it really did restart the Google stage. - Scheduler: check the abort flag between stages and never clear the Google abort after a Stop (that started a fresh all-users Google scan). A stopped job is recorded as "cancelled" and skips email/retention; scheduler_done carries cancelled=true. - SSE: scan_cancelled and file_scan_done now end the current scan like scan_done, and sse.replay_messages() never replays a finished scan. Previously a stopped M365 scan (and every file scan) stayed "current", so each reconnect replayed it and re-showed Stop + progress. - Frontend: scan_cancelled clears the M365 running flag and only tears down the UI when no Google/file scan is still running; scheduler_done / scheduler_error clear all running flags. - run_scan: Stop during collection now ends the scan (checkpoint, finish, scan_cancelled) instead of announcing scan_start / "Scanning…". - Stop button: switches to a disabled "Stopping…" state on click; all show/hide goes through _setStopBtnDisplay(), which resets it when hidden or when a new scan starts. Adds tests/test_stop_scan.py (10 tests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
3.4 KiB
Python
79 lines
3.4 KiB
Python
"""
|
|
sse.py — Server-Sent Events for GDPRScanner.
|
|
|
|
Provides:
|
|
broadcast(event, data) — push an event to all connected browsers
|
|
_sse_queues — list of per-connection Queue objects
|
|
_sse_buffer — deque replay buffer for late-connecting browsers
|
|
_current_scan_id — injected into every broadcast message
|
|
"""
|
|
from __future__ import annotations
|
|
import json
|
|
import logging
|
|
import queue
|
|
from collections import deque
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ── SSE state ─────────────────────────────────────────────────────────────────
|
|
_sse_queues: list = []
|
|
_sse_buffer: deque = deque(maxlen=500)
|
|
_current_scan_id: str = ""
|
|
# Scans that have ended (done / stopped). Their events stay in the buffer but
|
|
# are never replayed: replaying a stopped scan re-showed the Stop button and
|
|
# progress bar on every reconnect, so Stop looked like a restart.
|
|
_finished_scan_ids: deque = deque(maxlen=20)
|
|
|
|
# Events that end the scan identified by _current_scan_id.
|
|
# scan_done: M365 finished; scan_cancelled: M365 stopped; file_scan_done:
|
|
# file scan finished or stopped. (Google scans don't set _current_scan_id.)
|
|
_TERMINAL_EVENTS = frozenset({"scan_done", "scan_cancelled", "file_scan_done"})
|
|
|
|
def replay_messages() -> list[str]:
|
|
"""Buffered messages to replay to a newly connected browser.
|
|
|
|
While a scan runs, only that scan's events. Otherwise everything except
|
|
events of scans that have already ended (done or stopped).
|
|
"""
|
|
buf = list(_sse_buffer)
|
|
if _current_scan_id:
|
|
tag = f'"scan_id": "{_current_scan_id}"'
|
|
return [m for m in buf if tag in m]
|
|
finished = [f'"scan_id": "{sid}"' for sid in _finished_scan_ids]
|
|
return [m for m in buf if not any(tag in m for tag in finished)]
|
|
|
|
|
|
def broadcast(event: str, data: dict):
|
|
global _current_scan_id
|
|
if _current_scan_id:
|
|
data = {**data, "scan_id": _current_scan_id}
|
|
msg = f"event: {event}\ndata: {json.dumps(data)}\n\n"
|
|
_sse_buffer.append(msg) # buffer for SSE replay on reconnect
|
|
for q in list(_sse_queues):
|
|
try:
|
|
q.put_nowait(msg)
|
|
except queue.Full:
|
|
pass
|
|
# Clear scan_id when the scan ends so replay knows it is finished
|
|
if event in _TERMINAL_EVENTS and _current_scan_id:
|
|
_finished_scan_ids.append(_current_scan_id)
|
|
_current_scan_id = ""
|
|
# When no browser is watching (e.g. scheduled scan), log key events
|
|
if not _sse_queues:
|
|
if event == "scan_phase":
|
|
logger.info("[scan] %s", data.get("phase", ""))
|
|
elif event == "scan_progress":
|
|
file = data.get("file") or data.get("name", "")
|
|
if file:
|
|
logger.info("[scan] %s/%s — %s", data.get("completed", ""), data.get("total", ""), file)
|
|
elif event in ("scan_error", "scheduler_error"):
|
|
logger.error("[scan] %s", data.get("error", "") or data.get("file", ""))
|
|
elif event == "scan_done":
|
|
logger.info("[scan] Done — %d flagged, %d scanned",
|
|
data.get("flagged_count", 0), data.get("total_scanned", 0))
|
|
elif event == "scheduler_started":
|
|
logger.info("[scan] Scheduler started — %s", data.get("job_name", ""))
|
|
elif event == "scheduler_done":
|
|
logger.info("[scan] Scheduler done — %d flagged", data.get("flagged", 0))
|
|
|