Writer thread survives write faults instead of dying mid-run
A single failed port write killed the paced writer thread permanently while the port still reported open, so every later byte (ACKs, the CheckRequest handshake) queued forever: wire tap showed 2 of 3 bytes of a stray ButtonPressed at t=54ms, then 1167 unanswered CheckRequests. The trigger was com0com flow control with the game side not yet open -- the third byte blocked past WriteTimeout, threw, and the catch returned. A real UART cannot wedge; it shifts bits into the line whether or not anyone listens. On a write fault, drop the stalled byte plus the stale backlog, log the stall/recovery transition once, and keep the writer alive -- TX resumes as soon as the host drains its end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,12 @@ namespace VRio.Core.Device;
|
||||
/// monotonic slot deadline (<c>slot = max(prevSlot + period, now)</c>), so
|
||||
/// the stream averages the true baud rate without bursting after idle.</para>
|
||||
///
|
||||
/// <para>A write fault never kills the writer — a real UART streams into an
|
||||
/// unterminated line rather than blocking. If the virtual wire's far side
|
||||
/// stops draining (peer end closed, write timeout), the stalled byte and the
|
||||
/// queued backlog are dropped and transmission resumes with the next fresh
|
||||
/// packet once the host reads again.</para>
|
||||
///
|
||||
/// <para>RIOJoy pulses DTR for 50 ms when it opens its end (the board-reset
|
||||
/// handshake); through a null modem that arrives here as a DSR blip, which is
|
||||
/// surfaced via <see cref="HostHandshake"/> so the UI can show that a host
|
||||
@@ -201,6 +207,7 @@ public sealed class VRioSerialService : IDisposable
|
||||
{
|
||||
var one = new byte[1];
|
||||
long slot = Stopwatch.GetTimestamp();
|
||||
bool txHealthy = true; // log stall/recovery transitions, not every byte
|
||||
|
||||
while (_running)
|
||||
{
|
||||
@@ -226,12 +233,33 @@ public sealed class VRioSerialService : IDisposable
|
||||
try
|
||||
{
|
||||
port.Write(one, 0, 1);
|
||||
if (!txHealthy)
|
||||
{
|
||||
txHealthy = true;
|
||||
Logged?.Invoke("TX recovered — host is draining the wire again");
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or InvalidOperationException or TimeoutException)
|
||||
{
|
||||
if (_running)
|
||||
Logged?.Invoke($"Write failed: {ex.Message}");
|
||||
return;
|
||||
if (!_running)
|
||||
return;
|
||||
// A real UART cannot wedge: it shifts bits onto the line whether
|
||||
// or not anyone is listening. A failed write means the virtual
|
||||
// wire's far side stopped draining (peer end closed), so the
|
||||
// queued backlog is already stale — drop it and keep serving
|
||||
// fresh traffic; writes land again once the host reads its end.
|
||||
int dropped;
|
||||
lock (_txGate)
|
||||
{
|
||||
dropped = _txQueue.Count;
|
||||
_txQueue.Clear();
|
||||
}
|
||||
if (txHealthy)
|
||||
{
|
||||
txHealthy = false;
|
||||
Logged?.Invoke($"TX stalled ({ex.Message.TrimEnd('.')}) — dropped {dropped + 1} stale byte(s), writer stays alive");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the wait overshot its slot, pace the next byte from the
|
||||
|
||||
Reference in New Issue
Block a user