fix(transport): a throwing StatusChanged subscriber no longer kills auto-reconnect or leaks the port handle - #504
Conversation
…nnect or leaks the port A drop runs transport.HandleConnectionLost -> DaqifiDevice.Status = Lost -> StatusChanged -> BeginReconnectIfEnabled on one thread, with the transport's handle dispose still pending. StatusChanged was the one event in that path raised unguarded, so a subscriber that threw (a WPF/WinForms handler touching a bound property from the background thread the event documents itself as firing on) skipped the reconnect start entirely and unwound back into the transport before it disposed the port -- which, with the field already nulled, meant Disconnect()/Dispose() skipped it too and the OS handle stayed claimed for the life of the process. The exception then vanished into the watchdog's catch. Isolate the raise in the Status setter, reporting the subscriber failure through the existing ErrorOccurred/logging surface as the new DeviceErrorSource.StatusNotification; dispose the transport handle in a finally on both the serial and TCP drop paths. closes #494 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by QodoIsolate StatusChanged exceptions to preserve reconnect and always release handles
AI Description
Diagram
High-Level Assessment
Files changed (8)
|
Code Review by Qodo
1.
|
…path Qodo round 1: the new catch made a bare-transport subscriber failure fully silent. On the reader path it used to reach the producer/consumer loop's own logging, so this was a real loss of diagnosability rather than a wash. Neither transport carries an ILogger, so use the same best-effort, self-contained trace DeviceFinderBase.RaiseIsolated and DaqifiStreamingDevice.SafeTrace already use for isolated raises in logger-less classes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Qodo-clean, CI green — ready for review. |
|
Code review by qodo was updated up to the latest commit 482390d |
The subscriber's exception is consumer code too, so rendering it is not a safe operation. Interpolating it into the trace message at the call site put that render outside SafeTrace's try, and an exception type whose ToString() throws escaped the very catch that exists to contain the subscriber -- the handle still went out in the finally, but the watchdog or reader thread was disrupted anyway. SafeTrace now takes the exception and builds the whole line inside its guard, in both transports. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Valid — fixed in 8524922. Confirmed by reproducing it: an exception whose
Full suite green on net9.0 (2915 Core + 43 Mcp) and net10.0 (2915), 0 failures, 0 warnings. |
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 8524922 |
…d close The TCP drop-path tests proved the socket was really released by having the peer read EOF. A close that the stack turns into a reset is equally good proof, so insisting on the FIN made them needlessly platform-sensitive. Both now accept EOF or ConnectionReset. A timeout is deliberately still a failure: a socket that was leaked rather than closed sends the peer neither a FIN nor an RST, so that is the one outcome the assertion has to keep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Round 3 triage — one taken, one declined. "FIN-only TCP assertion" — valid, fixed in 720da1b. Replied on the inline thread and resolved it. Both TCP tests now accept EOF or "Global Trace listener mutation" — declining. This is the settled convention from #428, which Qodo's own relevance note flags ( Verification on 720da1b: full suite green on net9.0 (2915 Core + 43 Mcp) and net10.0 (2915), 0 failures, 0 warnings. Test-only round, so the bench result from 8524922 still stands. |
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 720da1b |
|
Qodo-clean, CI green — ready for review. Round 4 on head |
What was wrong
Pull the USB cable on a device whose
StatusChangedhandler throws — the classic case being a WPF or WinForms app that updates a bound property from the handler, which throws a cross-threadInvalidOperationExceptionbecause the event is documented as firing on a background thread — and three things went wrong at once, silently:ReconnectOptions.Enableddid nothing at all, because the exception escaped theLostnotification and skipped the reconnect start that runs right after it.Disconnect()/Dispose()found null and skipped it too. Re-plugging the device then fails with "Access is denied".How it was fixed
StatusChangedwas the last event on that path still raised unguarded —ErrorOccurred,SendFailed, the reconnect events and the classified-message events are all already isolated. It now goes through the same kind of guard: the transition always completes, whatever happens after it still runs, and the subscriber's exception is reported onErrorOccurred(and the device logger) under a newDeviceErrorSource.StatusNotification. Both transports additionally dispose their handle in afinally, so a consumer subscribed directly to a bare transport can't leak it either.Things you may want to push back on:
Lost. A throwing subscriber used to makeConnect()itself throw after the device was fully connected, which is the same bug wearing a different hat.DeviceErrorSourcegains a member (StatusNotification = 4). Additive, but it is a public enum.ILogger, additionally emit the same best-effortTracelineDeviceFinderBaseuses for its isolated raises, so a bare-transport consumer isn't left with silence.Verification
ToString()throws) and the device (connect/disconnect/drop, theErrorOccurredreport, a throwingErrorOccurredhandler on top of a throwingStatusChangedone, and the reconnect loop starting and completing). Each was re-run against the un-fixed code first — all failed — so they pin the bug rather than the fix./dev/cu.usbmodem1101: four connect → status → stream → disconnect cycles across the review rounds, exit 0, 1186–1187 samples in 3 s at 500 Hz (this unit's known ~79% clock ratio), cleanDisconnectedtransition through the new raise path. Non-destructive; serial only.closes #494
Not merging — for review.