using System.IO.Pipes;
namespace RioJoy.Core.Feedback;
///
/// Named-pipe listener for the inbound feedback protocol: serves
/// \\.\pipe\<name>, reassembles lines
/// (), and hands each to the owner — parsing
/// and routing live in , so this class is pure
/// transport. Modeled on vRIO's VRioPipeService (dedicated background
/// threads — net40 has no WaitForConnectionAsync; throwaway poke-connect
/// on stop because a pending WaitForConnection can survive Dispose on
/// net48), with two deliberate differences: the pipe is
/// — the server never writes, so the 0-byte
/// pipe-buffer write deadlock class cannot occur and no reply path exists — and
/// up to clients may stay connected at once (a sim
/// export script and a SimHub plugin both live here). Clients reconnect
/// forever; a malformed or overlong line never costs a client its connection.
///
public sealed class FeedbackPipeServer : IDisposable
{
/// Concurrent client cap (pipe instances of the served name).
public const int MaxClients = 4;
private readonly string _pipeName;
private readonly Action _onLine;
private readonly Action? _log;
private readonly SemaphoreSlim _slots = new(MaxClients, MaxClients);
private readonly object _stateGate = new();
private readonly List _open = new();
private readonly List _readers = new();
private Thread? _accept;
private volatile bool _running;
public FeedbackPipeServer(string pipeName, Action onLine, Action? log = null)
{
if (string.IsNullOrWhiteSpace(pipeName))
throw new ArgumentException("Pipe name is required.", nameof(pipeName));
_pipeName = pipeName;
_onLine = onLine ?? throw new ArgumentNullException(nameof(onLine));
_log = log;
}
/// The served pipe name (without the \\.\pipe\ prefix).
public string PipeName => _pipeName;
/// Start listening (idempotent). Clients may come and go forever.
public void Start()
{
if (_running)
return;
_running = true;
_accept = new Thread(AcceptLoop)
{
IsBackground = true,
Name = $"RIOJoy feedback pipe ({_pipeName})",
};
_accept.Start();
_log?.Invoke($@"feedback: listening on \\.\pipe\{_pipeName}");
}
public void Dispose()
{
if (!_running)
return;
_running = false;
// A WaitForConnection pending on a disposed stream can survive the
// Dispose on net48; a throwaway client connect releases it either way.
try
{
using var poke = new NamedPipeClientStream(".", _pipeName, PipeDirection.Out);
poke.Connect(100);
}
catch (Exception ex) when (ex is IOException or TimeoutException or UnauthorizedAccessException) { }
NamedPipeServerStream[] open;
Thread[] readers;
lock (_stateGate)
{
open = _open.ToArray();
_open.Clear();
readers = _readers.ToArray();
_readers.Clear();
}
foreach (NamedPipeServerStream pipe in open)
{
try { pipe.Dispose(); }
catch (IOException) { }
}
_accept?.Join(1000);
_accept = null;
foreach (Thread reader in readers)
reader.Join(1000);
}
private void AcceptLoop()
{
bool busyLogged = false; // log a name collision once, not per retry
while (_running)
{
// At capacity, park until a reader frees its slot (timed, so
// shutdown can't wedge on a missed release).
if (!_slots.Wait(200))
continue;
if (!_running)
{
_slots.Release();
return;
}
NamedPipeServerStream pipe;
try
{
pipe = new NamedPipeServerStream(_pipeName, PipeDirection.In, MaxClients,
PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// Name already served — most likely a second RIOJoy instance.
_slots.Release();
if (!busyLogged)
{
busyLogged = true;
_log?.Invoke($@"feedback: \\.\pipe\{_pipeName} is busy ({ex.Message.TrimEnd('.')}) — retrying");
}
for (int i = 0; i < 20 && _running; i++)
Thread.Sleep(100);
continue;
}
busyLogged = false;
lock (_stateGate)
_open.Add(pipe);
try
{
pipe.WaitForConnection();
}
catch (Exception ex) when (ex is IOException or ObjectDisposedException or InvalidOperationException)
{
Drop(pipe);
continue; // disposed by Dispose(), or the client vanished mid-connect
}
if (!_running)
{
Drop(pipe);
return;
}
var reader = new Thread(() => ReadUntilDisconnect(pipe))
{
IsBackground = true,
Name = $"RIOJoy feedback pipe reader ({_pipeName})",
};
lock (_stateGate)
_readers.Add(reader);
reader.Start(); // the reader owns the slot + stream from here
}
}
private void ReadUntilDisconnect(NamedPipeServerStream pipe)
{
var buffer = new byte[512];
var lines = new FeedbackLineBuffer();
try
{
while (_running)
{
int n;
try
{
n = pipe.Read(buffer, 0, buffer.Length);
}
catch (Exception ex) when (
ex is IOException or ObjectDisposedException or InvalidOperationException)
{
return; // client gone or shutdown
}
if (n == 0)
return; // client closed its end
foreach (string line in lines.Feed(buffer, n))
Handle(line);
}
}
finally
{
Drop(pipe);
lock (_stateGate)
_readers.Remove(Thread.CurrentThread);
}
}
private void Handle(string line)
{
try
{
_onLine(line);
}
catch (Exception ex)
{
// The line sink must never kill a reader; log and keep serving.
_log?.Invoke($"feedback: line handler failed: {ex.Message}");
}
}
private void Drop(NamedPipeServerStream pipe)
{
lock (_stateGate)
_open.Remove(pipe);
try { pipe.Dispose(); }
catch (IOException) { }
_slots.Release();
}
}