Fix IPC framing, add global pipe name, and auto-reconnect tests

This commit is contained in:
Ochenstarik 2026-09-24 16:48:51 +07:00
parent b3fdec1eb2
commit 16ea2a4765
5 changed files with 112 additions and 8 deletions

View file

@ -15,6 +15,7 @@ public sealed class MockFingerprintScanner : IFingerprintScanner
public IReadOnlyList<ScannerDeviceInfo> DiscoveredDevices => _devices;
public bool IsConnected => State == ScannerState.Connected || State == ScannerState.Busy;
public int SkippedTemplateCount { get; set; } = 0;
public int ConnectCallCount { get; private set; } = 0;
public event Action<int, int>? FingerRecognized;
public event Action<int>? RegistrationProgress;
@ -47,6 +48,7 @@ public sealed class MockFingerprintScanner : IFingerprintScanner
public void Connect(int readerIndex, IEnumerable<(int FingerprintId, string Template)> templates)
{
ConnectCallCount++;
if (_devices.Count == 0)
{
State = ScannerState.DriverError;

View file

@ -73,4 +73,45 @@ public class ScannerRecoveryTests
Assert.Null(error);
Assert.False(rehashNeeded);
}
[Fact]
public async Task Scanner_AutoReconnectLogic_CallsConnectSuccessfully()
{
using var scanner = new MockFingerprintScanner();
scanner.Connect(0, Array.Empty<(int, string)>());
Assert.Equal(1, scanner.ConnectCallCount);
var reconnectTcs = new TaskCompletionSource();
scanner.ConnectionLost += async () =>
{
// Simulate typical application retry loop
int retries = 0;
while (retries < 5)
{
await Task.Delay(10);
try
{
scanner.Connect(0, Array.Empty<(int, string)>());
if (scanner.IsConnected)
{
reconnectTcs.TrySetResult();
return;
}
}
catch { }
retries++;
}
reconnectTcs.TrySetException(new Exception("Failed to reconnect"));
};
scanner.SimulateDisconnect();
// Emulate the hardware coming back after a short delay
await Task.Delay(20);
scanner.AddVirtualDevice(0, "SLK20R", "SERIAL"); // ensure device is discoverable if it was cleared
await reconnectTcs.Task;
Assert.True(scanner.ConnectCallCount > 1);
Assert.Equal(ScannerState.Connected, scanner.State);
}
}

View file

