using System.Collections.Concurrent; using System.Text.Json; using KronaStaffTerminal.Models; using KronaStaffTerminal.Services; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace KronaStaffService; public sealed class KronaStaffWorker : BackgroundService { private readonly ILogger _logger; private readonly TerminalStore _store; private readonly AttendanceSyncService _syncService; private readonly ILicenseStatusPort _licensePort; private readonly SemaphoreSlim _syncGate = new(1, 1); private readonly ConcurrentDictionary _acceptedIpcNonces = new(); private TerminalIpcServer? _ipcServer; private bool _scannerSession0Supported; private string _scannerDiagnostic = ""; private DateTimeOffset? _lastSyncUtc; private string? _lastSyncError; public KronaStaffWorker( ILogger logger, TerminalStore store, AttendanceSyncService syncService, ILicenseStatusPort licensePort) { _logger = logger; _store = store; _syncService = syncService; _licensePort = licensePort; } public override Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Krona Staff Service starting..."); CrashLogService.WriteDiagnostic("Krona Staff Windows Service starting."); // Probe whether ZKFinger SDK supports device capture in Session 0 _scannerSession0Supported = FingerprintScanner.ProbeSession0Support(out _scannerDiagnostic); _logger.LogInformation("Session 0 scanner probe: Supported={Supported}, Diagnostic='{Diag}'", _scannerSession0Supported, _scannerDiagnostic); CrashLogService.WriteDiagnostic($"Session 0 scanner probe: {_scannerDiagnostic}"); // Initialize IPC server _ipcServer = new TerminalIpcServer(HandleIpcRequestAsync); _ipcServer.Start(); _logger.LogInformation("IPC Server listening on pipe {PipeName}", TerminalIpcConstants.PipeName); return base.StartAsync(cancellationToken); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Background attendance sync worker loop started."); while (!stoppingToken.IsCancellationRequested) { try { await RunSyncCycleAsync(stoppingToken); } catch (OperationCanceledException) { break; } catch (Exception ex) { _logger.LogError(ex, "Unexpected error in background sync loop."); CrashLogService.Write(ex); } try { await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken); } catch (OperationCanceledException) { break; } } _logger.LogInformation("Background attendance sync worker loop stopped."); } public async Task RunSyncCycleAsync(CancellationToken cancellationToken) { await _syncGate.WaitAsync(cancellationToken); try { TerminalState state; try { state = await _store.LoadAsync(); } catch (Exception ex) { _lastSyncError = $"Не удалось прочитать состояние: {ex.Message}"; _logger.LogWarning(ex, "Could not load terminal state for sync."); return new AttendanceSyncResult { LastError = _lastSyncError }; } var hasPending = state.Attendance.Any(a => !a.ServerSynced); if (!hasPending) { return new AttendanceSyncResult(); } var result = await _syncService.SyncPendingAsync(state, cancellationToken); if (result.SuccessCount > 0) { await _store.SaveAsync(state); _lastSyncUtc = DateTimeOffset.UtcNow; _lastSyncError = null; _logger.LogInformation("Synced {Count} attendance record(s) to server.", result.SuccessCount); } if (result.FailedCount > 0) { _lastSyncError = result.LastError; _logger.LogWarning("Failed to sync {Count} record(s): {Error}", result.FailedCount, result.LastError); } return result; } finally { _syncGate.Release(); } } private async Task HandleIpcRequestAsync(IpcRequest request) { try { var authenticationState = await _store.LoadAsync(); var now = DateTimeOffset.UtcNow; if (!IpcRequestAuthenticator.Verify( request, authenticationState.Settings.IpcAuthenticationKey, now)) { return new IpcResponse { Success = false, ErrorMessage = "Unauthorized request" }; } foreach (var nonce in _acceptedIpcNonces.Where(item => item.Value < now.AddMinutes(-1).ToUnixTimeSeconds())) _acceptedIpcNonces.TryRemove(nonce.Key, out _); if (!_acceptedIpcNonces.TryAdd(request.Nonce, request.IssuedAtUnixSeconds)) return new IpcResponse { Success = false, ErrorMessage = "Replayed request" }; switch (request.Command) { case "Ping": return new IpcResponse { Success = true, Status = await GetCurrentStatusAsync() }; case "GetStatus": return new IpcResponse { Success = true, Status = await GetCurrentStatusAsync() }; case "GetLicenseStatus": return new IpcResponse { Success = true, Status = new IpcServiceStatus { LicenseStatus = _licensePort.GetStatus().ToString() } }; case "EnqueueAttendance": if (string.IsNullOrWhiteSpace(request.Payload)) { return new IpcResponse { Success = false, ErrorMessage = "Missing attendance payload" }; } var record = JsonSerializer.Deserialize(request.Payload); if (record is null) { return new IpcResponse { Success = false, ErrorMessage = "Invalid attendance record JSON" }; } if (!System.Text.RegularExpressions.Regex.IsMatch(record.Id, "^[0-9A-HJKMNP-TV-Z]{26}$") || !System.Text.RegularExpressions.Regex.IsMatch(record.EmployeeId, "^[0-9A-HJKMNP-TV-Z]{26}$") || record.EventType is not ("Приход" or "Уход" or "Уход автоматически") || record.OccurredAtUtc < DateTimeOffset.UtcNow.AddDays(-180) || record.OccurredAtUtc > DateTimeOffset.UtcNow.AddMinutes(5) || record.ReaderIndex < 0) { return new IpcResponse { Success = false, ErrorMessage = "Invalid attendance record" }; } await _syncGate.WaitAsync(); try { var state = await _store.LoadAsync(); var employee = state.Employees.FirstOrDefault(item => item.Id == record.EmployeeId); if (employee is null || employee.IsBlocked) { return new IpcResponse { Success = false, ErrorMessage = "Employee is unavailable" }; } record.EmployeeName = employee.DisplayName; record.ServerSynced = false; record.ServerSyncError = null; record.NotificationDelivered = false; record.NotificationError = null; if (state.Attendance.All(a => a.Id != record.Id)) { state.Attendance.Insert(0, record); await _store.SaveAsync(state); } } finally { _syncGate.Release(); } // Trigger immediate sync _ = Task.Run(() => RunSyncCycleAsync(CancellationToken.None)); return new IpcResponse { Success = true }; case "TriggerSync": var syncResult = await RunSyncCycleAsync(CancellationToken.None); return new IpcResponse { Success = true, SyncedCount = syncResult.SuccessCount, Payload = JsonSerializer.Serialize(syncResult) }; default: return new IpcResponse { Success = false, ErrorMessage = $"Unknown command: {request.Command}" }; } } catch (Exception ex) { CrashLogService.Write(ex); return new IpcResponse { Success = false, ErrorMessage = ex.Message }; } } private async Task GetCurrentStatusAsync() { var pendingCount = 0; try { var state = await _store.LoadAsync(); pendingCount = state.Attendance.Count(a => !a.ServerSynced); } catch { // ignore if unable to read count } return new IpcServiceStatus { ServiceRunning = true, ServiceVersion = "0.3.1", ScannerInSession0Supported = _scannerSession0Supported, ScannerDiagnostic = _scannerDiagnostic, PendingSyncCount = pendingCount, LastSyncUtc = _lastSyncUtc, LastSyncError = _lastSyncError, LicenseStatus = _licensePort.GetStatus().ToString() }; } public override Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Krona Staff Service stopping..."); _ipcServer?.Dispose(); CrashLogService.WriteDiagnostic("Krona Staff Windows Service stopped."); return base.StopAsync(cancellationToken); } }