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 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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
Loading…
Reference in a new issue