Fix #10 + #11: transport hardening + controller resource leaks

#10 transport (obdcore/transport.py):
- TcpTransport.read raises IOError on a real socket error or peer-close instead
  of swallowing it as a timeout, so a dead WiFi link surfaces (via the #8 poll
  handler) as 'connection lost' rather than a frozen dashboard.
- TcpTransport.reset_input_buffer drains at most 64 chunks — never spins forever.
- BleTransport closes the client + stops the event-loop thread on connect
  timeout (no leak), caps the notification buffer at 64 KiB, and close() is
  robust when only partially initialised.

#11 controller (gui/controller.py, obdcore/store.py):
- connect() closes the transport and nulls the link if init()/connect() raises,
  so a failed/retried connect doesn't orphan sockets/threads.
- stop_record() unhooks store.recorder BEFORE closing it, and CsvRecorder now
  has a 'closed' guard so a poll-thread write racing close() is a no-op instead
  of an I/O-on-closed-file crash.

Closes #10
Closes #11

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:
2026-07-01 19:39:47 -04:00
parent fa7225d6dc
commit 39fcf3fb55
4 changed files with 86 additions and 22 deletions
+18 -8
View File
@@ -76,12 +76,21 @@ class Controller:
self.link = ElmLink.ble(c["address"])
else:
self.link = ElmLink.serial(c.get("port", port), c.get("baud", baud))
self.link.init()
ok = self.link.connect()
try:
self.link.fast_timing(True)
try: # don't leak the transport if handshake fails
if not mock:
self.link.init()
ok = self.link.connect()
try:
self.link.fast_timing(True)
except Exception:
pass
except Exception:
pass
try:
self.link.close()
except Exception:
pass
self.link = None
raise
self.poll_error = None
self.sched = PollScheduler(self.link, self.reg, self.store, clock=time.time,
on_error=self._on_poll_error)
@@ -119,9 +128,10 @@ class Controller:
self.store.recorder = CsvRecorder(path)
def stop_record(self):
if self.store.recorder:
self.store.recorder.close()
self.store.recorder = None
rec = self.store.recorder
if rec:
self.store.recorder = None # unhook first so the poll thread stops writing
rec.close()
def now(self):
return (time.time() - self.t0) if self.t0 else 0.0