using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.IO; using System.Linq; using System.ServiceProcess; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Timers; using System.Configuration; namespace HanvonF710XAttendanceService { public partial class HanvonF710XWindowsService : ServiceBase { // Separate timers for attendance/users and template sync. System.Timers.Timer timer = new System.Timers.Timer(); // attendance + users System.Timers.Timer timer1 = new System.Timers.Timer(); // templates // Independent locks/flags so jobs don't block each other. private static readonly object _attendanceLock = new object(); private static readonly object _templateLock = new object(); private static bool _attendanceJobRunning = false; private static bool _templateJobRunning = false; public HanvonF710XWindowsService() { InitializeComponent(); } /// Starts timers and jobs when running outside the Windows Service Control Manager. public void StartForConsole(string[] args) { OnStart(args ?? Array.Empty()); } /// Stops timers and flushes logs when running in console/debug mode. public void StopForConsole() { OnStop(); } protected override void OnStart(string[] args) { // So the service finds HwDevComm.dll (and sibling DLLs) in the EXE folder string exeDir = AppDomain.CurrentDomain.BaseDirectory; try { Environment.CurrentDirectory = exeDir; } catch (Exception ex) { try { WriteInternalLog("OnStart set CurrentDirectory failed: " + ex.Message); } catch { } } // Start buffered, cross-process-safe logging. try { LogService.Start(); } catch { } WriteToFile("--Service is started at " + DateTime.Now); WriteInternalLog("--Service is started at " + DateTime.Now); if (Program.IsConsoleMode) { Console.WriteLine("--Service is started at " + DateTime.Now); } int attendanceMinutes = GetIntConfig("ATTENDANCE_JOB_INTERVAL_MINUTES", 20); int templateMinutes = GetIntConfig("TEMPLATE_JOB_INTERVAL_MINUTES", 240); double attendanceIntervalMs = Math.Max(1, attendanceMinutes) * 60 * 1000; double templateIntervalMs = Math.Max(1, templateMinutes) * 60 * 1000; timer.Elapsed += new ElapsedEventHandler(OnAttendanceElapsed); timer.Interval = attendanceIntervalMs; timer.AutoReset = true; timer.Enabled = true; timer1.Elapsed += new ElapsedEventHandler(OnTemplateElapsed); timer1.Interval = templateIntervalMs; timer1.AutoReset = true; timer1.Enabled = true; WriteInternalLog($"Attendance job interval: {attendanceMinutes} minutes"); WriteInternalLog($"Template job interval: {templateMinutes} minutes"); WriteToFile($"Attendance job interval: {attendanceMinutes} minutes"); WriteToFile($"Template job interval: {templateMinutes} minutes"); // Run both jobs once immediately on startup (non-blocking). // Uses the same guarded execution path as the timer callbacks. try { WriteInternalLog("--Startup trigger: running attendance job immediately at " + DateTime.Now); Task.Run(() => RunAttendanceJob()); } catch { } try { int delaySec = 30; WriteInternalLog("--Startup trigger: running template job after " + delaySec + "s at " + DateTime.Now); Task.Run(async () => { try { await Task.Delay(delaySec * 1000).ConfigureAwait(false); RunTemplateJob(); } catch { } }); } catch { } } protected override void OnStop() { WriteToFile("--Service is stopped at " + DateTime.Now); try { timer.Enabled = false; timer1.Enabled = false; timer.Dispose(); timer1.Dispose(); } catch { } try { LogService.StopAndFlush(TimeSpan.FromSeconds(5)); } catch { } } private static int GetIntConfig(string key, int defaultValue) { try { var raw = ConfigurationManager.AppSettings[key]; if (string.IsNullOrWhiteSpace(raw)) return defaultValue; return int.TryParse(raw, out var v) && v > 0 ? v : defaultValue; } catch { return defaultValue; } } // Attendance + machine-user job private void OnAttendanceElapsed(object source, ElapsedEventArgs e) { RunAttendanceJob(); } private void RunAttendanceJob() { if (!Monitor.TryEnter(_attendanceLock)) { WriteInternalLog("--Attendance tick skipped (previous attendance job still running) at " + DateTime.Now); return; } try { if (_attendanceJobRunning) { WriteInternalLog("--Attendance tick skipped (previous attendance job still running) at " + DateTime.Now); return; } _attendanceJobRunning = true; WriteSyncCycleHeader(); Program.BeginSyncCycle(); WriteInternalLog("--Attendance job started at " + DateTime.Now); WriteAttendanceServiceLog("--Attendance job started at " + DateTime.Now); try { syncMachineUsers(); } catch (Exception ex) { WriteInternalLog("syncMachineUsers error: " + ex.Message); WriteMachineUserServiceLog("syncMachineUsers error: " + ex.Message); } try { syncAttendance(); } catch (Exception ex) { WriteInternalLog("syncAttendance error: " + ex.Message); WriteAttendanceServiceLog("syncAttendance error: " + ex.Message); } WriteInternalLog("--Attendance job finished at " + DateTime.Now); WriteAttendanceServiceLog("--Attendance job finished at " + DateTime.Now); } finally { try { Program.EndSyncCycle(); } catch { } _attendanceJobRunning = false; Monitor.Exit(_attendanceLock); } } // Template job private void OnTemplateElapsed(object source, ElapsedEventArgs e) { RunTemplateJob(); } private void RunTemplateJob() { if (!Monitor.TryEnter(_templateLock)) { WriteInternalLog("--Template tick skipped (previous template job still running) at " + DateTime.Now); return; } try { if (_templateJobRunning) { WriteInternalLog("--Template tick skipped (previous template job still running) at " + DateTime.Now); return; } _templateJobRunning = true; WriteJobHeaderToSimpleLogs("TEMPLATE"); WriteInternalLog("--Template job started at " + DateTime.Now); WriteSaveFaceTemplateLog("--Template job started at " + DateTime.Now); WriteFaceTemplateTransferLog("--Template job started at " + DateTime.Now); try { syncTemplatesIfEnabled(); } catch (Exception ex) { WriteInternalLog("syncTemplatesIfEnabled error: " + ex.ToString()); WriteFaceTemplateTransferLog("syncTemplatesIfEnabled error: " + ex.ToString()); WriteSaveFaceTemplateLog("syncTemplatesIfEnabled error: " + ex.ToString()); } WriteInternalLog("--Template job finished at " + DateTime.Now); WriteSaveFaceTemplateLog("--Template job finished at " + DateTime.Now); WriteFaceTemplateTransferLog("--Template job finished at " + DateTime.Now); } finally { _templateJobRunning = false; Monitor.Exit(_templateLock); } } public void syncAttendance() { WriteToFile("--Sync Attendance is recall at " + DateTime.Now); WriteInternalLog("--Sync Attendance is recall at " + DateTime.Now); WriteAttendanceServiceLog("--Sync Attendance is recall at " + DateTime.Now); List responses = Program.syncAttendance(); foreach (var response in responses) { WriteToFile(response); WriteInternalLog(response); // Attendance simple log: only machine-level results and connectivity. if (IsAttendanceSimpleLine(response)) { WriteAttendanceServiceLog(response); } } } public void syncMachineUsers() { WriteToFile("--Sync Machine users is recall at " + DateTime.Now); WriteInternalLog("--Sync Machine users is recall at " + DateTime.Now); WriteMachineUserServiceLog("--Sync Machine users is recall at " + DateTime.Now); List responses = Program.syncMachineUsers(); foreach (var response in responses) { WriteToFile(response); WriteInternalLog(response); // Machine user simple log: only machine totals and connectivity. if (IsMachineUserSimpleLine(response)) { WriteMachineUserServiceLog(response); } } } public void syncTemplatesIfEnabled() { WriteToFile("--Template sync is recall at " + DateTime.Now); WriteInternalLog("--Template sync is recall at " + DateTime.Now); // We'll route template logs into either SaveFaceTemplate or FaceTransfer logs based on TRANSFER_MODE. List responses = Program.syncTemplates(); foreach (var response in responses) { WriteToFile(response); WriteInternalLog(response); // SaveFaceTemplate logs: DEVICE_TO_DB and any "Serial number--" lines. if (IsSaveFaceTemplateLine(response)) { WriteSaveFaceTemplateLog(response); } // FaceTransfer logs: DB_TO_DEVICE / DEVICE_TO_DEVICE and connectivity issues. if (IsFaceTransferLine(response)) { WriteFaceTemplateTransferLog(response); } } } public void WriteToFile(string Message) { string filepath = AppDomain.CurrentDomain.BaseDirectory + "\\Logs\\ServiceLog_" + DateTime.Now.Date.ToShortDateString().Replace('/', '_') + ".txt"; LogService.EnqueueLine(filepath, Message); } private void WriteSyncCycleHeader() { string time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); string line1 = "========================================================"; string line2 = "SYNC CYCLE START"; string line3 = "Time: " + time; string line4 = "========================="; WriteToFile(line1); WriteToFile(line2); WriteToFile(line3); WriteToFile(line4); WriteInternalLog(line1); WriteInternalLog(line2); WriteInternalLog(line3); WriteInternalLog(line4); // Simple per-feature logs (keep InternalLogs unchanged). WriteAttendanceServiceLog(line1); WriteAttendanceServiceLog(line2); WriteAttendanceServiceLog(line3); WriteAttendanceServiceLog(line4); WriteMachineUserServiceLog(line1); WriteMachineUserServiceLog(line2); WriteMachineUserServiceLog(line3); WriteMachineUserServiceLog(line4); } // Ensures per-feature simple logs have a header even when a job doesn't call WriteSyncCycleHeader. // Used by template job. private void WriteJobHeaderToSimpleLogs(string jobName) { string time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); string line1 = "========================================================"; string line2 = "SYNC CYCLE START"; string line3 = "Time: " + time; string line4 = "========================="; string line5 = "JOB: " + jobName; WriteSaveFaceTemplateLog(line1); WriteSaveFaceTemplateLog(line2); WriteSaveFaceTemplateLog(line3); WriteSaveFaceTemplateLog(line4); WriteSaveFaceTemplateLog(line5); WriteFaceTemplateTransferLog(line1); WriteFaceTemplateTransferLog(line2); WriteFaceTemplateTransferLog(line3); WriteFaceTemplateTransferLog(line4); WriteFaceTemplateTransferLog(line5); } private static bool IsAttendanceSimpleLine(string line) { if (string.IsNullOrWhiteSpace(line)) return false; // Keep only the machine summary lines. if (line.IndexOf(" has ", StringComparison.OrdinalIgnoreCase) >= 0 && line.IndexOf(" records", StringComparison.OrdinalIgnoreCase) >= 0) return true; if (line.IndexOf(" is not connected", StringComparison.OrdinalIgnoreCase) >= 0) return true; return false; } private static bool IsMachineUserSimpleLine(string line) { if (string.IsNullOrWhiteSpace(line)) return false; // Keep only "MACHINE X -> N", "X removed from MACHINE Y", and connectivity lines. if (line.IndexOf(" -> ", StringComparison.OrdinalIgnoreCase) >= 0) return true; if (line.IndexOf(" removed from ", StringComparison.OrdinalIgnoreCase) >= 0) return true; if (line.IndexOf(" is not connected", StringComparison.OrdinalIgnoreCase) >= 0) return true; return false; } private static bool IsSaveFaceTemplateLine(string line) { if (string.IsNullOrWhiteSpace(line)) return false; if (line.StartsWith("Serial number--", StringComparison.OrdinalIgnoreCase)) return true; if (line.IndexOf("SaveTemplateDB", StringComparison.OrdinalIgnoreCase) >= 0) return true; if (line.IndexOf("DEVICE_TO_DB", StringComparison.OrdinalIgnoreCase) >= 0) return true; return false; } private static bool IsFaceTransferLine(string line) { if (string.IsNullOrWhiteSpace(line)) return false; // Any transfer mode other than DEVICE_TO_DB and connectivity issues. if (line.IndexOf("DB_TO_DEVICE", StringComparison.OrdinalIgnoreCase) >= 0) return true; if (line.IndexOf("DEVICE_TO_DEVICE", StringComparison.OrdinalIgnoreCase) >= 0) return true; if (line.IndexOf("[DB_TO_DEVICE]", StringComparison.OrdinalIgnoreCase) >= 0) return true; if (line.IndexOf("TemplateDistribution", StringComparison.OrdinalIgnoreCase) >= 0) return true; if (line.IndexOf("TemplateRegistrationTargets", StringComparison.OrdinalIgnoreCase) >= 0) return true; // Peer distribution uses job id "TemplateDist:" in [TemplateJob ...] lines from GetEmployeeID on targets. if (line.IndexOf("TemplateDist:", StringComparison.OrdinalIgnoreCase) >= 0) return true; if (line.IndexOf("SetEmployee", StringComparison.OrdinalIgnoreCase) >= 0) return true; if (line.IndexOf(" is not connected", StringComparison.OrdinalIgnoreCase) >= 0) return true; return false; } private static void AppendLineToDatedLog(string folderName, string filePrefix, string message) { string baseDir = AppDomain.CurrentDomain.BaseDirectory; string logsRoot = Path.Combine(baseDir, "Logs"); string folderPath = Path.Combine(logsRoot, folderName); string datePart = DateTime.Now.ToString("yyyy-MM-dd"); string filePath = Path.Combine(folderPath, filePrefix + "_" + datePart + ".txt"); LogService.EnqueueLine(filePath, message); } private static void WriteAttendanceServiceLog(string message) { AppendLineToDatedLog("Attendance", "AttendanceServiceLogs", message); } private static void WriteMachineUserServiceLog(string message) { AppendLineToDatedLog("Machine User service", "MachineServiceLogs", message); } private static void WriteSaveFaceTemplateLog(string message) { AppendLineToDatedLog("SaveFaceTemplate logs", "SaveFaceTemplateLogs", message); } private static void WriteFaceTemplateTransferLog(string message) { AppendLineToDatedLog("FaceTransferLog", "FaceTransferLogs", message); } public void WriteInternalLog(string Message) { string filepath = AppDomain.CurrentDomain.BaseDirectory + "\\InternalLogs\\InternalLog_" + DateTime.Now.Date.ToShortDateString().Replace('/', '_') + ".txt"; LogService.EnqueueLine(filepath, Message); } } }