using MySql.Data.MySqlClient; using System; using System.Collections.Generic; using System.Configuration; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading; namespace HanvonF710XAttendanceService { internal enum TemplateTransferMode { DEVICE_TO_DB, DB_TO_DEVICE, DEVICE_TO_DEVICE } internal sealed class TemplateTransferRunner { private readonly AttendanceMachineDAO _machineDao; private readonly AttendanceMachineFaceTemplateDAO _templateDao; public TemplateTransferRunner(AttendanceMachineDAO machineDao, AttendanceMachineFaceTemplateDAO templateDao) { _machineDao = machineDao ?? throw new ArgumentNullException(nameof(machineDao)); _templateDao = templateDao ?? throw new ArgumentNullException(nameof(templateDao)); } public void RunJobs(DeviceSettingsConfig settings, TemplateTransferMode mode, MySqlConnection connection, List logs) { if (mode == TemplateTransferMode.DEVICE_TO_DB) { // TEMPLATE SAVE (DEVICE_TO_DB) now follows the same scope model // as attendance and machine user sync (CENTRAL vs SITE). var machinesInScope = Program.GetMachinesForAttendanceAndUsers(_machineDao, connection, logs); var scope = (ConfigurationManager.AppSettings["ATTENDANCE_SCOPE"] ?? "CENTRAL").ToUpperInvariant(); logs.Add($"Template DEVICE_TO_DB scope={scope}. machines_in_scope={machinesInScope.Count}"); bool autoDistributeAfterSave = GetBoolAppSetting("TEMPLATE_AUTO_DISTRIBUTE_AFTER_DEVICE_TO_DB", true); if (machinesInScope.Count == 0) { logs.Add("Template DEVICE_TO_DB: no machines found in scope."); return; } foreach (var m in machinesInScope) { if (!_machineDao.TryGetAttendanceMachineByIp(connection, m.MachineIp, out var sourceMachine, out var sourceType)) { logs.Add($"[TemplateSave] source machine not found in DB. machine_id={m.MachineId} ip={m.MachineIp}"); continue; } if (!MachineScope.IsHanvonMachineType(sourceType)) { logs.Add($"[TemplateSave] source machine_type={sourceType} not supported for Hanvon F710X service. machine_id={sourceMachine.MachineId} ip={sourceMachine.MachineIp}. Skipping."); continue; } logs.Add($"[TemplateSave] DEVICE_TO_DB start. machine_id={sourceMachine.MachineId} ip={sourceMachine.MachineIp}"); bool saveOk = SaveTemplateDbFromDevice(sourceMachine, connection, logs, $"TemplateSave:{sourceMachine.MachineIp}"); if (saveOk && autoDistributeAfterSave) { DistributeMissingTemplatesAfterDeviceToDb(sourceMachine, machinesInScope, logs); } else if (!saveOk) { LogDistributionLine($"[TemplateDistribution] skipped for source_machine={sourceMachine.MachineId} ip={sourceMachine.MachineIp} (DEVICE_TO_DB did not complete; device unreachable or no employee list).", logs); } } return; } if (settings?.TemplateTransfer == null || !settings.TemplateTransfer.Enabled) { logs.Add("TemplateTransfer is disabled in DeviceSettings."); return; } var enabledJobs = settings.TemplateTransfer.Jobs.Where(j => j.Enabled).ToList(); if (enabledJobs.Count == 0) { logs.Add("No enabled TemplateTransfer jobs found."); return; } foreach (var job in enabledJobs) { logs.Add($"[TemplateJob {job.Id}] Mode={mode} SourceIp={job.SourceIp} Targets={job.TargetIps.Count}"); if (!_machineDao.TryGetAttendanceMachineByIp(connection, job.SourceIp, out var sourceMachine, out var sourceType)) { logs.Add($"[TemplateJob {job.Id}] Source machine not found in DB. ip={job.SourceIp}"); continue; } if (!MachineScope.IsHanvonMachineType(sourceType)) { logs.Add($"[TemplateJob {job.Id}] Source machine_type={sourceType} is not a Hanvon F710X device. Skipping."); continue; } var targetMachines = new List<(AttendanceMachine Machine, string Type)>(); foreach (var targetIp in job.TargetIps.Distinct(StringComparer.OrdinalIgnoreCase)) { if (!_machineDao.TryGetAttendanceMachineByIp(connection, targetIp, out var targetMachine, out var targetType)) { logs.Add($"[TemplateJob {job.Id}] Target machine not found in DB. ip={targetIp}"); continue; } if (!MachineScope.IsHanvonMachineType(targetType)) { logs.Add($"[TemplateJob {job.Id}] Target ip={targetIp} machine_type={targetType} is not a Hanvon F710X device. Skipping this target."); continue; } targetMachines.Add((targetMachine, targetType)); } if (targetMachines.Count == 0) { logs.Add($"[TemplateJob {job.Id}] No valid targets to transfer to."); continue; } if (!TryGetEmployeeIdsFromDevice(sourceMachine, out var employeeIds, logs, job.Id)) { continue; } if (employeeIds.Count == 0) { logs.Add($"[TemplateJob {job.Id}] Source device returned 0 employee IDs."); continue; } if (mode == TemplateTransferMode.DB_TO_DEVICE) { TransferDbToDevice(job, employeeIds, targetMachines, connection, logs); } else if (mode == TemplateTransferMode.DEVICE_TO_DEVICE) { TransferDeviceToDevice(job.Id, sourceMachine, employeeIds, targetMachines, logs); } } } private bool TryGetEmployeeIdsFromDevice(AttendanceMachine machine, out List employeeIds, List logs, string jobId) { employeeIds = new List(); if (DeviceProtocol.UseHttp()) { var client = HanvonHttpApiClient.ForMachine(machine); if (!client.TryLogin(out string loginErr)) { logs.Add($"[TemplateJob {jobId}] HTTP login failed. source={machine.MachineIp} err={loginErr}"); Program.RecordUnreachableMachine(machine, "TemplateSave HTTP: " + loginErr); return false; } var users = client.GetUserList(out string listErr); if (!string.IsNullOrEmpty(listErr) && (users == null || users.Count == 0)) { logs.Add($"[TemplateJob {jobId}] getuserlist failed. source={machine.MachineIp} err={listErr}"); Program.RecordUnreachableMachine(machine, "TemplateSave HTTP getuserlist: " + listErr); return false; } foreach (var u in users) { // Prefer device enroll id for getuserinfo; also keep serial for DB key. if (!string.IsNullOrWhiteSpace(u.DeviceUserId)) employeeIds.Add(u.DeviceUserId); else if (!string.IsNullOrWhiteSpace(u.SerialNumber)) employeeIds.Add(u.SerialNumber); } logs.Add($"[TemplateJob {jobId}] HTTP getuserlist ok. parsed={employeeIds.Count}"); return true; } var callback = new CallBack(Program.BeCalled); string devInfo = machine.GetDeviceInfo(); string cmd = "GetEmployeeID()"; string response = ""; uint recvLen = 0; if (Program.test(devInfo, devInfo.Length, cmd, cmd.Length, ref response, ref recvLen, callback) != 0) { logs.Add($"[TemplateJob {jobId}] GetEmployeeID() failed. source={machine.MachineIp}"); Program.RecordUnreachableMachine(machine, "TemplateSave: device not connected"); return false; } employeeIds = TemplateParser.ParseEmployeeIdsFromGetEmployeeIdResponse(response); try { logs.Add($"[TemplateJob {jobId}] GetEmployeeID() ok. total={Program.GetLoopCnt(response)} parsed={employeeIds.Count}"); } catch { logs.Add($"[TemplateJob {jobId}] GetEmployeeID() ok. parsed={employeeIds.Count}"); } return true; } private bool SaveTemplateDbFromDevice(AttendanceMachine sourceMachine, MySqlConnection connection, List logs, string jobId) { var swTotal = Stopwatch.StartNew(); if (!TryGetEmployeeIdsFromDevice(sourceMachine, out var employeeIds, logs, jobId)) { return false; } int inserted = 0; int skippedExisting = 0; int failed = 0; bool capturedRaw = false; int totalIds = employeeIds.Count; int batchSize = ChooseBatchSize(totalIds); var batches = SplitIntoBatches(employeeIds, batchSize).ToList(); logs.Add($"[TemplateSave] machine_id={sourceMachine.MachineId} ip={sourceMachine.MachineIp} total_ids={totalIds} batch_size={batchSize} total_batches={batches.Count}"); for (int bi = 0; bi < batches.Count; bi++) { var swBatch = Stopwatch.StartNew(); int attempted = 0; int bInserted = 0; int bExisting = 0; int bFailed = 0; foreach (var empId in batches[bi]) { attempted++; try { if (_templateDao.ExistsBySerialNo(connection, empId)) { skippedExisting++; bExisting++; // Simple log line for SaveFaceTemplate logs (keep deep logs in InternalLogs as-is). logs.Add($"Serial number-- {empId} already exists in the database"); continue; } if (DeviceProtocol.UseHttp()) { if (!int.TryParse(empId, out int enrollId)) { failed++; bFailed++; logs.Add($"[TemplateJob {jobId}] invalid enroll id for HTTP. emp={empId}"); continue; } var httpClient = HanvonHttpApiClient.ForMachine(sourceMachine); if (!httpClient.TryGetFaceRecord(enrollId, out string recordB64, out string empName, out string faceErr)) { failed++; bFailed++; logs.Add($"[TemplateJob {jobId}] HTTP getuserinfo failed. emp={empId} err={faceErr}"); continue; } // Store under employee name/serial when available. string serialKey = !string.IsNullOrWhiteSpace(empName) ? empName.Trim() : empId; if (_templateDao.ExistsBySerialNo(connection, serialKey)) { skippedExisting++; bExisting++; logs.Add($"Serial number-- {serialKey} already exists in the database"); continue; } var faceBytes = Encoding.UTF8.GetBytes(recordB64); int faceRows = _templateDao.InsertTemplate(connection, serialKey, faceBytes, DateTime.Now); if (faceRows > 0) { inserted++; bInserted++; } else { failed++; bFailed++; } continue; } var callback = new CallBack(Program.BeCalled); string devInfo = sourceMachine.GetDeviceInfo(); string cmd = "GetEmployee(id=\"" + empId + "\")"; string response = ""; uint recvLen = 0; if (Program.test(devInfo, devInfo.Length, cmd, cmd.Length, ref response, ref recvLen, callback) != 0) { failed++; bFailed++; logs.Add($"[TemplateJob {jobId}] GetEmployee failed. emp={empId} source={sourceMachine.MachineIp}"); continue; } if (!capturedRaw && response != null && response.IndexOf("success", StringComparison.OrdinalIgnoreCase) >= 0) { capturedRaw = true; WriteRawTemplateLog($"[TemplateJob {jobId}] GetEmployee raw response emp={empId}", response); } if (response == null || response.IndexOf("success", StringComparison.OrdinalIgnoreCase) < 0) { failed++; bFailed++; logs.Add($"[TemplateJob {jobId}] GetEmployee returned non-success. emp={empId} source={sourceMachine.MachineIp}"); continue; } if (!TemplateParser.TryExtractTemplatePayloadFromGetEmployeeResponse(response, out var payload)) { failed++; bFailed++; logs.Add($"[TemplateJob {jobId}] Failed to parse template payload. emp={empId}"); continue; } var bytes = Encoding.UTF8.GetBytes(payload); int rows = _templateDao.InsertTemplate(connection, empId, bytes, DateTime.Now); if (rows > 0) { inserted++; bInserted++; } else { failed++; bFailed++; } } catch (Exception ex) { failed++; bFailed++; logs.Add($"[TemplateJob {jobId}] SaveTemplateDB error. emp={empId} err={ex.Message}"); } } swBatch.Stop(); logs.Add($"[TemplateSaveBatch] machine_id={sourceMachine.MachineId} ip={sourceMachine.MachineIp} batch={bi + 1}/{batches.Count} attempted={attempted} inserted={bInserted} existing={bExisting} failed={bFailed} duration_sec={Math.Round(swBatch.Elapsed.TotalSeconds, 1)}"); } swTotal.Stop(); logs.Add($"[TemplateSave] machine_id={sourceMachine.MachineId} ip={sourceMachine.MachineIp} completed inserted={inserted} existing={skippedExisting} failed={failed} duration_sec={Math.Round(swTotal.Elapsed.TotalSeconds, 1)}"); logs.Add($"[TemplateJob {jobId}] SaveTemplateDB done. inserted={inserted} existing={skippedExisting} failed={failed}"); return true; } /// /// After DEVICE_TO_DB, distribute templates to peer Nedo machines using registration-based target selection: /// only push an employee template to machines where that employee is registered (attendance_machine_user, is_deleted=0). /// Central truth is still DB templates; device lists are only used to skip already-present templates. /// Uses a dedicated DB connection for query + template fetch (no shared connection with attendance job). /// private void DistributeMissingTemplatesAfterDeviceToDb(AttendanceMachine sourceMachine, List machinesInScope, List logs) { if (sourceMachine == null || machinesInScope == null || machinesInScope.Count == 0) { return; } var peers = machinesInScope.Where(m => !IsSameScopedMachine(m, sourceMachine)).ToList(); if (peers.Count == 0) { LogDistributionLine($"[TemplateDistributionPlan] source_machine={sourceMachine.MachineId} ip={sourceMachine.MachineIp} no peer Hanvon machines in scope; skipping.", logs); return; } try { using (var distConn = new MySqlConnection(Program.GetConnectionString())) { distConn.Open(); // Targets to consider are already filtered by ATTENDANCE_SCOPE + DeviceSettings in machinesInScope. // Now apply registration-based selection inside DB queries. var targets = new List(); foreach (var t in peers) { if (t == null) continue; if (string.IsNullOrWhiteSpace(t.MachineId) || string.IsNullOrWhiteSpace(t.MachineIp)) { continue; } targets.Add(t); } if (targets.Count == 0) { LogDistributionLine("[TemplateDistribution] no valid peer Hanvon targets in scope; skipping.", logs); return; } var allowedMachineIds = targets .Select(t => t.MachineId) .Where(id => !string.IsNullOrWhiteSpace(id)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); // 1) For each target machine, get already-present employee ids from the device, // then get DB templates only for employees registered on that specific target. // 2) missing = registeredTemplates - onDeviceIds var toSendByTarget = new Dictionary>(StringComparer.OrdinalIgnoreCase); var missingSerialsUnion = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var targetMachine in targets) { if (!TryGetEmployeeIdsFromDevice(targetMachine, out var onDeviceIds, logs, "TemplateDist:" + targetMachine.MachineIp)) { continue; // unreachable or device communication error already logged } var onDevice = new HashSet(onDeviceIds, StringComparer.OrdinalIgnoreCase); // Registration-based: only templates whose employee is registered to THIS target machine. var registeredTemplates = _templateDao.GetActiveTemplatesForRegisteredMachine(distConn, targetMachine.MachineId); var missing = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var kvp in registeredTemplates) { if (!onDevice.Contains(kvp.Key)) { missing[kvp.Key] = kvp.Value; missingSerialsUnion.Add(kvp.Key); } } toSendByTarget[targetMachine.MachineId] = missing; } if (missingSerialsUnion.Count == 0) { LogDistributionLine("[TemplateDistribution] nothing missing on peer Hanvon machines; registration-based distribution skipped.", logs); return; } // Build serial -> list of registered target machine ids (restricted to current scope targets). var serialToRegisteredTargets = _templateDao.GetRegisteredTargetMachineIdsForSerialNos( distConn, missingSerialsUnion.ToList(), allowedMachineIds); // Pre-compute pushed_to / skipped_existing per serial (based on which targets have missing templates). var pushedTargetsBySerial = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var serial in missingSerialsUnion) { pushedTargetsBySerial[serial] = new HashSet(StringComparer.OrdinalIgnoreCase); } foreach (var kvp in toSendByTarget) { var targetMachineId = kvp.Key; var missingTemplates = kvp.Value; if (missingTemplates == null || missingTemplates.Count == 0) continue; foreach (var serial in missingTemplates.Keys) { if (pushedTargetsBySerial.TryGetValue(serial, out var set)) { set.Add(targetMachineId); } } } // Log registration targets + per-serial distribution summary. foreach (var serial in missingSerialsUnion) { int registeredTargets = 0; if (serialToRegisteredTargets.TryGetValue(serial, out var targetList) && targetList != null) { registeredTargets = targetList.Count; } int pushedTo = 0; if (pushedTargetsBySerial.TryGetValue(serial, out var pushedSet) && pushedSet != null) { pushedTo = pushedSet.Count; } int skippedExisting = Math.Max(0, registeredTargets - pushedTo); LogDistributionLine($"[TemplateRegistrationTargets] serial_no={serial} registered_targets={registeredTargets}", logs); LogDistributionLine($"[TemplateDistributionSummary] serial_no={serial} total_targets={registeredTargets} pushed_to={pushedTo} skipped_existing={skippedExisting}", logs); } // 3) Push missing templates in batches per target machine. foreach (var targetMachine in targets) { if (!toSendByTarget.TryGetValue(targetMachine.MachineId, out var missingTemplates) || missingTemplates == null || missingTemplates.Count == 0) { continue; } int batchSize = ChooseBatchSize(missingTemplates.Count); int totalBatches = (missingTemplates.Count + batchSize - 1) / batchSize; LogDistributionLine( $"[TemplateDistributionPlan] target_machine={targetMachine.MachineId} missing_templates={missingTemplates.Count} batch_size={batchSize} total_batches={totalBatches}", logs); foreach (var serial in missingTemplates.Keys) { LogDistributionLine( $"[TemplateDistributionPlan] serial_no={serial} target_machine={targetMachine.MachineId} already_present=false will_push=true", logs); } PushDistributionBatches(targetMachine, missingTemplates, batchSize, logs, out _); } } } catch (Exception ex) { LogDistributionLine("[TemplateDistribution] failed: " + ex.ToString(), logs); } } private static void LogDistributionLine(string message, List logs) { logs?.Add(message); } private static bool IsSameScopedMachine(AttendanceMachine a, AttendanceMachine b) { if (a == null || b == null) return false; if (!string.IsNullOrWhiteSpace(a.MachineId) && !string.IsNullOrWhiteSpace(b.MachineId)) { return string.Equals(a.MachineId.Trim(), b.MachineId.Trim(), StringComparison.OrdinalIgnoreCase); } return string.Equals(a.MachineIp?.Trim(), b.MachineIp?.Trim(), StringComparison.OrdinalIgnoreCase); } /// Send only missing templates; does not issue SetEmployee for serials already on device. private int PushDistributionBatches(AttendanceMachine target, Dictionary templates, int batchSize, List logs, out int failedTotal) { failedTotal = 0; if (templates == null || templates.Count == 0) { return 0; } bool verifyAfterPush = GetBoolAppSetting("TEMPLATE_VERIFY_AFTER_PUSH", true); int delayMs = GetIntAppSetting("TEMPLATE_DEVICE_DELAY_MS", 0); string targetDevInfo = target.GetDeviceInfo(); var callback = new CallBack(Program.BeCalled); var pairs = templates.ToList(); var batches = SplitIntoBatches(pairs, batchSize).ToList(); int sent = 0; for (int bi = 0; bi < batches.Count; bi++) { var swBatch = Stopwatch.StartNew(); int attempted = 0; int success = 0; int failed = 0; foreach (var kvp in batches[bi]) { attempted++; string empId = kvp.Key; string templateStr = Encoding.UTF8.GetString(kvp.Value ?? Array.Empty()); if (!TemplateParser.TryBuildSetEmployeeCommand(templateStr, out var setCmd)) { failed++; failedTotal++; LogDistributionLine($"[TemplateDistribution] target={target.MachineIp} emp={empId} send=FAIL reason=BuildSetEmployee", logs); continue; } string response = ""; uint recvLen = 0; int rc = Program.test(targetDevInfo, targetDevInfo.Length, setCmd, setCmd.Length, ref response, ref recvLen, callback); if (rc != 0 || (response != null && response.IndexOf("fail", StringComparison.OrdinalIgnoreCase) >= 0)) { failed++; failedTotal++; LogDistributionLine($"[TemplateDistribution] target={target.MachineIp} emp={empId} send=FAIL rc={rc}", logs); continue; } success++; sent++; if (verifyAfterPush) { string verifyCmd = "GetEmployee(id=\"" + empId + "\")"; string verifyResp = ""; uint verifyRecvLen = 0; int vrc = Program.test(targetDevInfo, targetDevInfo.Length, verifyCmd, verifyCmd.Length, ref verifyResp, ref verifyRecvLen, callback); bool ok = vrc == 0 && verifyResp != null && verifyResp.IndexOf("success", StringComparison.OrdinalIgnoreCase) >= 0; LogDistributionLine(ok ? $"[TemplateDistribution] target={target.MachineIp} emp={empId} verify=OK" : $"[TemplateDistribution] target={target.MachineIp} emp={empId} verify=FAIL rc={vrc}", logs); } } if (delayMs > 0) { Thread.Sleep(delayMs); } swBatch.Stop(); LogDistributionLine( $"[TemplateDistributionBatch] target_machine={target.MachineId} ip={target.MachineIp} batch={bi + 1}/{batches.Count} attempted={attempted} success={success} failed={failed} duration_sec={Math.Round(swBatch.Elapsed.TotalSeconds, 1)}", logs); } return sent; } private static int ChooseBatchSize(int total) { if (total <= 0) return 1; if (total <= 20) return 5; if (total <= 100) return 10; if (total <= 300) return 25; if (total <= 1000) return 50; return 100; } private static IEnumerable> SplitIntoBatches(IReadOnlyList items, int batchSize) { if (items == null || items.Count == 0) yield break; if (batchSize <= 0) batchSize = 1; for (int i = 0; i < items.Count; i += batchSize) { int count = Math.Min(batchSize, items.Count - i); var batch = new List(count); for (int j = 0; j < count; j++) { batch.Add(items[i + j]); } yield return batch; } } private void TransferDbToDevice(TemplateTransferJob job, List employeeIds, List<(AttendanceMachine Machine, string Type)> targets, MySqlConnection connection, List logs) { bool verifyAfterPush = GetBoolAppSetting("TEMPLATE_VERIFY_AFTER_PUSH", true); int delayMs = GetIntAppSetting("TEMPLATE_DEVICE_DELAY_MS", 0); var effectiveEmployeeIds = employeeIds ?? new List(); // Optional filtering by EmpIds configured on the job (for safe, small-scope tests) if (job.EmpIds != null && job.EmpIds.Count > 0) { var filter = new HashSet(job.EmpIds, StringComparer.OrdinalIgnoreCase); effectiveEmployeeIds = effectiveEmployeeIds.Where(id => filter.Contains(id)).ToList(); Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} source={job.SourceIp} employees_filtered={effectiveEmployeeIds.Count}/{employeeIds.Count}"); } var templates = _templateDao.GetActiveTemplatesBySerialNos(connection, effectiveEmployeeIds); logs.Add($"[TemplateJob {job.Id}] DB templates fetched. count={templates.Count}"); // Log whether each requested employee has a template in DB foreach (var empId in effectiveEmployeeIds) { if (templates.ContainsKey(empId)) { Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} source={job.SourceIp} emp={empId} template=FOUND"); } else { Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} source={job.SourceIp} emp={empId} template=NOT_FOUND"); } } // Batch per target machine. var templatePairs = templates.ToList(); int totalTemplates = templatePairs.Count; int batchSize = ChooseBatchSize(totalTemplates); var batches = SplitIntoBatches(templatePairs, batchSize).ToList(); foreach (var target in targets) { string targetDevInfo = target.Machine.GetDeviceInfo(); var callback = new CallBack(Program.BeCalled); int ok = 0; int fail = 0; int verifyOk = 0; int verifyFail = 0; logs.Add($"[TemplateTransfer] job={job.Id} target_ip={target.Machine.MachineIp} total_templates={totalTemplates} batch_size={batchSize} total_batches={batches.Count}"); for (int bi = 0; bi < batches.Count; bi++) { var swBatch = Stopwatch.StartNew(); int bAttempted = 0; int bOk = 0; int bFail = 0; int bVerifyOk = 0; int bVerifyFail = 0; foreach (var kvp in batches[bi]) { bAttempted++; string empId = kvp.Key; string templateStr = Encoding.UTF8.GetString(kvp.Value ?? Array.Empty()); if (!TemplateParser.TryBuildSetEmployeeCommand(templateStr, out var setCmd)) { fail++; bFail++; logs.Add($"[TemplateJob {job.Id}] Build SetEmployee failed. emp={empId} target={target.Machine.MachineIp}"); Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=FAIL reason=BuildSetEmployee"); continue; } string response = ""; uint recvLen = 0; int rc = Program.test(targetDevInfo, targetDevInfo.Length, setCmd, setCmd.Length, ref response, ref recvLen, callback); if (rc != 0 || (response != null && response.IndexOf("fail", StringComparison.OrdinalIgnoreCase) >= 0)) { fail++; bFail++; logs.Add($"[TemplateJob {job.Id}] SetEmployee failed. emp={empId} target={target.Machine.MachineIp}"); Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=FAIL rc={rc}"); } else { ok++; bOk++; Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=OK"); if (verifyAfterPush) { string verifyCmd = "GetEmployee(id=\"" + empId + "\")"; string verifyResp = ""; uint verifyRecvLen = 0; int vrc = Program.test(targetDevInfo, targetDevInfo.Length, verifyCmd, verifyCmd.Length, ref verifyResp, ref verifyRecvLen, callback); bool success = vrc == 0 && verifyResp != null && verifyResp.IndexOf("success", StringComparison.OrdinalIgnoreCase) >= 0; if (success) { verifyOk++; bVerifyOk++; Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} verify=OK"); } else { verifyFail++; bVerifyFail++; Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} verify=FAIL rc={vrc}"); } } } } if (delayMs > 0) { System.Threading.Thread.Sleep(delayMs); } swBatch.Stop(); logs.Add($"[TemplateTransferBatch] job={job.Id} target_ip={target.Machine.MachineIp} batch={bi + 1}/{batches.Count} attempted={bAttempted} ok={bOk} fail={bFail} verify_ok={bVerifyOk} verify_fail={bVerifyFail} duration_sec={Math.Round(swBatch.Elapsed.TotalSeconds, 1)}"); } if (verifyAfterPush) { logs.Add($"[TemplateJob {job.Id}] DB_TO_DEVICE target={target.Machine.MachineIp} ok={ok} fail={fail} verify_ok={verifyOk} verify_fail={verifyFail}"); } else { logs.Add($"[TemplateJob {job.Id}] DB_TO_DEVICE target={target.Machine.MachineIp} ok={ok} fail={fail}"); } } } private static bool GetBoolAppSetting(string key, bool defaultValue) { try { var val = ConfigurationManager.AppSettings[key]; if (string.IsNullOrWhiteSpace(val)) return defaultValue; return bool.TryParse(val.Trim(), out var parsed) ? parsed : defaultValue; } catch { return defaultValue; } } private static int GetIntAppSetting(string key, int defaultValue) { try { var val = ConfigurationManager.AppSettings[key]; if (string.IsNullOrWhiteSpace(val)) return defaultValue; return int.TryParse(val.Trim(), out var parsed) ? parsed : defaultValue; } catch { return defaultValue; } } private void TransferDeviceToDevice(string jobId, AttendanceMachine source, List employeeIds, List<(AttendanceMachine Machine, string Type)> targets, List logs) { string sourceDevInfo = source.GetDeviceInfo(); var callback = new CallBack(Program.BeCalled); foreach (var target in targets) { int ok = 0; int fail = 0; string targetDevInfo = target.Machine.GetDeviceInfo(); foreach (var empId in employeeIds) { string getCmd = "GetEmployee(id=\"" + empId + "\")"; string getResp = ""; uint recvLen = 0; int rc = Program.test(sourceDevInfo, sourceDevInfo.Length, getCmd, getCmd.Length, ref getResp, ref recvLen, callback); if (rc != 0 || getResp == null || getResp.IndexOf("success", StringComparison.OrdinalIgnoreCase) < 0) { fail++; continue; } if (!TemplateParser.TryBuildSetEmployeeCommand(getResp, out var setCmd)) { fail++; continue; } string setResp = ""; uint setRecvLen = 0; int rc2 = Program.test(targetDevInfo, targetDevInfo.Length, setCmd, setCmd.Length, ref setResp, ref setRecvLen, callback); if (rc2 != 0 || (setResp != null && setResp.IndexOf("fail", StringComparison.OrdinalIgnoreCase) >= 0)) { fail++; } else { ok++; } } logs.Add($"[TemplateJob {jobId}] DEVICE_TO_DEVICE target={target.Machine.MachineIp} ok={ok} fail={fail}"); } } private static void WriteRawTemplateLog(string header, string raw) { try { string filepath = AppDomain.CurrentDomain.BaseDirectory + "\\TemplateRawLogs\\TemplateRaw_" + DateTime.Now.Date.ToShortDateString().Replace('/', '_') + ".txt"; LogService.EnqueueLine(filepath, "----- " + DateTime.Now + " -----"); LogService.EnqueueLine(filepath, header); LogService.EnqueueLine(filepath, raw ?? ""); LogService.EnqueueLine(filepath, ""); } catch { // ignore logging failures } } } }