HanvonAttendanceService/TemplateTransferRunner.cs

984 lines
48 KiB
C#

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<string> 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 (mode == TemplateTransferMode.DB_TO_DEVICE)
{
List<string> idsForTransfer;
if (job.EmpIds != null && job.EmpIds.Count > 0)
{
// Restore from DB even when user was deleted from source device.
idsForTransfer = job.EmpIds
.Where(id => !string.IsNullOrWhiteSpace(id))
.Select(id => id.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
logs.Add($"[TemplateJob {job.Id}] DB_TO_DEVICE using EmpIds from config (source device list skipped): {string.Join(",", idsForTransfer)}");
}
else
{
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;
}
idsForTransfer = employeeIds;
}
TransferDbToDevice(job, idsForTransfer, targetMachines, connection, logs);
continue;
}
if (!TryGetEmployeeIdsFromDevice(sourceMachine, out var deviceEmployeeIds, logs, job.Id))
{
continue;
}
if (deviceEmployeeIds.Count == 0)
{
logs.Add($"[TemplateJob {job.Id}] Source device returned 0 employee IDs.");
continue;
}
if (mode == TemplateTransferMode.DEVICE_TO_DEVICE)
{
TransferDeviceToDevice(job.Id, sourceMachine, deviceEmployeeIds, targetMachines, logs);
}
}
}
private bool TryGetEmployeeIdsFromDevice(AttendanceMachine machine, out List<string> employeeIds, List<string> logs, string jobId)
{
employeeIds = new List<string>();
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<string> 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;
}
/// <summary>
/// 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).
/// </summary>
private void DistributeMissingTemplatesAfterDeviceToDb(AttendanceMachine sourceMachine, List<AttendanceMachine> machinesInScope, List<string> 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<AttendanceMachine>();
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<string, Dictionary<string, byte[]>>(StringComparer.OrdinalIgnoreCase);
var missingSerialsUnion = new HashSet<string>(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<string>(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<string, byte[]>(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<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var serial in missingSerialsUnion)
{
pushedTargetsBySerial[serial] = new HashSet<string>(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<string> 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);
}
/// <summary>Send only missing templates; does not issue SetEmployee for serials already on device.</summary>
private int PushDistributionBatches(AttendanceMachine target, Dictionary<string, byte[]> templates, int batchSize, List<string> 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<byte>());
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<List<T>> SplitIntoBatches<T>(IReadOnlyList<T> 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<T>(count);
for (int j = 0; j < count; j++)
{
batch.Add(items[i + j]);
}
yield return batch;
}
}
private void TransferDbToDevice(TemplateTransferJob job, List<string> employeeIds, List<(AttendanceMachine Machine, string Type)> targets, MySqlConnection connection, List<string> logs)
{
bool verifyAfterPush = GetBoolAppSetting("TEMPLATE_VERIFY_AFTER_PUSH", true);
int delayMs = GetIntAppSetting("TEMPLATE_DEVICE_DELAY_MS", 0);
var effectiveEmployeeIds = employeeIds ?? new List<string>();
// 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<string>(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)
{
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}");
if (DeviceProtocol.UseHttp())
{
var httpClient = HanvonHttpApiClient.ForMachine(target.Machine);
if (!httpClient.TryLogin(out string loginErr))
{
logs.Add($"[TemplateJob {job.Id}] HTTP login failed for target. ip={target.Machine.MachineIp} err={loginErr}");
Program.RecordUnreachableMachine(target.Machine, "DB_TO_DEVICE HTTP login: " + loginErr);
continue;
}
var machineUserDao = new AttendanceMachineUserDAO();
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;
byte[] templateBlob = kvp.Value ?? Array.Empty<byte>();
string recordBase64 = HanvonHttpApiClient.NormalizeFaceTemplatePayload(templateBlob, out string pushMode);
if (string.IsNullOrWhiteSpace(recordBase64))
{
fail++;
bFail++;
logs.Add($"[TemplateJob {job.Id}] Empty template payload. emp={empId} blob_bytes={templateBlob.Length} target={target.Machine.MachineIp}");
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=FAIL reason=EmptyTemplate blob_bytes={templateBlob.Length}");
continue;
}
if (!int.TryParse(empId, out int enrollId))
{
fail++;
bFail++;
logs.Add($"[TemplateJob {job.Id}] Invalid enroll id for HTTP setuserinfo. emp={empId} target={target.Machine.MachineIp}");
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=FAIL reason=InvalidEnrollId");
continue;
}
string displayName = machineUserDao.GetEmployeeNameForMachine(connection, target.Machine.MachineId, empId)
?? empId;
if (!httpClient.TryRestoreUserWithFaceTemplate(enrollId, displayName, recordBase64, pushMode, out string pushErr))
{
fail++;
bFail++;
logs.Add($"[TemplateJob {job.Id}] HTTP template push failed. emp={empId} mode={pushMode} payload_len={recordBase64.Length} target={target.Machine.MachineIp} err={pushErr}");
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} mode={pushMode} payload_len={recordBase64.Length} send=FAIL err={pushErr}");
continue;
}
ok++;
bOk++;
logs.Add($"[TemplateJob {job.Id}] HTTP template push ok. emp={empId} name={displayName} mode={pushMode} payload_len={recordBase64.Length} target={target.Machine.MachineIp}");
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=OK");
if (verifyAfterPush)
{
if (httpClient.UserHasFaceTemplate(enrollId, out string verifyErr))
{
verifyOk++;
bVerifyOk++;
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} verify=OK");
}
else
{
verifyFail++;
bVerifyFail++;
logs.Add($"[TemplateJob {job.Id}] HTTP verify failed after push. emp={empId} target={target.Machine.MachineIp} err={verifyErr}");
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} verify=FAIL err={verifyErr}");
}
}
}
if (delayMs > 0)
{
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)}");
}
}
else
{
string targetDevInfo = target.Machine.GetDeviceInfo();
var callback = new CallBack(Program.BeCalled);
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<byte>());
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)
{
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<string> employeeIds, List<(AttendanceMachine Machine, string Type)> targets, List<string> 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
}
}
}
}