Both devices now serve \.\pipe\vrio and \.\pipe\vplasma for the whole app lifetime alongside the COM rows — the com0com-free path. Framing per the pinned contract (0x00 len data / 0x01 DTR+RTS lines, one lines frame on connect, unknown type = log + drop): PipeFraming/PipeFrameDecoder and VRioPipeService (TX paced at the wire rate, peer DTR edges feed HostHandshake) in VRio.Core, listener-only VPlasmaPipeService twin in VPlasma.Core. Busy pipe names retry, so vRIO's built-in glass and the standalone vPLASMA coexist. README documents the transport and the fork conf lines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99 lines
3.2 KiB
C#
99 lines
3.2 KiB
C#
using System.Diagnostics;
|
|
using System.IO.Pipes;
|
|
using VPlasma.Core.Device;
|
|
using Xunit;
|
|
|
|
namespace VPlasma.Core.Tests;
|
|
|
|
public class VPlasmaPipeServiceTests
|
|
{
|
|
private static string UniqueName() => $"vplasma-test-{Guid.NewGuid():N}";
|
|
|
|
private static void WaitFor(Func<bool> condition, int timeoutMs = 2000)
|
|
{
|
|
var clock = Stopwatch.StartNew();
|
|
while (!condition())
|
|
{
|
|
Assert.True(clock.ElapsedMilliseconds < timeoutMs, "timed out waiting for a condition");
|
|
Thread.Sleep(10);
|
|
}
|
|
}
|
|
|
|
private static NamedPipeClientStream Connect(string name)
|
|
{
|
|
var pipe = new NamedPipeClientStream(".", name, PipeDirection.InOut, PipeOptions.Asynchronous);
|
|
var clock = Stopwatch.StartNew();
|
|
while (true)
|
|
{
|
|
try
|
|
{
|
|
pipe.Connect(200);
|
|
return pipe;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or TimeoutException)
|
|
{
|
|
if (clock.ElapsedMilliseconds >= 3000)
|
|
throw;
|
|
Thread.Sleep(25);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Framed_text_reaches_the_display_and_lines_are_ignored()
|
|
{
|
|
string name = UniqueName();
|
|
var device = new VPlasmaDevice();
|
|
using var service = new VPlasmaPipeService(device, name);
|
|
service.Start();
|
|
|
|
using var client = Connect(name);
|
|
var hello = new byte[2];
|
|
Assert.Equal(1, client.Read(hello, 0, 1)); // on-connect lines frame,
|
|
Assert.Equal(1, client.Read(hello, 1, 1)); // possibly split
|
|
Assert.Equal(new byte[] { 0x01, 0x03 }, hello); // DTR+RTS: display present
|
|
|
|
// RTS chatter (DOS console redirection toggles it per byte) then "HI",
|
|
// the data frame torn in half mid-payload.
|
|
client.Write(new byte[] { 0x01, 0x02, 0x00, 0x02, (byte)'H' }, 0, 5);
|
|
client.Flush();
|
|
Thread.Sleep(20);
|
|
client.Write(new byte[] { (byte)'I' }, 0, 1);
|
|
client.Flush();
|
|
|
|
WaitFor(() => device.BytesReceived == 2);
|
|
Assert.Equal(2, device.TextCharsDrawn);
|
|
}
|
|
|
|
[Fact]
|
|
public void Unknown_frame_type_drops_the_client_and_server_recovers()
|
|
{
|
|
string name = UniqueName();
|
|
var device = new VPlasmaDevice();
|
|
using var service = new VPlasmaPipeService(device, name);
|
|
service.Start();
|
|
|
|
using (var bad = Connect(name))
|
|
{
|
|
var hello = new byte[2];
|
|
bad.Read(hello, 0, 2);
|
|
bad.Write(new byte[] { 0x7F }, 0, 1); // not a frame type
|
|
bad.Flush();
|
|
// Contract: log + drop, no resync — our next read sees the close.
|
|
var eof = Task.Run(() =>
|
|
{
|
|
try { return bad.Read(new byte[1], 0, 1) == 0; }
|
|
catch (IOException) { return true; }
|
|
});
|
|
Assert.True(eof.Wait(2000), "server did not drop the connection");
|
|
Assert.True(eof.Result);
|
|
}
|
|
|
|
using var good = Connect(name);
|
|
var again = new byte[2];
|
|
Assert.Equal(1, good.Read(again, 0, 1));
|
|
Assert.Equal(1, good.Read(again, 1, 1));
|
|
Assert.Equal(new byte[] { 0x01, 0x03 }, again);
|
|
}
|
|
}
|