using MySql.Data.MySqlClient; using System; using System.Collections.Generic; using System.Data; using System.Diagnostics; using System.IO; using System.Configuration; using System.Linq; using System.Runtime.InteropServices; using System.ServiceProcess; using System.Text; using System.Threading; using System.Threading.Tasks; namespace HanvonF710XAttendanceService { static class Program { /// True when started via console/debug entry (not SCM). internal static bool IsConsoleMode { get; private set; } private static MySqlConnection connection; //public static int TotalEmp; //static List DBusers = new List(); static bool result; private static readonly object _unreachableLock = new object(); private static readonly List<(string MachineId, string MachineIp, int SiteId)> _unreachableThisCycle = new List<(string, string, int)>(); private static void Initialize() { connection = new MySqlConnection(GetConnectionString()); } internal static string GetConnectionString() { var configured = ConfigurationManager.ConnectionStrings["HrmsDb"]?.ConnectionString; if (!string.IsNullOrWhiteSpace(configured)) { return configured; } throw new InvalidOperationException("Connection string 'HrmsDb' is not configured in App.config."); } internal static MySqlConnection OpenDbConnectionWithRetry(string context) { int attempts = GetIntAppSetting("DB_CONNECTION_RETRY_ATTEMPTS", 3); int delayMs = GetIntAppSetting("DB_CONNECTION_RETRY_DELAY_MS", 2000); Exception lastError = null; for (int attempt = 1; attempt <= attempts; attempt++) { try { var conn = new MySqlConnection(GetConnectionString()); conn.Open(); if (attempt > 1) { WriteInternalLog($"[DB] {context}: connection succeeded on attempt {attempt}/{attempts}"); } return conn; } catch (Exception ex) { lastError = ex; WriteInternalLog($"[DB] {context}: connection attempt {attempt}/{attempts} failed: {ex.Message}"); if (attempt < attempts) { Thread.Sleep(delayMs * attempt); } } } throw new InvalidOperationException($"[DB] {context}: unable to open connection after {attempts} attempts.", lastError); } private static int GetIntAppSetting(string key, int defaultValue) { try { var raw = ConfigurationManager.AppSettings[key]; if (string.IsNullOrWhiteSpace(raw)) return defaultValue; return int.TryParse(raw.Trim(), out var v) && v > 0 ? v : defaultValue; } catch { return defaultValue; } } private static bool GetBoolAppSetting(string key, bool defaultValue) { try { var value = ConfigurationManager.AppSettings[key]; if (string.IsNullOrWhiteSpace(value)) return defaultValue; return bool.TryParse(value.Trim(), out var parsed) ? parsed : defaultValue; } catch { return defaultValue; } } public const string HwDevCommDLL = @"HwDevComm.dll"; public const string HDCP_UtilsDLL = @"HDCP_Utils.dll"; [DllImport(HwDevCommDLL, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)] public static extern int HwDev_Execute(string pDevInfoBuf, int nDevInfoLen, IntPtr pSendBuf, int nSendLen, ref IntPtr pRecvBuf, ref uint pRecvLen, CallBack pFuncTotalDone); [DllImport(HwDevCommDLL, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)] public static extern int HwDev_Finish(ref IntPtr pRecvBuf); [DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)] public static extern bool GlobalUnlock(IntPtr hMem); [DllImport("kernel32.dll")] public static extern IntPtr GlobalAlloc(int uFlags, int dwBytes); [DllImport("kernel32.dll")] public static extern IntPtr GlobalFree(IntPtr hMem); [DllImport("kernel32.dll")] public static extern IntPtr GlobalLock(IntPtr hMem); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern bool SetDllDirectory(string lpPathName); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool AllocConsole(); /// /// Entry point. Runs as a Windows Service when started by the SCM; /// runs in console/debug mode when interactive or when --console / --debug is passed. /// static void Main(string[] args) { string baseDir = AppDomain.CurrentDomain.BaseDirectory; try { SetDllDirectory(baseDir); Environment.CurrentDirectory = baseDir; } catch { } if (ShouldRunAsConsole(args)) { RunAsConsole(args); } else { ServiceBase.Run(new ServiceBase[] { new HanvonF710XWindowsService() }); } } private static bool ShouldRunAsConsole(string[] args) { if (HasArg(args, "--service", "/service")) { return false; } if (HasArg(args, "--console", "/console", "-c", "--debug", "/debug", "-d")) { return true; } #if DEBUG // F5 / debugger: prefer console so OnStart/OnStop can be exercised without InstallUtil. if (Environment.UserInteractive) { return true; } #endif return Environment.UserInteractive; } private static bool HasArg(string[] args, params string[] flags) { if (args == null || args.Length == 0 || flags == null) return false; foreach (var arg in args) { if (string.IsNullOrWhiteSpace(arg)) continue; foreach (var flag in flags) { if (string.Equals(arg.Trim(), flag, StringComparison.OrdinalIgnoreCase)) { return true; } } } return false; } private static void RunAsConsole(string[] args) { IsConsoleMode = true; try { AllocConsole(); } catch { } Console.Title = "Hanvon F710X Attendance Service (Console Mode)"; Console.WriteLine("========================================================"); Console.WriteLine("Hanvon F710X Attendance Service — CONSOLE / DEBUG MODE"); Console.WriteLine("Press Ctrl+C or Enter to stop."); Console.WriteLine("========================================================"); Console.WriteLine(); var service = new HanvonF710XWindowsService(); var stop = new ManualResetEvent(false); ConsoleCancelEventHandler cancelHandler = (sender, e) => { e.Cancel = true; Console.WriteLine(); Console.WriteLine("Stop requested..."); stop.Set(); }; Console.CancelKeyPress += cancelHandler; try { service.StartForConsole(args ?? Array.Empty()); Console.WriteLine("Service logic started. Waiting for stop signal..."); Console.WriteLine("(Press Enter to stop)"); // Wait for Ctrl+C or Enter on a background thread. var enterWait = Task.Run(() => { try { Console.ReadLine(); } catch { } stop.Set(); }); stop.WaitOne(); } catch (Exception ex) { Console.WriteLine("Fatal error: " + ex); WriteInternalLog("Console mode fatal error: " + ex); } finally { try { Console.CancelKeyPress -= cancelHandler; } catch { } try { Console.WriteLine("Stopping..."); service.StopForConsole(); Console.WriteLine("Stopped."); } catch (Exception ex) { Console.WriteLine("Stop error: " + ex.Message); } } } public static void WriteToFile(string Message) { string path = AppDomain.CurrentDomain.BaseDirectory + "\\TemplateRawLogs"; if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } string filepath = AppDomain.CurrentDomain.BaseDirectory + "\\TemplateRawLogs\\ServiceLog_" + DateTime.Now.Date.ToShortDateString().Replace('/', '_') + ".txt"; LogService.EnqueueLine(filepath, Message); } public static void WriteInternalLog(string message) { try { string filepath = AppDomain.CurrentDomain.BaseDirectory + "\\InternalLogs\\InternalLog_" + DateTime.Now.Date.ToShortDateString().Replace('/', '_') + ".txt"; LogService.EnqueueLine(filepath, message); } catch { // swallow internal logging failures } } public static void RecordUnreachableMachine(AttendanceMachine machine, string reason) { try { string filepath = AppDomain.CurrentDomain.BaseDirectory + "\\InternalLogs\\UnreachableMachines_" + DateTime.Now.Date.ToShortDateString().Replace('/', '_') + ".txt"; LogService.EnqueueLine(filepath, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\tmachine_id={machine.MachineId}\tip={machine.MachineIp}\tsite_id={machine.SiteId}\treason={reason}"); lock (_unreachableLock) { _unreachableThisCycle.Add((machine.MachineId, machine.MachineIp, machine.SiteId)); } } catch { // ignore logging failures } } public static void BeginSyncCycle() { lock (_unreachableLock) { _unreachableThisCycle.Clear(); } } public static void EndSyncCycle() { List<(string MachineId, string MachineIp, int SiteId)> snapshot; lock (_unreachableLock) { if (_unreachableThisCycle.Count == 0) { return; } snapshot = new List<(string, string, int)>(_unreachableThisCycle); } try { string filepath = AppDomain.CurrentDomain.BaseDirectory + "\\InternalLogs\\UnreachableMachines_" + DateTime.Now.Date.ToShortDateString().Replace('/', '_') + ".txt"; LogService.EnqueueLine(filepath, ""); LogService.EnqueueLine(filepath, "---"); LogService.EnqueueLine(filepath, ""); LogService.EnqueueLine(filepath, "UNREACHABLE MACHINES"); LogService.EnqueueLine(filepath, "Detected At: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); LogService.EnqueueLine(filepath, "--------------------------------"); LogService.EnqueueLine(filepath, ""); var groups = snapshot .GroupBy(e => e.SiteId) .OrderBy(g => g.Key); foreach (var g in groups) { LogService.EnqueueLine(filepath, "SITE " + g.Key); foreach (var m in g.OrderBy(x => x.MachineId)) { LogService.EnqueueLine(filepath, " " + m.MachineId + " " + m.MachineIp); } LogService.EnqueueLine(filepath, ""); } } catch { // ignore logging failures } } public static List syncAttendance() { List responses = new List(); Initialize(); using (var conn = OpenDbConnectionWithRetry("syncAttendance")) { AttendanceMachineDAO attendanceMachineDAO = new AttendanceMachineDAO(); var machines = GetMachinesForAttendanceAndUsers(attendanceMachineDAO, conn, responses); foreach (var machine in machines) { machine.Status = "SYNCING"; attendanceMachineDAO.update(machine, conn); responses.Add(syncAttendance(machine, conn)); if (!string.Equals(machine.Status, "NOT CONNECTED", StringComparison.OrdinalIgnoreCase)) { machine.Status = "IDLE"; } Console.WriteLine(machine.MachineId + " => " + machine.LastSyncDate); attendanceMachineDAO.update(machine, conn); } } return responses; } public static string syncAttendance(AttendanceMachine machine, MySqlConnection conn) { if (DeviceProtocol.UseHttp()) { return syncAttendanceHttp(machine, conn); } return syncAttendanceHdcp(machine, conn); } private static string syncAttendanceHttp(AttendanceMachine machine, MySqlConnection conn) { string response = ""; DateTime prevLastSync = machine.LastSyncDate; DateTime now = DateTime.Now.AddMinutes(-5); var client = HanvonHttpApiClient.ForMachine(machine); WriteInternalLog($"[AttendanceConnect] protocol=HTTP machine_id={machine.MachineId} ip={machine.MachineIp} http_port={ConfigurationManager.AppSettings["DEVICE_HTTP_PORT"] ?? "80"} username={MachineScope.GetDeviceUsername()} from={prevLastSync:yyyy-MM-dd HH:mm:ss}"); if (!client.TryLogin(out string loginErr)) { response = machine.MachineId + " is not connected !"; machine.Status = "NOT CONNECTED"; WriteInternalLog($"[AttendanceConnectFail] protocol=HTTP machine_id={machine.MachineId} ip={machine.MachineIp} err={loginErr}"); RecordUnreachableMachine(machine, "syncAttendance HTTP: " + loginErr); return response; } var logs = client.GetLogs(prevLastSync, null, out string getErr); if (!string.IsNullOrEmpty(getErr) && (logs == null || logs.Count == 0)) { response = machine.MachineId + " is not connected !"; machine.Status = "NOT CONNECTED"; WriteInternalLog($"[AttendanceConnectFail] protocol=HTTP getlog machine_id={machine.MachineId} ip={machine.MachineIp} err={getErr}"); RecordUnreachableMachine(machine, "syncAttendance HTTP getlog: " + getErr); return response; } var parsedRows = new List(); foreach (var log in logs) { parsedRows.Add(new Attendance(log.AcNo, log.CheckTime, false, machine.MachineId, "1", machine.MachineIp, DateTime.Now)); } int totalFetched = parsedRows.Count; response = machine.MachineId + " has " + totalFetched + " records !"; WriteInternalLog($"[AttendanceSync] protocol=HTTP machine_id={machine.MachineId} ip={machine.MachineIp} fetched_total={totalFetched}"); var summary = ProcessAttendanceInBatches(parsedRows, machine, conn, prevLastSync); if (GetBoolAppSetting("ATTENDANCE_DELETE_AFTER_INSERT", true) && totalFetched > 0) { if (client.CleanLog(out string cleanErr)) { WriteInternalLog($"[AttendanceDelete] protocol=HTTP machine_id={machine.MachineId} ip={machine.MachineIp} cleanlog=OK"); } else { WriteInternalLog($"[AttendanceDelete] protocol=HTTP machine_id={machine.MachineId} ip={machine.MachineIp} cleanlog=FAIL err={cleanErr}"); } } machine.LastSyncDate = now; WriteInternalLog( $"[AttendanceSync] machine_id={machine.MachineId} ip={machine.MachineIp} prev_last_sync={prevLastSync:yyyy-MM-dd HH:mm:ss} new_last_sync={machine.LastSyncDate:yyyy-MM-dd HH:mm:ss} fetched_total={totalFetched} first_checktime={(summary.FirstCheckTime.HasValue ? summary.FirstCheckTime.Value.ToString("yyyy-MM-dd HH:mm:ss") : "NULL")} last_checktime={(summary.LastCheckTime.HasValue ? summary.LastCheckTime.Value.ToString("yyyy-MM-dd HH:mm:ss") : "NULL")}" ); return response; } private static string syncAttendanceHdcp(AttendanceMachine machine, MySqlConnection conn) { string response = ""; DateTime prevLastSync = machine.LastSyncDate; string requestedStartTime = prevLastSync.ToString("yyyy-MM-dd HH:mm:ss"); DateTime now = DateTime.Now.AddMinutes(-5); CallBack myCallBack = new CallBack(Program.BeCalled); string devInfo = machine.GetDeviceInfo(); string str4 = "GetRecord(start_time=\"" + requestedStartTime + "\")"; string str5 = ""; uint num2 = 0; WriteInternalLog($"[AttendanceConnect] protocol=HDCP machine_id={machine.MachineId} ip={machine.MachineIp} port={machine.PortNumber} username={MachineScope.GetDeviceUsername()} devInfo={machine.GetDeviceInfoForLog()} cmd={str4}"); int connectRc = test(devInfo, devInfo.Length, str4, str4.Length, ref str5, ref num2, myCallBack); if (connectRc != 0) { Console.WriteLine("Not Connected"); response = machine.MachineId + " is not connected !"; machine.Status = "NOT CONNECTED"; WriteInternalLog($"[AttendanceConnectFail] machine_id={machine.MachineId} ip={machine.MachineIp} port={machine.PortNumber} rc={connectRc}"); RecordUnreachableMachine(machine, $"syncAttendance: device not connected rc={connectRc} port={machine.PortNumber}"); } else { Console.WriteLine("Connected"); var parsedRows = ParseAttendanceRows(str5, machine); int totalFetched = parsedRows.Count; response = machine.MachineId + " has " + totalFetched + " records !"; Console.WriteLine(response); var summary = ProcessAttendanceInBatches(parsedRows, machine, conn, prevLastSync); machine.LastSyncDate = now; WriteInternalLog( $"[AttendanceSync] machine_id={machine.MachineId} ip={machine.MachineIp} prev_last_sync={prevLastSync:yyyy-MM-dd HH:mm:ss} new_last_sync={machine.LastSyncDate:yyyy-MM-dd HH:mm:ss} requested_start_time={requestedStartTime} fetched_total={totalFetched} first_checktime={(summary.FirstCheckTime.HasValue ? summary.FirstCheckTime.Value.ToString("yyyy-MM-dd HH:mm:ss") : "NULL")} last_checktime={(summary.LastCheckTime.HasValue ? summary.LastCheckTime.Value.ToString("yyyy-MM-dd HH:mm:ss") : "NULL")}" ); } return response; } public static string getEmployeeInfo(int employeeId) { string response = ""; CallBack myCallBack = new CallBack(Program.BeCalled); Initialize(); if (OpenConnection() == true) { AttendanceMachineDAO attendanceMachineDAO = new AttendanceMachineDAO(); List machines = attendanceMachineDAO.getAttendanceMachines(connection, "2"); foreach (var machine in machines) { if (machine.MachineId.Equals("SITE2-MACHINE-9")) { string devInfo = machine.GetDeviceInfo(); string str4 = "GetEmployee(id=\"" + employeeId + "\")"; string str5 = ""; uint num2 = 0; if (test(devInfo, devInfo.Length, str4, str4.Length, ref str5, ref num2, myCallBack) != 0) { Console.WriteLine("Not Connected"); response = machine.MachineId + " is not connected !"; RecordUnreachableMachine(machine, "getEmployeeInfo: device not connected"); } else { Console.WriteLine("Connected"); Console.WriteLine(str5); Console.WriteLine("Process Completed"); } } } CloseConnection(); } return response; } public static List syncMachineUsers() { List responses = new List(); CallBack myCallBack = new CallBack(Program.BeCalled); Initialize(); using (var conn = OpenDbConnectionWithRetry("syncMachineUsers")) { AttendanceMachineUserDAO attendanceMachineUsersDAO = new AttendanceMachineUserDAO(); List DBusers = null; try { DBusers = attendanceMachineUsersDAO.getActiveEmployees(conn); } catch (Exception ex) { WriteInternalLog("syncMachineUsers: getAllEmployees failed (attempt 1): " + ex.Message); try { Thread.Sleep(2000); Initialize(); using (var retryConn = OpenDbConnectionWithRetry("syncMachineUsers-retry")) { DBusers = attendanceMachineUsersDAO.getActiveEmployees(retryConn); } WriteInternalLog("syncMachineUsers: getAllEmployees succeeded on retry (attempt 2)."); } catch (Exception ex2) { WriteInternalLog("syncMachineUsers: getAllEmployees failed (attempt 2): " + ex2.Message); throw; } } AttendanceMachineDAO attendanceMachineDAO = new AttendanceMachineDAO(); var machines = GetMachinesForAttendanceAndUsers(attendanceMachineDAO, conn, responses); foreach (var machine in machines) { if (DeviceProtocol.UseHttp()) { syncMachineUsersHttp(machine, conn, attendanceMachineDAO, attendanceMachineUsersDAO, DBusers, responses); } else { syncMachineUsersHdcp(machine, conn, attendanceMachineDAO, attendanceMachineUsersDAO, DBusers, responses, myCallBack); } } Console.WriteLine("COMPLETED !"); } return responses; } private static void syncMachineUsersHttp( AttendanceMachine machine, MySqlConnection conn, AttendanceMachineDAO attendanceMachineDAO, AttendanceMachineUserDAO attendanceMachineUsersDAO, List DBusers, List responses) { var client = HanvonHttpApiClient.ForMachine(machine); WriteInternalLog($"[MachineUserConnect] protocol=HTTP machine_id={machine.MachineId} ip={machine.MachineIp}"); if (!client.TryLogin(out string loginErr)) { responses.Add(machine.MachineId + " is not connected !"); WriteInternalLog($"[MachineUserConnectFail] protocol=HTTP machine_id={machine.MachineId} ip={machine.MachineIp} err={loginErr}"); RecordUnreachableMachine(machine, "syncMachineUsers HTTP: " + loginErr); return; } var users = client.GetUserList(out string listErr); if (!string.IsNullOrEmpty(listErr) && (users == null || users.Count == 0)) { responses.Add(machine.MachineId + " is not connected !"); WriteInternalLog($"[MachineUserConnectFail] protocol=HTTP getuserlist machine_id={machine.MachineId} err={listErr}"); RecordUnreachableMachine(machine, "syncMachineUsers HTTP getuserlist: " + listErr); return; } int totalEmp = users?.Count ?? 0; attendanceMachineDAO.UpdateTotalEmpInMachines(machine.MachineId, totalEmp, conn); responses.Add(machine.MachineId + " -> " + totalEmp); WriteInternalLog($"[MachineUserSync] protocol=HTTP machine_id={machine.MachineId} ip={machine.MachineIp} total={totalEmp}"); try { var toDelete = attendanceMachineUsersDAO.GetDeletionRequestedSerials(conn, machine.MachineId); if (toDelete.Count > 0) { // Map serials to device enroll ids when possible. var deleteIds = new List(); foreach (var empId in toDelete) { if (string.IsNullOrWhiteSpace(empId)) continue; var match = users.FirstOrDefault(u => string.Equals(u.SerialNumber, empId.Trim(), StringComparison.OrdinalIgnoreCase) || string.Equals(u.DeviceUserId, empId.Trim(), StringComparison.OrdinalIgnoreCase) || string.Equals(u.Name, empId.Trim(), StringComparison.OrdinalIgnoreCase)); deleteIds.Add(match != null ? match.DeviceUserId : empId.Trim()); } if (client.DeleteUsers(deleteIds, out string delErr)) { foreach (var empId in toDelete) { attendanceMachineUsersDAO.MarkUserDeleted(conn, machine.MachineId, empId); string line = empId + " removed from " + machine.MachineId; responses.Add(line); WriteInternalLog(line); } } else { WriteInternalLog("syncMachineUsers HTTP deleteusers failed: " + delErr); } } } catch (Exception exDel) { WriteInternalLog("syncMachineUsers HTTP: deletion-request handling failed: " + exDel); } foreach (var u in users) { bool exists = false; foreach (var dbUser in DBusers) { if (dbUser.SerialNumber != null && dbUser.MachineId != null && dbUser.SerialNumber.Contains(u.SerialNumber) && dbUser.MachineId.Contains(machine.MachineId)) { exists = true; break; } } if (!exists) { attendanceMachineUsersDAO.Add(new AttendanceMachineUser(machine.MachineId, u.SerialNumber, u.Name ?? ""), conn); } } } private static void syncMachineUsersHdcp( AttendanceMachine machine, MySqlConnection conn, AttendanceMachineDAO attendanceMachineDAO, AttendanceMachineUserDAO attendanceMachineUsersDAO, List DBusers, List responses, CallBack myCallBack) { string devInfo = machine.GetDeviceInfo(); string str4 = "GetEmployeeID()"; string str5 = ""; uint num2 = 0; WriteInternalLog($"[MachineUserConnect] protocol=HDCP machine_id={machine.MachineId} ip={machine.MachineIp} port={machine.PortNumber} cmd={str4}"); int userRc = test(devInfo, devInfo.Length, str4, str4.Length, ref str5, ref num2, myCallBack); if (userRc != 0) { Console.WriteLine("Not Connected"); responses.Add(machine.MachineId + " is not connected !"); WriteInternalLog($"[MachineUserConnectFail] machine_id={machine.MachineId} ip={machine.MachineIp} port={machine.PortNumber} rc={userRc}"); RecordUnreachableMachine(machine, $"syncMachineUsers: device not connected rc={userRc} port={machine.PortNumber}"); return; } Console.WriteLine("Connected"); int TotalEmp = GetLoopCnt(str5); attendanceMachineDAO.UpdateTotalEmpInMachines(machine.MachineId, TotalEmp, conn); responses.Add(machine.MachineId + " -> " + TotalEmp); Console.WriteLine(machine.MachineId + " -> " + TotalEmp); try { var toDelete = attendanceMachineUsersDAO.GetDeletionRequestedSerials(conn, machine.MachineId); foreach (var empId in toDelete) { if (string.IsNullOrWhiteSpace(empId)) continue; string delCmd = "DeleteEmployee(id=\"" + empId + "\")"; string delResp = ""; uint delRecv = 0; if (test(devInfo, devInfo.Length, delCmd, delCmd.Length, ref delResp, ref delRecv, myCallBack) == 0) { attendanceMachineUsersDAO.MarkUserDeleted(conn, machine.MachineId, empId); string line = empId + " removed from " + machine.MachineId; responses.Add(line); WriteInternalLog(line); } } } catch (Exception exDel) { WriteInternalLog("syncMachineUsers: deletion-request handling failed: " + exDel.ToString()); } saveMachineUsers(str5, machine, DBusers, conn); } internal static List GetMachinesForAttendanceAndUsers(AttendanceMachineDAO attendanceMachineDAO, MySqlConnection connection, List logs) { if (!TryLoadDeviceSettings(out var settings, logs)) { logs?.Add("DeviceSettings could not be loaded; no machines selected."); return new List(); } var attendanceScope = ConfigurationManager.AppSettings["ATTENDANCE_SCOPE"] ?? "CENTRAL"; if (!string.Equals(attendanceScope, "SITE", StringComparison.OrdinalIgnoreCase)) { // Centralized mode: process all DB machines (no DeviceSettings filtering). var allCentral = attendanceMachineDAO.getAllAttendanceMachines(connection); logs?.Add($"Attendance scope=CENTRAL. Hanvon DB machines fetched={allCentral.Count}"); return allCentral; } // SITE / local mode: fetch machines from DB scoped by config SiteId (like the old SITE_ID behavior), // then optionally apply DeviceSettings IP filtering within that site scope. List dbMachines; if (settings.SiteId.HasValue) { dbMachines = attendanceMachineDAO.getAttendanceMachines(connection, settings.SiteId.Value.ToString()); logs?.Add($"DeviceSettings SiteId={settings.SiteId.Value}. DB machines fetched={dbMachines.Count}"); } else { dbMachines = attendanceMachineDAO.getAllAttendanceMachines(connection); logs?.Add($"DeviceSettings SiteId not set. DB machines fetched={dbMachines.Count}"); } if (dbMachines == null) dbMachines = new List(); // Compare config IPs only against machines of this site scope. string NormalizeIp(string ip) { if (string.IsNullOrWhiteSpace(ip)) return ""; return new string(ip.Trim().Where(c => !char.IsWhiteSpace(c)).ToArray()); } var dbIpSet = new HashSet(dbMachines.Select(m => NormalizeIp(m.MachineIp)), StringComparer.OrdinalIgnoreCase); var matchingRules = settings.Devices .Where(d => dbIpSet.Contains(NormalizeIp(d.IpAddress))) .ToList(); // If ANY matching IP is Enabled="true" => allowlist within this site scope. if (matchingRules.Any(r => r.Enabled)) { var allow = new HashSet(matchingRules.Where(r => r.Enabled).Select(r => NormalizeIp(r.IpAddress)), StringComparer.OrdinalIgnoreCase); var selected = dbMachines.Where(m => allow.Contains(NormalizeIp(m.MachineIp))).ToList(); logs?.Add($"DeviceSettings allowlist active (within site scope). selected={selected.Count}"); return selected; } // Else if matching rules exist and some are Enabled="false" => blocklist within site scope. if (matchingRules.Count > 0) { var block = new HashSet(matchingRules.Where(r => !r.Enabled).Select(r => NormalizeIp(r.IpAddress)), StringComparer.OrdinalIgnoreCase); var filtered = dbMachines.Where(m => !block.Contains(NormalizeIp(m.MachineIp))).ToList(); logs?.Add($"DeviceSettings blocklist active (within site scope). machines={dbMachines.Count} filtered={filtered.Count}"); return filtered; } // If no rules match any DB machine in site scope (or no devices listed) => sync all DB machines in scope. if (settings.Devices.Count > 0) { logs?.Add("DeviceSettings has entries but none match DB machines in scope. Syncing all DB machines in scope."); } else { logs?.Add("DeviceSettings has no entries. Syncing all DB machines in scope."); } return dbMachines; } public static List syncTemplates() { var logs = new List(); if (!GetAppSettingBool("TEMPLATE_SYNC_ENABLED", false)) { return logs; } var modeStr = ConfigurationManager.AppSettings["TRANSFER_MODE"] ?? "DEVICE_TO_DEVICE"; if (!Enum.TryParse(modeStr, true, out TemplateTransferMode mode)) { logs.Add("Invalid TRANSFER_MODE. Expected DEVICE_TO_DB, DB_TO_DEVICE, or DEVICE_TO_DEVICE."); return logs; } if (!TryLoadDeviceSettings(out var settings, logs)) { return logs; } try { Initialize(); using (var templateConn = OpenDbConnectionWithRetry("syncTemplates")) { var runner = new TemplateTransferRunner(new AttendanceMachineDAO(), new AttendanceMachineFaceTemplateDAO()); runner.RunJobs(settings, mode, templateConn, logs); } } catch (Exception ex) { logs.Add("Template sync failed: " + ex.ToString()); } return logs; } // ApplyDeviceSelectionRulesIfConfigured removed: selection is now config-driven (IP allowlist/blocklist) without SITE_ID env var. private static bool TryLoadDeviceSettings(out DeviceSettingsConfig settings, List logs) { settings = null; string path = ConfigurationManager.AppSettings["DEVICE_SETTINGS_PATH"]; if (string.IsNullOrWhiteSpace(path)) { path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "DeviceSettings.xml"); } if (!DeviceSettingsConfig.TryLoad(path, out settings, out var error)) { logs?.Add(error); return false; } return true; } private static bool GetAppSettingBool(string key, bool defaultValue) { try { var value = ConfigurationManager.AppSettings[key]; if (string.IsNullOrWhiteSpace(value)) { return defaultValue; } if (bool.TryParse(value.Trim(), out var parsed)) { return parsed; } return defaultValue; } catch { return defaultValue; } } public static string deleteEmployeeInfo(int employeeId) { string response = ""; CallBack myCallBack = new CallBack(Program.BeCalled); Initialize(); if (OpenConnection() == true) { AttendanceMachineDAO attendanceMachineDAO = new AttendanceMachineDAO(); List machines = attendanceMachineDAO.getAttendanceMachines(connection, "2"); foreach (var machine in machines) { if (machine.MachineId.Equals("SITE2-MACHINE-2")) { string devInfo = machine.GetDeviceInfo(); string str4 = "DeleteEmployee(id=\"" + employeeId + "\")"; //string str4 = "GetEmployeeID()"; string str5 = ""; uint num2 = 0; if (test(devInfo, devInfo.Length, str4, str4.Length, ref str5, ref num2, myCallBack) != 0) { Console.WriteLine("Not Connected"); response = machine.MachineId + " is not connected !"; RecordUnreachableMachine(machine, "deleteEmployeeInfo: device not connected"); } else { Console.WriteLine("Connected"); Console.WriteLine(str5); } } } CloseConnection(); } return response; } public static int GetLoopCnt(string retstr) { if (string.IsNullOrEmpty(retstr)) return 0; int index = retstr.IndexOf("total=\"", StringComparison.OrdinalIgnoreCase); if (index < 0) return 0; int start = index + 7; if (start >= retstr.Length) return 0; int end = retstr.IndexOf("\"", start, StringComparison.Ordinal); if (end < 0) return 0; string numStr = retstr.Substring(start, end - start).Trim(); if (string.IsNullOrEmpty(numStr)) return 0; return int.TryParse(numStr, out int n) ? n : 0; } private static int GetAttendanceBatchSize(int totalRecords) { if (totalRecords <= 50) return 10; if (totalRecords <= 200) return 25; if (totalRecords <= 500) return 50; if (totalRecords <= 2000) return 100; return 200; } private static List ParseAttendanceRows(string response, AttendanceMachine machine) { var rows = new List(); if (string.IsNullOrWhiteSpace(response)) return rows; string[] allObj = response.Split(new string[] { "time" }, StringSplitOptions.None); for (int i = 0; i < allObj.Length; i++) { string s = allObj[i]; if (!s.Contains("Return")) { string[] objDetail = s.Split(new string[] { "\"" }, StringSplitOptions.None); if (objDetail.Length < 4) continue; DateTime checkTime = Convert.ToDateTime(objDetail[1]); rows.Add(new Attendance(objDetail[3], checkTime, false, machine.MachineId, "1", machine.MachineIp, DateTime.Now)); } } return rows; } private static (DateTime? FirstCheckTime, DateTime? LastCheckTime, int InsertedCount) ProcessAttendanceInBatches( List parsedRows, AttendanceMachine machine, MySqlConnection conn, DateTime prevLastSync) { var attendanceDAO = new AttendanceDAO(); var attendanceMachineDAO = new AttendanceMachineDAO(); int total = parsedRows?.Count ?? 0; if (total == 0) return (null, null, 0); int batchSize = GetAttendanceBatchSize(total); int totalBatches = (int)Math.Ceiling(total / (double)batchSize); WriteInternalLog($"[AttendanceBatchPlan] machine_id={machine.MachineId} ip={machine.MachineIp} total_records={total} batch_size={batchSize} total_batches={totalBatches}"); int inserted = 0; DateTime? firstCheckTime = null; DateTime? lastCheckTime = null; DateTime runningPrevSync = prevLastSync; for (int b = 0; b < totalBatches; b++) { var sw = Stopwatch.StartNew(); int start = b * batchSize; int take = Math.Min(batchSize, total - start); DateTime? batchFirst = null; DateTime? batchLast = null; for (int i = start; i < start + take; i++) { var log = parsedRows[i]; if (!firstCheckTime.HasValue || log.CheckTime < firstCheckTime.Value) firstCheckTime = log.CheckTime; if (!lastCheckTime.HasValue || log.CheckTime > lastCheckTime.Value) lastCheckTime = log.CheckTime; if (!batchFirst.HasValue || log.CheckTime < batchFirst.Value) batchFirst = log.CheckTime; if (!batchLast.HasValue || log.CheckTime > batchLast.Value) batchLast = log.CheckTime; try { int rows = attendanceDAO.Add(log, conn); WriteInternalLog($"[AttendanceInsert] machine_id={machine.MachineId} ip={machine.MachineIp} ac_no={log.AcNo} checktime={log.CheckTime:yyyy-MM-dd HH:mm:ss} rows={rows}"); inserted++; } catch (Exception ex) { WriteInternalLog($"[AttendanceInsertError] machine_id={machine.MachineId} ip={machine.MachineIp} ac_no={log.AcNo} checktime={log.CheckTime:yyyy-MM-dd HH:mm:ss} err={ex.Message}"); throw; } } if (!DeviceProtocol.UseHttp() && GetBoolAppSetting("ATTENDANCE_DELETE_AFTER_INSERT", true) && batchFirst.HasValue && batchLast.HasValue) { TryDeleteAttendanceFromDevice(machine, batchFirst.Value, batchLast.Value, b + 1, totalBatches); } sw.Stop(); WriteInternalLog($"[AttendanceBatch] machine_id={machine.MachineId} ip={machine.MachineIp} batch={b + 1}/{totalBatches} attempted={take} duration_sec={Math.Round(sw.Elapsed.TotalSeconds, 1)}"); if (batchLast.HasValue) { WriteInternalLog($"[AttendanceSyncProgress] machine_id={machine.MachineId} ip={machine.MachineIp} latest_processed_checktime={batchLast.Value:yyyy-MM-dd HH:mm:ss}"); machine.LastSyncDate = batchLast.Value; attendanceMachineDAO.update(machine, conn); WriteInternalLog($"[AttendanceSyncUpdate] machine_id={machine.MachineId} ip={machine.MachineIp} prev_last_sync={runningPrevSync:yyyy-MM-dd HH:mm:ss} new_last_sync={machine.LastSyncDate:yyyy-MM-dd HH:mm:ss}"); runningPrevSync = machine.LastSyncDate; } } return (firstCheckTime, lastCheckTime, inserted); } private static void TryDeleteAttendanceFromDevice(AttendanceMachine machine, DateTime batchFirst, DateTime batchLast, int batchNumber, int totalBatches) { try { var callback = new CallBack(Program.BeCalled); string devInfo = machine.GetDeviceInfo(); string start = batchFirst.ToString("yyyy-MM-dd HH:mm:ss"); string end = batchLast.ToString("yyyy-MM-dd HH:mm:ss"); string cmd = "DeleteRecord(start_time=\"" + start + "\" end_time=\"" + end + "\")"; string response = ""; uint recvLen = 0; int rc = test(devInfo, devInfo.Length, cmd, cmd.Length, ref response, ref recvLen, callback); if (rc == 0) { WriteInternalLog($"[AttendanceDelete] machine_id={machine.MachineId} ip={machine.MachineIp} batch={batchNumber}/{totalBatches} start={start} end={end} status=OK"); } else { WriteInternalLog($"[AttendanceDelete] machine_id={machine.MachineId} ip={machine.MachineIp} batch={batchNumber}/{totalBatches} start={start} end={end} status=FAIL rc={rc}"); } } catch (Exception ex) { WriteInternalLog($"[AttendanceDeleteError] machine_id={machine.MachineId} ip={machine.MachineIp} batch={batchNumber}/{totalBatches} err={ex.Message}"); } } public static void saveMachineUsers(string response, AttendanceMachine machine, List DBusers, MySqlConnection conn) { AttendanceMachineUserDAO attendanceMachineUserDAO = new AttendanceMachineUserDAO(); string[] allObj = response.Split(new string[] { "id" }, StringSplitOptions.None); int count = 0; for (int i = 0; i < allObj.Length; i++) { string s = allObj[i]; if (!s.Contains("Return")) { //Console.WriteLine(s); string[] objDetail = s.Split(new string[] { "\"" }, StringSplitOptions.None); /*Console.WriteLine("id: " + objDetail[1]); Console.WriteLine("time: " + objDetail[1]); Console.WriteLine("id: " + objDetail[3]); Console.WriteLine("name: " + objDetail[5]); Console.WriteLine("workCode: " + objDetail[7]); Console.WriteLine("status: " + objDetail[9]); Console.WriteLine("authority: " + objDetail[11]); Console.WriteLine("cardSrc: " + objDetail[13]);*/ //objDetail[1] = "6286"; AttendanceMachineUser temp = new AttendanceMachineUser(machine.MachineId, objDetail[1], ""); foreach (var users in DBusers) { result = (users.SerialNumber.Contains(objDetail[1]) && users.MachineId.Contains(temp.MachineId)); if (result == true) { break; } } if (result == false) { attendanceMachineUserDAO.Add(temp, conn); } //Attendance log = new Attendance(objDetail[3], Convert.ToDateTime(objDetail[1]), false, machine.MachineId, "1", machine.MachineIp, Convert.ToDateTime(objDetail[1])); //attendanceDAO.Add(log, connection); count++; } } //dbConnect.CloseConnection(); //Console.WriteLine(count); } public static int test(string pDevInfoBuf, int nDevInfoLen, string pSendBuf, int nSendLen, ref string pRecvBuf, ref uint pRecvLen, CallBack pFuncTotalDone) { byte[] bytes; int num = 0; IntPtr zero = IntPtr.Zero; IntPtr ptr2 = IntPtr.Zero; try { bytes = Encoding.Default.GetBytes(pSendBuf); int length = bytes.Length; zero = Program.GlobalAlloc(0, length); if (!(zero == IntPtr.Zero)) { zero = Program.GlobalLock(zero); if (!(zero == IntPtr.Zero)) { Marshal.Copy(bytes, 0, zero, length); num = Program.HwDev_Execute(pDevInfoBuf, pDevInfoBuf.Length, zero, length, ref ptr2, ref pRecvLen, pFuncTotalDone); pRecvBuf = Marshal.PtrToStringAnsi(ptr2); Program.HwDev_Finish(ref ptr2); } else { return -1; } } else { return -1; } } catch (Exception exception1) { num = -1; Console.Write(exception1.ToString()); throw new OperationCanceledException(exception1.Message); } finally { Program.GlobalUnlock(zero); Program.GlobalFree(zero); bytes = null; } return num; } public static int BeCalled(uint nTotal, uint nDone) => 0; public static bool OpenConnection() { try { connection.Open(); Console.WriteLine("Connected !"); return true; } catch (MySqlException ex) { switch (ex.Number) { case 0: Console.WriteLine("Cannot connect to server. Contact administrator"); break; case 1045: Console.WriteLine("Invalid username/password, please try again"); break; } return false; } } //Close connection public static bool CloseConnection() { try { connection.Close(); return true; } catch (MySqlException ex) { Console.WriteLine(ex.Message); return false; } } } public delegate int CallBack(uint nTotal, uint nDone); }