diff --git a/src/RioJoy.Core/Serial/SerialPortTransport.cs b/src/RioJoy.Core/Serial/SerialPortTransport.cs index bb2107a..20f8dc2 100644 --- a/src/RioJoy.Core/Serial/SerialPortTransport.cs +++ b/src/RioJoy.Core/Serial/SerialPortTransport.cs @@ -17,6 +17,9 @@ public sealed class SerialPortTransport : IRioTransport /// DTR reset-pulse hold time on open. public static readonly TimeSpan DtrPulse = TimeSpan.FromMilliseconds(50); + /// Longest will wait for the port to close. + 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); } }