krona-staff/apps/KronaStaffService/KronaStaffWorker.cs

245 lines
8.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<KronaStaffWorker> _logger;
private readonly TerminalStore _store;
private readonly AttendanceSyncService _syncService;
private readonly ILicenseStatusPort _licensePort;
private readonly SemaphoreSlim _syncGate = new(1, 1);
private TerminalIpcServer? _ipcServer;
private bool _scannerSession0Supported;
private string _scannerDiagnostic = "";
private DateTimeOffset? _lastSyncUtc;
private string? _lastSyncError;
public KronaStaffWorker(
ILogger<KronaStaffWorker> 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<AttendanceSyncResult> 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<IpcResponse> HandleIpcRequestAsync(IpcRequest request)
{
try
{
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<AttendanceRecord>(request.Payload);
if (record is null)
{
return new IpcResponse { Success = false, ErrorMessage = "Invalid attendance record JSON" };
}
await _syncGate.WaitAsync();
try
{
var state = await _store.LoadAsync();
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<IpcServiceStatus> 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.0",
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);
}
}