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
+27
View File
@@ -5,6 +5,7 @@ import os
import socket
import sys
import threading
import time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -89,6 +90,31 @@ def test_wifi_transport():
srv.stop()
def test_tcp_read_raises_on_closed_peer():
# A dead WiFi connection must surface as an error, not silently look like a
# perpetual timeout (which left the app frozen on "Connected").
srv = socket.socket()
srv.bind(("127.0.0.1", 0)); srv.listen(1)
port = srv.getsockname()[1]
def serve():
c, _ = srv.accept()
c.close() # drop the client immediately
threading.Thread(target=serve, daemon=True).start()
t = TcpTransport("127.0.0.1", port)
time.sleep(0.1)
raised = False
try:
for _ in range(5):
t.read(64)
except IOError:
raised = True
assert raised, "read should raise IOError when the peer closed the socket"
t.close(); srv.close()
print(" TCP dead-connection detected (read raises, not silent): OK")
def test_factory_helpers():
# the factory methods build the right transport type
assert hasattr(ElmLink, "serial") and hasattr(ElmLink, "tcp") and hasattr(ElmLink, "ble")
@@ -97,5 +123,6 @@ def test_factory_helpers():
if __name__ == "__main__":
test_wifi_transport()
test_tcp_read_raises_on_closed_peer()
test_factory_helpers()
print("\nALL TRANSPORT TESTS PASS")