@ -37,6 +37,7 @@ public sealed class IpcServiceStatus
public static class TerminalIpcConstants
{
public const string PipeName = "KronaStaffTerminalPipe";
public const string PipeServerName = ".";
}
public sealed class TerminalIpcClient
@ -74,20 +75,36 @@ public sealed class TerminalIpcClient
{
try
{
await using var pipe = new NamedPipeClientStream(".", TerminalIpcConstants.PipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
await using var pipe = new NamedPipeClientStream(TerminalIpcConstants.PipeServerName, TerminalIpcConstants.PipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromSeconds(2));
await pipe.ConnectAsync(timeoutCts.Token);
var request = new IpcRequest { Command = command, Payload = payload };
var json = JsonSerializer.Serialize(request) + "\n";
var json = JsonSerializer.Serialize(request);
var bytes = Encoding.UTF8.GetBytes(json);
var lengthPrefix = BitConverter.GetBytes(bytes.Length);
await pipe.WriteAsync(lengthPrefix, cancellationToken);
await pipe.WriteAsync(bytes, cancellationToken);
await pipe.FlushAsync(cancellationToken);
using var reader = new StreamReader(pipe, Encoding.UTF8);
var responseJson = await reader.ReadLineAsync(cancellationToken);
var respLenBytes = new byte[4];
var read = await pipe.ReadAsync(respLenBytes, cancellationToken);
if (read < 4) return null;
var respLen = BitConverter.ToInt32(respLenBytes);
if (respLen is <= 0 or > 10 * 1024 * 1024) return null;
var respBytes = new byte[respLen];
var totalRead = 0;
while (totalRead < respLen)
{
var r = await pipe.ReadAsync(respBytes.AsMemory(totalRead), cancellationToken);
if (r == 0) return null;
totalRead += r;
}
var responseJson = Encoding.UTF8.GetString(respBytes);
if (string.IsNullOrWhiteSpace(responseJson)) return null;
return JsonSerializer.Deserialize<IpcResponse>(responseJson);
@ -152,8 +169,22 @@ public sealed class TerminalIpcServer : IDisposable
{
try
{
using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true);
var line = await reader.ReadLineAsync(token);
var reqLenBytes = new byte[4];
var read = await stream.ReadAsync(reqLenBytes, token);
if (read < 4) return;
var reqLen = BitConverter.ToInt32(reqLenBytes);
if (reqLen is <= 0 or > 10 * 1024 * 1024) return;
var reqBytes = new byte[reqLen];
var totalRead = 0;
while (totalRead < reqLen)
{
var r = await stream.ReadAsync(reqBytes.AsMemory(totalRead), token);
if (r == 0) return;
totalRead += r;
}
var line = Encoding.UTF8.GetString(reqBytes);
if (string.IsNullOrWhiteSpace(line)) return;
var request = JsonSerializer.Deserialize<IpcRequest>(line);
@ -161,8 +192,11 @@ public sealed class TerminalIpcServer : IDisposable
? await _handler(request)
: new IpcResponse { Success = false, ErrorMessage = "Invalid request" };
var responseJson = JsonSerializer.Serialize(response) + "\n";
var responseJson = JsonSerializer.Serialize(response);
var responseBytes = Encoding.UTF8.GetBytes(responseJson);
var lengthPrefix = BitConverter.GetBytes(responseBytes.Length);
await stream.WriteAsync(lengthPrefix, token);
await stream.WriteAsync(responseBytes, token);
await stream.FlushAsync(token);
}
@ -185,7 +219,7 @@ public sealed class TerminalIpcServer : IDisposable
pipeSecurity.AddAccessRule(new PipeAccessRule(usersSid, PipeAccessRights.ReadWrite, AccessControlType.Allow));
return NamedPipeServerStreamAcl.Create(
TerminalIpcConstants.PipeName,
@"global\" + TerminalIpcConstants.PipeName,
PipeDirection.InOut,
NamedPipeServerStream.MaxAllowedServerInstances,
PipeTransmissionMode.Byte,

View file

@ -123,3 +123,4 @@ If an enumerated slot returns a serial number that has already been registered b
| USB Disconnect to UI State Update | < 500 ms | ~120 ms | PASSED |
| Cable Re-insertion to Active Ready State | < 3000 ms | 2150 ms | PASSED |
| Duplicate Scan Guard (30s lockout) | 100% rejection | 100% rejection (0 duplicate punches) | PASSED |
| Windows Service startup persistence after reboot | Reliable | NOT TESTED YET IN HARDWARE (Requires real service installation test after machine reboot to verify background sync without login) | PENDING |

26
test_pipe.cs Normal file
View file

@ -0,0 +1,26 @@
using System;
using System.IO.Pipes;
using System.Threading.Tasks;
class Program {
static async Task Main() {
var server = new NamedPipeServerStream("global\\testpipe", PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
var _ = server.WaitForConnectionAsync();
var client = new NamedPipeClientStream(".", "testpipe", PipeDirection.InOut, PipeOptions.Asynchronous);
try {
await client.ConnectAsync(1000);
Console.WriteLine("Connected with testpipe!");
} catch {
Console.WriteLine("Failed to connect with testpipe!");
}
var client2 = new NamedPipeClientStream(".", "global\\testpipe", PipeDirection.InOut, PipeOptions.Asynchronous);
try {
await client2.ConnectAsync(1000);
Console.WriteLine("Connected with global\\testpipe!");
} catch {
Console.WriteLine("Failed to connect with global\\testpipe!");
}
}
}