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);
}
}
}