Fix IPC framing, add global pipe name, and auto-reconnect tests
This commit is contained in:
parent
b3fdec1eb2
commit
39f0840087
4 changed files with 86 additions and 8 deletions
|
|
@ -15,6 +15,7 @@ public sealed class MockFingerprintScanner : IFingerprintScanner
|
||||||
public IReadOnlyList<ScannerDeviceInfo> DiscoveredDevices => _devices;
|
public IReadOnlyList<ScannerDeviceInfo> DiscoveredDevices => _devices;
|
||||||
public bool IsConnected => State == ScannerState.Connected || State == ScannerState.Busy;
|
public bool IsConnected => State == ScannerState.Connected || State == ScannerState.Busy;
|
||||||
public int SkippedTemplateCount { get; set; } = 0;
|
public int SkippedTemplateCount { get; set; } = 0;
|
||||||
|
public int ConnectCallCount { get; private set; } = 0;
|
||||||
|
|
||||||
public event Action<int, int>? FingerRecognized;
|
public event Action<int, int>? FingerRecognized;
|
||||||
public event Action<int>? RegistrationProgress;
|
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)
|
public void Connect(int readerIndex, IEnumerable<(int FingerprintId, string Template)> templates)
|
||||||
{
|
{
|
||||||
|
ConnectCallCount++;
|
||||||
if (_devices.Count == 0)
|
if (_devices.Count == 0)
|
||||||
{
|
{
|
||||||
State = ScannerState.DriverError;
|
State = ScannerState.DriverError;
|
||||||
|
|
|
||||||
|
|
@ -73,4 +73,45 @@ public class ScannerRecoveryTests
|
||||||
Assert.Null(error);
|
Assert.Null(error);
|
||||||
Assert.False(rehashNeeded);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ public sealed class IpcServiceStatus
|
||||||
public static class TerminalIpcConstants
|
public static class TerminalIpcConstants
|
||||||
{
|
{
|
||||||
public const string PipeName = "KronaStaffTerminalPipe";
|
public const string PipeName = "KronaStaffTerminalPipe";
|
||||||
|
public const string PipeServerName = ".";
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class TerminalIpcClient
|
public sealed class TerminalIpcClient
|
||||||
|
|
@ -74,20 +75,36 @@ public sealed class TerminalIpcClient
|
||||||
{
|
{
|
||||||
try
|
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);
|
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
timeoutCts.CancelAfter(TimeSpan.FromSeconds(2));
|
timeoutCts.CancelAfter(TimeSpan.FromSeconds(2));
|
||||||
|
|
||||||
await pipe.ConnectAsync(timeoutCts.Token);
|
await pipe.ConnectAsync(timeoutCts.Token);
|
||||||
|
|
||||||
var request = new IpcRequest { Command = command, Payload = payload };
|
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 bytes = Encoding.UTF8.GetBytes(json);
|
||||||
|
var lengthPrefix = BitConverter.GetBytes(bytes.Length);
|
||||||
|
await pipe.WriteAsync(lengthPrefix, cancellationToken);
|
||||||
await pipe.WriteAsync(bytes, cancellationToken);
|
await pipe.WriteAsync(bytes, cancellationToken);
|
||||||
await pipe.FlushAsync(cancellationToken);
|
await pipe.FlushAsync(cancellationToken);
|
||||||
|
|
||||||
using var reader = new StreamReader(pipe, Encoding.UTF8);
|
var respLenBytes = new byte[4];
|
||||||
var responseJson = await reader.ReadLineAsync(cancellationToken);
|
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;
|
if (string.IsNullOrWhiteSpace(responseJson)) return null;
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<IpcResponse>(responseJson);
|
return JsonSerializer.Deserialize<IpcResponse>(responseJson);
|
||||||
|
|
@ -152,8 +169,22 @@ public sealed class TerminalIpcServer : IDisposable
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true);
|
var reqLenBytes = new byte[4];
|
||||||
var line = await reader.ReadLineAsync(token);
|
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;
|
if (string.IsNullOrWhiteSpace(line)) return;
|
||||||
|
|
||||||
var request = JsonSerializer.Deserialize<IpcRequest>(line);
|
var request = JsonSerializer.Deserialize<IpcRequest>(line);
|
||||||
|
|
@ -161,8 +192,11 @@ public sealed class TerminalIpcServer : IDisposable
|
||||||
? await _handler(request)
|
? await _handler(request)
|
||||||
: new IpcResponse { Success = false, ErrorMessage = "Invalid 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 responseBytes = Encoding.UTF8.GetBytes(responseJson);
|
||||||
|
var lengthPrefix = BitConverter.GetBytes(responseBytes.Length);
|
||||||
|
|
||||||
|
await stream.WriteAsync(lengthPrefix, token);
|
||||||
await stream.WriteAsync(responseBytes, token);
|
await stream.WriteAsync(responseBytes, token);
|
||||||
await stream.FlushAsync(token);
|
await stream.FlushAsync(token);
|
||||||
}
|
}
|
||||||
|
|
@ -185,7 +219,7 @@ public sealed class TerminalIpcServer : IDisposable
|
||||||
pipeSecurity.AddAccessRule(new PipeAccessRule(usersSid, PipeAccessRights.ReadWrite, AccessControlType.Allow));
|
pipeSecurity.AddAccessRule(new PipeAccessRule(usersSid, PipeAccessRights.ReadWrite, AccessControlType.Allow));
|
||||||
|
|
||||||
return NamedPipeServerStreamAcl.Create(
|
return NamedPipeServerStreamAcl.Create(
|
||||||
TerminalIpcConstants.PipeName,
|
@"global\" + TerminalIpcConstants.PipeName,
|
||||||
PipeDirection.InOut,
|
PipeDirection.InOut,
|
||||||
NamedPipeServerStream.MaxAllowedServerInstances,
|
NamedPipeServerStream.MaxAllowedServerInstances,
|
||||||
PipeTransmissionMode.Byte,
|
PipeTransmissionMode.Byte,
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
| USB Disconnect to UI State Update | < 500 ms | ~120 ms | PASSED |
|
||||||
| Cable Re-insertion to Active Ready State | < 3000 ms | 2150 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 |
|
| 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 |
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue