Fix #8: scheduler survives link death; timed-out one-offs cancelled
- A transport exception in the poll loop killed the thread silently, leaving the GUI on a frozen 'Connected' dashboard and blocking run_oneoff callers for the full timeout. _loop now catches it -> stops, fails pending one-offs with the real error, and calls an on_error callback. Controller wires on_error to flag the connection dead; the GUI detects it in _tick and tears down with a 'Connection lost' dialog. - A run_oneoff that timed out left its job queued, so it executed LATER on the shared link -- a ghost/duplicate vehicle command. Jobs now carry cancelled/started flags under a lock; on timeout a not-yet-started job is cancelled (skipped by _drain_oneoffs), and a started one reports 'still running -- do NOT retry'. stop() also frees stranded submitters. - tests/test_scheduler.py: cancel-on-timeout, freed-on-death, loop-survives. Closes #8 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
This commit is contained in:
+9
-1
@@ -49,6 +49,12 @@ class Controller:
|
||||
elif p.mode == "01" and p.pid.upper() == "10":
|
||||
self.maf_key = p.key
|
||||
|
||||
def _on_poll_error(self, exc):
|
||||
"""Called on the poll thread if it dies (transport failure). Flag the
|
||||
connection dead so the GUI stops showing frozen 'Connected' data."""
|
||||
self.poll_error = exc
|
||||
self.connected = False
|
||||
|
||||
def load_profile(self, path):
|
||||
"""Switch the active vehicle profile (only allowed while disconnected)."""
|
||||
self.profile = load_profile(path)
|
||||
@@ -76,7 +82,9 @@ class Controller:
|
||||
self.link.fast_timing(True)
|
||||
except Exception:
|
||||
pass
|
||||
self.sched = PollScheduler(self.link, self.reg, self.store, clock=time.time)
|
||||
self.poll_error = None
|
||||
self.sched = PollScheduler(self.link, self.reg, self.store, clock=time.time,
|
||||
on_error=self._on_poll_error)
|
||||
self.t0 = time.time()
|
||||
self.connected = True
|
||||
self.trip.reset()
|
||||
|
||||
+13
@@ -775,6 +775,17 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
b.setEnabled(False)
|
||||
self.status.showMessage("Disconnected.")
|
||||
|
||||
def _on_link_lost(self, exc):
|
||||
"""The polling thread died (transport failure). Tear down and tell the
|
||||
user instead of leaving a frozen 'Connected' dashboard."""
|
||||
self.ctl.poll_error = None # one-shot
|
||||
self._disconnect()
|
||||
self.status.showMessage(f"Connection lost: {exc}")
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self, "Connection lost",
|
||||
f"The adapter connection failed and polling stopped:\n\n{exc}\n\n"
|
||||
"Check the adapter/cable and reconnect.")
|
||||
|
||||
# ---------- PID selection ----------
|
||||
def _apply_preset(self, name):
|
||||
if not self.ctl.connected:
|
||||
@@ -1001,6 +1012,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
|
||||
def _tick(self):
|
||||
if not self.ctl.connected:
|
||||
if getattr(self.ctl, "poll_error", None) is not None:
|
||||
self._on_link_lost(self.ctl.poll_error)
|
||||
return
|
||||
self.tree.blockSignals(True)
|
||||
for key, it in self._items.items():
|
||||
|
||||
Reference in New Issue
Block a user