Fix WeFax start/stop/SSE reliability

- Replace blocking stdout.read() with select()-based non-blocking reads
  so the decode thread responds to stop within 0.5s
- Make stop() non-blocking by releasing the lock before terminating the
  process and removing the redundant wait()
- Move initial scanning SSE event from start() into the decode thread so
  it fires after the frontend EventSource connects
- Update frontend stop() to give immediate UI feedback before the fetch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-24 16:10:34 +00:00
parent b72a2f1092
commit 9745215038
2 changed files with 42 additions and 19 deletions
+7 -3
View File
@@ -205,16 +205,20 @@ var WeFax = (function () {
} }
function stop() { function stop() {
// Immediate UI feedback before waiting for backend response
state.running = false;
updateButtons(false);
setStatus('Stopping...');
disconnectSSE();
fetch('/wefax/stop', { method: 'POST' }) fetch('/wefax/stop', { method: 'POST' })
.then(function (r) { return r.json(); }) .then(function (r) { return r.json(); })
.then(function () { .then(function () {
state.running = false;
updateButtons(false);
setStatus('Stopped'); setStatus('Stopped');
disconnectSSE();
loadImages(); loadImages();
}) })
.catch(function (err) { .catch(function (err) {
setStatus('Stopped');
console.error('WeFax stop error:', err); console.error('WeFax stop error:', err);
}); });
} }
+35 -16
View File
@@ -15,6 +15,8 @@ import base64
import contextlib import contextlib
import io import io
import math import math
import os
import select
import subprocess import subprocess
import threading import threading
import time import time
@@ -297,11 +299,6 @@ class WeFaxDecoder:
f"WeFax decoder started: {frequency_khz} kHz, " f"WeFax decoder started: {frequency_khz} kHz, "
f"station={station}, IOC={ioc}, LPM={lpm}" f"station={station}, IOC={ioc}, LPM={lpm}"
) )
self._emit_progress(WeFaxProgress(
status='scanning',
station=station,
message=f'Scanning {frequency_khz} kHz for WeFax start tone...',
))
return True return True
except Exception as e: except Exception as e:
@@ -395,9 +392,29 @@ class WeFaxDecoder:
f"samples/line={samples_per_line}" f"samples/line={samples_per_line}"
) )
# Emit initial scanning progress here (not in start()) so the
# frontend SSE connection is established before this event fires.
time.sleep(0.1)
self._emit_progress(WeFaxProgress(
status='scanning',
station=self._station,
message=f'Scanning {self._frequency_khz} kHz for WeFax start tone...',
))
while self._running and self._rtl_process: while self._running and self._rtl_process:
try: try:
raw_data = self._rtl_process.stdout.read(chunk_bytes) proc = self._rtl_process
if not proc or not proc.stdout:
break
# Non-blocking read via select() — allows checking _running
# on timeout instead of blocking indefinitely in read().
fd = proc.stdout.fileno()
ready, _, _ = select.select([fd], [], [], 0.5)
if not ready:
if not self._running:
break
continue
raw_data = os.read(fd, chunk_bytes)
if not raw_data: if not raw_data:
if self._running: if self._running:
stderr_msg = '' stderr_msg = ''
@@ -667,20 +684,22 @@ class WeFaxDecoder:
)) ))
def stop(self) -> None: def stop(self) -> None:
"""Stop WeFax decoder.""" """Stop WeFax decoder.
Non-blocking: sets _running=False and terminates the process,
then returns immediately. The decode thread handles cleanup
when its read() returns empty.
"""
with self._lock: with self._lock:
self._running = False self._running = False
proc = self._rtl_process
self._rtl_process = None
if self._rtl_process: if proc:
try: with contextlib.suppress(Exception):
self._rtl_process.terminate() proc.terminate()
self._rtl_process.wait(timeout=5)
except Exception:
with contextlib.suppress(Exception):
self._rtl_process.kill()
self._rtl_process = None
logger.info("WeFax decoder stopped") logger.info("WeFax decoder stopped")
def get_images(self) -> list[WeFaxImage]: def get_images(self) -> list[WeFaxImage]:
"""Get list of decoded images.""" """Get list of decoded images."""