Fix UI hang on editor close: bound the serial port teardown

Closing the profile editor froze the tray app: the FormClosed handler
tears down the coordinator on the UI thread, and net48's
SerialStream.Dispose calls FlushFileBuffers, which blocks until the
driver drains buffered TX — indefinitely when the peer has stopped
reading (a wedged board, or a virtual-port pair with no reader). The
55 ms analog polls guarantee a TX backlog against such a peer.
Diagnosed from a live hang: the UI thread was parked in
NtFlushBuffersFile under SerialPortTransport.Dispose.

SerialPortTransport.Dispose now aborts/clears both queues first (so the
flush has nothing to wait on) and runs the close on the pool with a 2 s
grace period, so no driver can hang the calling thread. Verified against
the same wedged COM1: a 400-byte TX backlog that never drains, yet
Dispose returns in ~0 ms and a full BeginEditorSession/EndEditorSession
cycle closes in 1 ms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-06 09:54:43 -05:00
co-authored by Claude Fable 5
parent e60d551f39
commit b2d1927c51
+31 -1
View File
@@ -17,6 +17,9 @@ public sealed class SerialPortTransport : IRioTransport
/// <summary>DTR reset-pulse hold time on open.</summary>
public static readonly TimeSpan DtrPulse = TimeSpan.FromMilliseconds(50);
/// <summary>Longest <see cref="Dispose"/> will wait for the port to close.</summary>
public static readonly TimeSpan CloseGracePeriod = TimeSpan.FromSeconds(2);
private readonly SerialPort _port;
private readonly Stream _stream;
@@ -63,6 +66,33 @@ public sealed class SerialPortTransport : IRioTransport
public void Dispose()
{
// Disposing closes and releases the COM port for other owners.
_port.Dispose();
//
// net48's SerialStream.Dispose calls FlushFileBuffers, which blocks until
// the driver drains any buffered TX bytes — indefinitely when the peer has
// stopped reading (a wedged board, or a virtual-port pair with no reader).
// Seen in the field as the tray UI freezing on editor close. Abort + clear
// both queues first so the flush has nothing to wait on, and bound the
// close so no driver can hang the calling (usually UI) thread.
try
{
_port.DiscardOutBuffer();
_port.DiscardInBuffer();
}
catch
{
// Port already gone (unplugged adapter / driver error) — closing below
// still releases the handle.
}
Task close = Task.Run(() =>
{
try { _port.Dispose(); }
catch { /* best-effort release */ }
});
// If the close is still stuck in the driver despite the purge, give up after
// the grace period and let it finish on the pool thread; reopening the port
// may fail until it does, which callers already surface as a status error.
close.Wait(CloseGracePeriod);
}
}