using System.IO.Pipes; using System.Security.AccessControl; using System.Security.Principal; using System.Text; using System.Text.Json; using KronaStaffTerminal.Models; namespace KronaStaffTerminal.Services; public sealed class IpcRequest { public string Command { get; set; } = ""; public string? Payload { get; set; } } public sealed class IpcResponse { public bool Success { get; set; } public string? ErrorMessage { get; set; } public IpcServiceStatus? Status { get; set; } public int? SyncedCount { get; set; } public string? Payload { get; set; } } public sealed class IpcServiceStatus { public bool ServiceRunning { get; set; } = true; public string ServiceVersion { get; set; } = "0.3.0"; public bool ScannerInSession0Supported { get; set; } public string ScannerDiagnostic { get; set; } = ""; public int PendingSyncCount { get; set; } public DateTimeOffset? LastSyncUtc { get; set; } public string? LastSyncError { get; set; } public string LicenseStatus { get; set; } = "Active"; } public static class TerminalIpcConstants { public const string PipeName = "KronaStaffTerminalPipe"; public const string PipeServerName = "."; } public sealed class TerminalIpcClient { public async Task IsServiceAvailableAsync(CancellationToken cancellationToken = default) { var response = await SendCommandAsync("Ping", null, cancellationToken); return response?.Success == true; } public async Task GetStatusAsync(CancellationToken cancellationToken = default) { var response = await SendCommandAsync("GetStatus", null, cancellationToken); return response?.Status; } public async Task EnqueueAttendanceAsync(AttendanceRecord record, CancellationToken cancellationToken = default) { var payload = JsonSerializer.Serialize(record); var response = await SendCommandAsync("EnqueueAttendance", payload, cancellationToken); return response?.Success == true; } public async Task TriggerSyncAsync(CancellationToken cancellationToken = default) { var response = await SendCommandAsync("TriggerSync", null, cancellationToken); if (response?.Success == true && !string.IsNullOrWhiteSpace(response.Payload)) { return JsonSerializer.Deserialize(response.Payload); } return null; } public async Task SendCommandAsync(string command, string? payload, CancellationToken cancellationToken = default) { try { 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); 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); 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(responseJson); } catch (Exception ex) when (ex is IOException or TimeoutException or OperationCanceledException) { // Service not running or unreachable return null; } } } public sealed class TerminalIpcServer : IDisposable { private readonly Func> _handler; private CancellationTokenSource? _cancellation; private Task? _listenTask; private bool _disposed; public TerminalIpcServer(Func> handler) { _handler = handler; } public void Start() { _cancellation = new CancellationTokenSource(); _listenTask = Task.Run(() => ListenLoopAsync(_cancellation.Token)); } private async Task ListenLoopAsync(CancellationToken token) { while (!token.IsCancellationRequested) { NamedPipeServerStream? server = null; try { server = CreatePipeServer(); await server.WaitForConnectionAsync(token); // Handle client in background task so server can accept next client var clientStream = server; _ = Task.Run(() => HandleClientAsync(clientStream, token), token); } catch (OperationCanceledException) { server?.Dispose(); break; } catch (Exception ex) { CrashLogService.WriteDiagnostic($"IPC server loop error: {ex.Message}"); server?.Dispose(); await Task.Delay(500, token); } } } private async Task HandleClientAsync(NamedPipeServerStream stream, CancellationToken token) { await using (stream) { try { 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(line); var response = request is not null ? await _handler(request) : new IpcResponse { Success = false, ErrorMessage = "Invalid request" }; 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); } catch (Exception ex) when (ex is not OperationCanceledException) { CrashLogService.WriteDiagnostic($"IPC client handle error: {ex.Message}"); } } } private static NamedPipeServerStream CreatePipeServer() { var pipeSecurity = new PipeSecurity(); var systemSid = new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null); var adminSid = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null); var usersSid = new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, null); pipeSecurity.AddAccessRule(new PipeAccessRule(systemSid, PipeAccessRights.FullControl, AccessControlType.Allow)); pipeSecurity.AddAccessRule(new PipeAccessRule(adminSid, PipeAccessRights.FullControl, AccessControlType.Allow)); pipeSecurity.AddAccessRule(new PipeAccessRule(usersSid, PipeAccessRights.ReadWrite, AccessControlType.Allow)); return NamedPipeServerStreamAcl.Create( @"global\" + TerminalIpcConstants.PipeName, PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous, inBufferSize: 4096, outBufferSize: 4096, pipeSecurity); } public void Dispose() { if (_disposed) return; _disposed = true; _cancellation?.Cancel(); _cancellation?.Dispose(); } }