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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user