From 9da7d1b00389248d82ac9f817d53c30df849284e Mon Sep 17 00:00:00 2001 From: Syed Mustafa Ahmed Naqvi Date: Fri, 4 Sep 2026 12:38:00 +0500 Subject: [PATCH] feat: make service paths and lifecycle Windows-Service safe Exe-relative paths + graceful timer shutdown / job isolation. --- ApplicationPaths.cs | 67 +++++++++++ HanvonF710XWindowsService.cs | 221 +++++++++++++++++++++++------------ 2 files changed, 214 insertions(+), 74 deletions(-) create mode 100644 ApplicationPaths.cs diff --git a/ApplicationPaths.cs b/ApplicationPaths.cs new file mode 100644 index 0000000..d8ab15e --- /dev/null +++ b/ApplicationPaths.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; + +namespace HanvonF710XAttendanceService +{ + internal static class ApplicationPaths + { + public static string BaseDirectory { get; private set; } = AppDomain.CurrentDomain.BaseDirectory ?? ""; + + public static string InternalLogs => Path.Combine(BaseDirectory, "InternalLogs"); + public static string Logs => Path.Combine(BaseDirectory, "Logs"); + public static string TemplateRawLogs => Path.Combine(BaseDirectory, "TemplateRawLogs"); + + public static void Initialize() + { + BaseDirectory = AppDomain.CurrentDomain.BaseDirectory ?? ""; + try + { + Environment.CurrentDirectory = BaseDirectory; + } + catch + { + // Best effort; paths below are always rooted to BaseDirectory. + } + + EnsureDirectory(InternalLogs); + EnsureDirectory(Logs); + EnsureDirectory(TemplateRawLogs); + EnsureDirectory(Path.Combine(Logs, "Attendance")); + EnsureDirectory(Path.Combine(Logs, "Machine User service")); + EnsureDirectory(Path.Combine(Logs, "SaveFaceTemplate logs")); + EnsureDirectory(Path.Combine(Logs, "FaceTransferLog")); + } + + public static string Resolve(string relativeOrAbsolutePath) + { + if (string.IsNullOrWhiteSpace(relativeOrAbsolutePath)) + { + return BaseDirectory; + } + + string trimmed = relativeOrAbsolutePath.Trim(); + return Path.IsPathRooted(trimmed) + ? trimmed + : Path.Combine(BaseDirectory, trimmed); + } + + public static string DatedFile(string directory, string filePrefix) + { + string datePart = DateTime.Now.Date.ToShortDateString().Replace('/', '_'); + return Path.Combine(directory, filePrefix + "_" + datePart + ".txt"); + } + + private static void EnsureDirectory(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return; + } + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + } + } +} diff --git a/HanvonF710XWindowsService.cs b/HanvonF710XWindowsService.cs index 4881c97..62971a7 100644 --- a/HanvonF710XWindowsService.cs +++ b/HanvonF710XWindowsService.cs @@ -16,48 +16,35 @@ 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 + private System.Timers.Timer timer = new System.Timers.Timer(); + private System.Timers.Timer timer1 = new System.Timers.Timer(); - // 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; + private static volatile bool _attendanceJobRunning; + private static volatile bool _templateJobRunning; + private volatile bool _stopping; + 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 { } - } + ApplicationPaths.Initialize(); - // Start buffered, cross-process-safe logging. - try { LogService.Start(); } catch { } + try { LogService.Start(); } catch (Exception ex) { Program.WriteInternalLog("LogService.Start failed: " + ex.Message); } WriteToFile("--Service is started at " + DateTime.Now); WriteInternalLog("--Service is started at " + DateTime.Now); @@ -72,12 +59,12 @@ namespace HanvonF710XAttendanceService double attendanceIntervalMs = Math.Max(1, attendanceMinutes) * 60 * 1000; double templateIntervalMs = Math.Max(1, templateMinutes) * 60 * 1000; - timer.Elapsed += new ElapsedEventHandler(OnAttendanceElapsed); + timer.Elapsed += OnAttendanceElapsed; timer.Interval = attendanceIntervalMs; timer.AutoReset = true; timer.Enabled = true; - timer1.Elapsed += new ElapsedEventHandler(OnTemplateElapsed); + timer1.Elapsed += OnTemplateElapsed; timer1.Interval = templateIntervalMs; timer1.AutoReset = true; timer1.Enabled = true; @@ -87,42 +74,117 @@ namespace HanvonF710XAttendanceService 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()); + Task.Run(() => RunStartupAttendanceJob()); } - catch { } + catch (Exception ex) + { + WriteInternalLog("Startup attendance trigger failed: " + ex.Message); + } + 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 { } - }); + Task.Run(() => RunStartupTemplateJob(delaySec)); + } + catch (Exception ex) + { + WriteInternalLog("Startup template trigger failed: " + ex.Message); } - catch { } } + protected override void OnStop() { - WriteToFile("--Service is stopped at " + DateTime.Now); + _stopping = true; + WriteToFile("--Service is stopping at " + DateTime.Now); + WriteInternalLog("--Service is stopping at " + DateTime.Now); + try { timer.Enabled = false; timer1.Enabled = false; + timer.Elapsed -= OnAttendanceElapsed; + timer1.Elapsed -= OnTemplateElapsed; + } + catch (Exception ex) + { + WriteInternalLog("OnStop timer shutdown failed: " + ex.Message); + } + + WaitForRunningJobs(); + + try + { timer.Dispose(); timer1.Dispose(); } - catch { } + catch (Exception ex) + { + WriteInternalLog("OnStop timer dispose failed: " + ex.Message); + } + try { LogService.StopAndFlush(TimeSpan.FromSeconds(5)); } catch { } + + WriteToFile("--Service is stopped at " + DateTime.Now); + WriteInternalLog("--Service is stopped at " + DateTime.Now); + } + + private void WaitForRunningJobs() + { + int waitSeconds = GetIntConfig("SERVICE_SHUTDOWN_WAIT_SECONDS", 300); + if (waitSeconds <= 0) + { + return; + } + + var deadline = DateTime.UtcNow.AddSeconds(waitSeconds); + while ((_attendanceJobRunning || _templateJobRunning) && DateTime.UtcNow < deadline) + { + Thread.Sleep(500); + } + + if (_attendanceJobRunning || _templateJobRunning) + { + WriteInternalLog($"--Service stop: jobs still running after {waitSeconds}s (attendance={_attendanceJobRunning}, template={_templateJobRunning})"); + } + } + + private void RunStartupAttendanceJob() + { + try + { + if (_stopping) + { + return; + } + + RunAttendanceJob(); + } + catch (Exception ex) + { + WriteInternalLog("Startup attendance job error: " + ex); + } + } + + private async Task RunStartupTemplateJob(int delaySec) + { + try + { + await Task.Delay(Math.Max(0, delaySec) * 1000).ConfigureAwait(false); + if (_stopping) + { + return; + } + + RunTemplateJob(); + } + catch (Exception ex) + { + WriteInternalLog("Startup template job error: " + ex); + } } private static int GetIntConfig(string key, int defaultValue) @@ -139,7 +201,6 @@ namespace HanvonF710XAttendanceService } } - // Attendance + machine-user job private void OnAttendanceElapsed(object source, ElapsedEventArgs e) { RunAttendanceJob(); @@ -147,11 +208,18 @@ namespace HanvonF710XAttendanceService private void RunAttendanceJob() { + if (_stopping) + { + WriteInternalLog("--Attendance tick skipped (service stopping) at " + DateTime.Now); + return; + } + if (!Monitor.TryEnter(_attendanceLock)) { WriteInternalLog("--Attendance tick skipped (previous attendance job still running) at " + DateTime.Now); return; } + try { if (_attendanceJobRunning) @@ -159,6 +227,7 @@ namespace HanvonF710XAttendanceService WriteInternalLog("--Attendance tick skipped (previous attendance job still running) at " + DateTime.Now); return; } + _attendanceJobRunning = true; WriteSyncCycleHeader(); @@ -172,32 +241,42 @@ namespace HanvonF710XAttendanceService } catch (Exception ex) { - WriteInternalLog("syncMachineUsers error: " + ex.Message); + WriteInternalLog("syncMachineUsers error: " + ex); WriteMachineUserServiceLog("syncMachineUsers error: " + ex.Message); } + if (_stopping) + { + WriteInternalLog("--Attendance job interrupted (service stopping) before attendance sync at " + DateTime.Now); + return; + } + try { syncAttendance(); } catch (Exception ex) { - WriteInternalLog("syncAttendance error: " + ex.Message); + WriteInternalLog("syncAttendance error: " + ex); WriteAttendanceServiceLog("syncAttendance error: " + ex.Message); } WriteInternalLog("--Attendance job finished at " + DateTime.Now); WriteAttendanceServiceLog("--Attendance job finished at " + DateTime.Now); } + catch (Exception ex) + { + WriteInternalLog("--Attendance job failed: " + ex); + WriteAttendanceServiceLog("--Attendance job failed: " + ex.Message); + } finally { - try { Program.EndSyncCycle(); } catch { } + try { Program.EndSyncCycle(); } catch (Exception ex) { WriteInternalLog("EndSyncCycle error: " + ex.Message); } _attendanceJobRunning = false; Monitor.Exit(_attendanceLock); } } - // Template job private void OnTemplateElapsed(object source, ElapsedEventArgs e) { RunTemplateJob(); @@ -205,11 +284,18 @@ namespace HanvonF710XAttendanceService private void RunTemplateJob() { + if (_stopping) + { + WriteInternalLog("--Template tick skipped (service stopping) at " + DateTime.Now); + return; + } + if (!Monitor.TryEnter(_templateLock)) { WriteInternalLog("--Template tick skipped (previous template job still running) at " + DateTime.Now); return; } + try { if (_templateJobRunning) @@ -217,6 +303,7 @@ namespace HanvonF710XAttendanceService WriteInternalLog("--Template tick skipped (previous template job still running) at " + DateTime.Now); return; } + _templateJobRunning = true; WriteJobHeaderToSimpleLogs("TEMPLATE"); @@ -229,14 +316,21 @@ namespace HanvonF710XAttendanceService } catch (Exception ex) { - WriteInternalLog("syncTemplatesIfEnabled error: " + ex.ToString()); - WriteFaceTemplateTransferLog("syncTemplatesIfEnabled error: " + ex.ToString()); - WriteSaveFaceTemplateLog("syncTemplatesIfEnabled error: " + ex.ToString()); + WriteInternalLog("syncTemplatesIfEnabled error: " + ex); + WriteFaceTemplateTransferLog("syncTemplatesIfEnabled error: " + ex); + WriteSaveFaceTemplateLog("syncTemplatesIfEnabled error: " + ex); } + WriteInternalLog("--Template job finished at " + DateTime.Now); WriteSaveFaceTemplateLog("--Template job finished at " + DateTime.Now); WriteFaceTemplateTransferLog("--Template job finished at " + DateTime.Now); } + catch (Exception ex) + { + WriteInternalLog("--Template job failed: " + ex); + WriteFaceTemplateTransferLog("--Template job failed: " + ex.Message); + WriteSaveFaceTemplateLog("--Template job failed: " + ex.Message); + } finally { _templateJobRunning = false; @@ -244,11 +338,8 @@ namespace HanvonF710XAttendanceService } } - 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); @@ -258,17 +349,15 @@ namespace HanvonF710XAttendanceService 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); @@ -278,43 +367,38 @@ namespace HanvonF710XAttendanceService 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"; + string filepath = ApplicationPaths.DatedFile(ApplicationPaths.Logs, "ServiceLog"); LogService.EnqueueLine(filepath, Message); } @@ -336,7 +420,6 @@ namespace HanvonF710XAttendanceService WriteInternalLog(line3); WriteInternalLog(line4); - // Simple per-feature logs (keep InternalLogs unchanged). WriteAttendanceServiceLog(line1); WriteAttendanceServiceLog(line2); WriteAttendanceServiceLog(line3); @@ -348,8 +431,6 @@ namespace HanvonF710XAttendanceService 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"); @@ -375,7 +456,6 @@ namespace HanvonF710XAttendanceService 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; @@ -385,7 +465,6 @@ namespace HanvonF710XAttendanceService 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; @@ -404,13 +483,11 @@ namespace HanvonF710XAttendanceService 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; @@ -419,10 +496,7 @@ namespace HanvonF710XAttendanceService 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 folderPath = Path.Combine(ApplicationPaths.Logs, folderName); string datePart = DateTime.Now.ToString("yyyy-MM-dd"); string filePath = Path.Combine(folderPath, filePrefix + "_" + datePart + ".txt"); LogService.EnqueueLine(filePath, message); @@ -447,11 +521,10 @@ namespace HanvonF710XAttendanceService { 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); + Program.WriteInternalLog(Message); } } }