1573 lines
69 KiB
C#
1573 lines
69 KiB
C#
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
|
|
{
|
|
/// <summary>True when started via console/debug entry (not SCM).</summary>
|
|
internal static bool IsConsoleMode { get; private set; }
|
|
|
|
private static MySqlConnection connection;
|
|
|
|
//public static int TotalEmp;
|
|
//static List<AttendanceMachineUser> DBusers = new List<AttendanceMachineUser>();
|
|
|
|
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();
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
static void Main(string[] args)
|
|
{
|
|
ApplicationPaths.Initialize();
|
|
try
|
|
{
|
|
SetDllDirectory(ApplicationPaths.BaseDirectory);
|
|
}
|
|
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 string GetArgValue(string[] args, params string[] keys)
|
|
{
|
|
if (args == null || keys == null) return null;
|
|
foreach (var arg in args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(arg)) continue;
|
|
foreach (var key in keys)
|
|
{
|
|
if (arg.StartsWith(key + "=", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return arg.Substring(key.Length + 1);
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static void RunAsConsole(string[] args)
|
|
{
|
|
if (HasArg(args, "--test-planning", "/test-planning"))
|
|
{
|
|
int failed = DbToDevicePlanningTests.RunAll();
|
|
failed += NedoTemplateConverterTests.RunAll();
|
|
failed += HrmsEmployeePhotoClientTests.RunAll();
|
|
failed += HrmsEmployeeMappingTests.RunAll();
|
|
failed += DbToDeviceSummaryTests.RunAll();
|
|
failed += DbToDeviceFaceErrorsTests.RunAll();
|
|
failed += MachineUserDeleteSyncTests.RunAll();
|
|
Environment.ExitCode = failed == 0 ? 0 : 1;
|
|
return;
|
|
}
|
|
|
|
if (HasArg(args, "--test-template-load", "/test-template-load"))
|
|
{
|
|
RunTemplateLoadTest();
|
|
return;
|
|
}
|
|
|
|
string diagnoseEmp = GetArgValue(args, "--diagnose-emp", "/diagnose-emp");
|
|
if (!string.IsNullOrWhiteSpace(diagnoseEmp))
|
|
{
|
|
RunDiagnoseEmployee(diagnoseEmp.Trim());
|
|
return;
|
|
}
|
|
|
|
string inspectPortalPhoto = GetArgValue(args, "--inspect-portal-photo", "/inspect-portal-photo");
|
|
if (!string.IsNullOrWhiteSpace(inspectPortalPhoto))
|
|
{
|
|
RunInspectPortalPhoto(inspectPortalPhoto.Trim());
|
|
return;
|
|
}
|
|
|
|
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<string>());
|
|
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 filepath = ApplicationPaths.DatedFile(ApplicationPaths.TemplateRawLogs, "ServiceLog");
|
|
LogService.EnqueueLine(filepath, Message);
|
|
}
|
|
|
|
public static void WriteInternalLog(string message)
|
|
{
|
|
try
|
|
{
|
|
string filepath = ApplicationPaths.DatedFile(ApplicationPaths.InternalLogs, "InternalLog");
|
|
LogService.EnqueueLine(filepath, message);
|
|
}
|
|
catch
|
|
{
|
|
// swallow internal logging failures
|
|
}
|
|
}
|
|
|
|
public static void RecordUnreachableMachine(AttendanceMachine machine, string reason)
|
|
{
|
|
try
|
|
{
|
|
string filepath = ApplicationPaths.DatedFile(ApplicationPaths.InternalLogs, "UnreachableMachines");
|
|
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 = ApplicationPaths.DatedFile(ApplicationPaths.InternalLogs, "UnreachableMachines");
|
|
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<string> syncAttendance()
|
|
{
|
|
List<string> responses = new List<string>();
|
|
|
|
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<Attendance>();
|
|
foreach (var log in logs)
|
|
{
|
|
string acNo = !string.IsNullOrWhiteSpace(log.EnrollId) ? log.EnrollId.Trim() : (log.AcNo ?? "").Trim();
|
|
if (string.IsNullOrWhiteSpace(acNo))
|
|
{
|
|
WriteInternalLog($"[AttendanceSkip] machine_id={machine.MachineId} ip={machine.MachineIp} reason=missing_enrollid name={log.Name ?? ""} time={log.CheckTime:yyyy-MM-dd HH:mm:ss}");
|
|
continue;
|
|
}
|
|
|
|
WriteInternalLog($"[AttendanceParsed] machine_id={machine.MachineId} ip={machine.MachineIp} enrollid={acNo} name={log.Name ?? ""} checktime={log.CheckTime:yyyy-MM-dd HH:mm:ss}");
|
|
parsedRows.Add(new Attendance(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<AttendanceMachine> 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<string> syncMachineUsers()
|
|
{
|
|
|
|
List<string> responses = new List<string>();
|
|
CallBack myCallBack = new CallBack(Program.BeCalled);
|
|
Initialize();
|
|
using (var conn = OpenDbConnectionWithRetry("syncMachineUsers"))
|
|
{
|
|
AttendanceMachineUserDAO attendanceMachineUsersDAO = new AttendanceMachineUserDAO();
|
|
|
|
List<AttendanceMachineUser> 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<AttendanceMachineUser> DBusers,
|
|
List<string> 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)
|
|
{
|
|
var pendingItems = new List<MachineUserDeleteItem>();
|
|
foreach (var empId in toDelete)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(empId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string trimmed = empId.Trim();
|
|
var match = users.FirstOrDefault(u =>
|
|
string.Equals(u.SerialNumber, trimmed, StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(u.DeviceUserId, trimmed, StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(u.Name, trimmed, StringComparison.OrdinalIgnoreCase));
|
|
string deviceEnrollId = match != null ? match.DeviceUserId : trimmed;
|
|
pendingItems.Add(new MachineUserDeleteItem(trimmed, deviceEnrollId));
|
|
}
|
|
|
|
int batchSize = MachineUserDeleteSettings.GetBatchSize();
|
|
var deleteResult = MachineUserDeleteSync.Execute(
|
|
machine.MachineId,
|
|
machine.MachineIp,
|
|
pendingItems,
|
|
batchSize,
|
|
(IReadOnlyList<string> deviceIds, out string delErr) => client.DeleteUsers(deviceIds, out delErr),
|
|
serial =>
|
|
{
|
|
attendanceMachineUsersDAO.MarkUserDeleted(conn, machine.MachineId, serial);
|
|
string line = serial + " removed from " + machine.MachineId;
|
|
responses.Add(line);
|
|
WriteInternalLog(line);
|
|
},
|
|
WriteInternalLog);
|
|
|
|
if (deleteResult.FailedCount > 0)
|
|
{
|
|
WriteInternalLog($"syncMachineUsers HTTP deleteusers completed with failures. machine_id={machine.MachineId} success={deleteResult.SuccessCount} failed={deleteResult.FailedCount}");
|
|
}
|
|
}
|
|
}
|
|
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<AttendanceMachineUser> DBusers,
|
|
List<string> 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<AttendanceMachine> GetMachinesForAttendanceAndUsers(AttendanceMachineDAO attendanceMachineDAO, MySqlConnection connection, List<string> logs)
|
|
{
|
|
if (!TryLoadDeviceSettings(out var settings, logs))
|
|
{
|
|
logs?.Add("DeviceSettings could not be loaded; no machines selected.");
|
|
return new List<AttendanceMachine>();
|
|
}
|
|
|
|
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<AttendanceMachine> 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<AttendanceMachine>();
|
|
|
|
// 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<string>(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<string>(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<string>(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 <Device> 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 <Device> entries but none match DB machines in scope. Syncing all DB machines in scope.");
|
|
}
|
|
else
|
|
{
|
|
logs?.Add("DeviceSettings has no <Device> entries. Syncing all DB machines in scope.");
|
|
}
|
|
|
|
return dbMachines;
|
|
}
|
|
|
|
public static List<string> syncTemplates()
|
|
{
|
|
var logs = new List<string>();
|
|
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;
|
|
}
|
|
|
|
private static void RunTemplateLoadTest()
|
|
{
|
|
var logs = new List<string>();
|
|
try
|
|
{
|
|
Initialize();
|
|
if (!TryLoadDeviceSettings(out var settings, logs))
|
|
{
|
|
foreach (var line in logs) Console.WriteLine(line);
|
|
Environment.ExitCode = 1;
|
|
return;
|
|
}
|
|
|
|
var job = settings?.TemplateTransfer?.Jobs?.FirstOrDefault(j => j.Enabled);
|
|
if (job == null)
|
|
{
|
|
Console.WriteLine("No enabled template transfer job found.");
|
|
Environment.ExitCode = 1;
|
|
return;
|
|
}
|
|
|
|
using (var conn = OpenDbConnectionWithRetry("testTemplateLoad"))
|
|
{
|
|
var employeeIds = DbToDevicePlanning.ResolveEmployeeIds(
|
|
job,
|
|
departmentIds => new AttendanceMachineFaceTemplateDAO().GetActiveSerialNumbersByDepartmentIds(conn, departmentIds, new List<string>()),
|
|
() => new List<string>(job.EmpIds ?? new List<string>()));
|
|
|
|
Console.WriteLine($"[DB_TO_DEVICE] Template load test employees={employeeIds.Count}");
|
|
var runner = new TemplateTransferRunner(new AttendanceMachineDAO(), new AttendanceMachineFaceTemplateDAO());
|
|
var templates = runner.LoadTemplatesForDbToDeviceTest(conn, employeeIds, logs);
|
|
int missing = employeeIds.Count - templates.Count;
|
|
logs.Add($"[DB_TO_DEVICE] Template loading complete requested={employeeIds.Count} loaded={templates.Count} missing={missing}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logs.Add("Template load test failed: " + ex);
|
|
}
|
|
|
|
foreach (var line in logs)
|
|
{
|
|
Console.WriteLine(line);
|
|
}
|
|
|
|
Environment.ExitCode = logs.Any(l => l.IndexOf("failed", StringComparison.OrdinalIgnoreCase) >= 0) ? 1 : 0;
|
|
}
|
|
|
|
private static void RunInspectPortalPhoto(string deviceSerial)
|
|
{
|
|
try
|
|
{
|
|
Initialize();
|
|
string photoEmployeeId = deviceSerial;
|
|
string mappedName = null;
|
|
using (var conn = OpenDbConnectionWithRetry("inspectPortalPhoto"))
|
|
{
|
|
var employeeDao = new HrmsEmployeeDAO();
|
|
if (employeeDao.TryGetByDeviceSerial(conn, deviceSerial, out HrmsEmployeeInfo employee))
|
|
{
|
|
photoEmployeeId = employee.EmployeeId;
|
|
mappedName = employee.ConcatenatedName;
|
|
Console.WriteLine($"[PORTAL_PHOTO] device_serial={deviceSerial} hrms_id={employee.EmployeeId} name={employee.ConcatenatedName}");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"[PORTAL_PHOTO] device_serial={deviceSerial} hrms_lookup=MISS (treating argument as employees.id for photo URL)");
|
|
}
|
|
}
|
|
|
|
string url = HrmsEmployeePhotoClient.BuildPhotoUrl(EmployeePhotoSourceSettings.GetBaseUrl(), photoEmployeeId);
|
|
Console.WriteLine($"[PORTAL_PHOTO] emp={deviceSerial} hrms_id={photoEmployeeId} enabled={EmployeePhotoSourceSettings.IsEnabled()} url={url}");
|
|
|
|
if (!HrmsEmployeePhotoClient.TryDownloadEmployeePhoto(photoEmployeeId, out HrmsEmployeePhotoDownloadResult result))
|
|
{
|
|
Console.WriteLine($"[PORTAL_PHOTO] emp={deviceSerial} hrms_id={photoEmployeeId} download=FAILED http={result?.StatusCode ?? 0} content_type={result?.ContentType ?? ""} err={result?.Error ?? "unknown"}");
|
|
Environment.ExitCode = 1;
|
|
return;
|
|
}
|
|
|
|
Console.WriteLine($"[PORTAL_PHOTO] emp={deviceSerial} hrms_id={photoEmployeeId} name={mappedName ?? ""} download=OK http={result.StatusCode} content_type={result.ContentType ?? ""}");
|
|
Console.WriteLine($"[PORTAL_PHOTO] emp={deviceSerial} photo_size={result.ImageBytes?.Length ?? 0} width={result.Width} height={result.Height} base64_len={result.Base64Length}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("Inspect portal photo failed: " + ex);
|
|
Environment.ExitCode = 1;
|
|
}
|
|
}
|
|
|
|
private static void RunDiagnoseEmployee(string empId)
|
|
{
|
|
var logs = new List<string>();
|
|
var writer = new StringWriter();
|
|
try
|
|
{
|
|
Initialize();
|
|
if (!TryLoadDeviceSettings(out var settings, logs))
|
|
{
|
|
foreach (var line in logs) Console.WriteLine(line);
|
|
Environment.ExitCode = 1;
|
|
return;
|
|
}
|
|
|
|
var job = settings?.TemplateTransfer?.Jobs?.FirstOrDefault(j => j.Enabled);
|
|
if (job == null || job.TargetIps == null || job.TargetIps.Count == 0)
|
|
{
|
|
Console.WriteLine("No enabled template job with targets found.");
|
|
Environment.ExitCode = 1;
|
|
return;
|
|
}
|
|
|
|
string targetIp = job.TargetIps[0];
|
|
using (var conn = OpenDbConnectionWithRetry("diagnoseEmp"))
|
|
{
|
|
var machineDao = new AttendanceMachineDAO();
|
|
if (!machineDao.TryGetAttendanceMachineByIp(conn, targetIp, out var machine, out _))
|
|
{
|
|
Console.WriteLine("Target machine not found for ip=" + targetIp);
|
|
Environment.ExitCode = 1;
|
|
return;
|
|
}
|
|
|
|
var templateDao = new AttendanceMachineFaceTemplateDAO();
|
|
var templates = templateDao.GetActiveTemplatesBySerialNos(conn, new List<string> { empId });
|
|
byte[] blob = null;
|
|
if (!templates.TryGetValue(empId, out blob))
|
|
{
|
|
if (!EmployeePhotoSourceSettings.IsEnabled())
|
|
{
|
|
Console.WriteLine("No active template in DB for emp=" + empId);
|
|
Environment.ExitCode = 1;
|
|
return;
|
|
}
|
|
|
|
blob = Array.Empty<byte>();
|
|
}
|
|
|
|
var uploadPlan = DbToDeviceUploadPlan.FromBlob(blob);
|
|
if (uploadPlan.Format == DbToDeviceTemplateFormat.Empty && EmployeePhotoSourceSettings.IsEnabled())
|
|
{
|
|
uploadPlan.Format = DbToDeviceTemplateFormat.NedoXml;
|
|
uploadPlan.PushMode = "face";
|
|
uploadPlan.UseProfileVerification = true;
|
|
}
|
|
bool portalPhotoEnabled = uploadPlan.Format == DbToDeviceTemplateFormat.NedoXml
|
|
&& EmployeePhotoSourceSettings.IsEnabled();
|
|
if (!uploadPlan.HasUploadPayload && !portalPhotoEnabled)
|
|
{
|
|
Console.WriteLine("No uploadable template payload for emp=" + empId + " format=" + uploadPlan.FormatLabel);
|
|
Environment.ExitCode = 1;
|
|
return;
|
|
}
|
|
|
|
var machineUserDao = new AttendanceMachineUserDAO();
|
|
var employeeDao = new HrmsEmployeeDAO();
|
|
HrmsEmployeeInfo hrmsEmployee = null;
|
|
try
|
|
{
|
|
employeeDao.TryGetByDeviceSerial(conn, empId, out hrmsEmployee);
|
|
}
|
|
catch (Exception exEmp)
|
|
{
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} hrms_lookup=FAILED err={exEmp.Message}");
|
|
}
|
|
|
|
string machineUserName = machineUserDao.GetEmployeeNameForMachine(conn, machine.MachineId, empId);
|
|
string displayName = !string.IsNullOrWhiteSpace(hrmsEmployee?.ConcatenatedName)
|
|
? hrmsEmployee.ConcatenatedName
|
|
: (!string.IsNullOrWhiteSpace(machineUserName) ? machineUserName : empId);
|
|
|
|
if (hrmsEmployee != null)
|
|
{
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} hrms_id={hrmsEmployee.EmployeeId} name={displayName}");
|
|
}
|
|
else
|
|
{
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} hrms_lookup=MISS name={displayName}");
|
|
}
|
|
|
|
if (!int.TryParse(empId, out int enrollId))
|
|
{
|
|
Console.WriteLine("empId must be numeric enrollid for Hanvon HTTP API. emp=" + empId);
|
|
Environment.ExitCode = 1;
|
|
return;
|
|
}
|
|
|
|
var client = HanvonHttpApiClient.ForMachine(machine);
|
|
if (!client.TryLogin(out string loginErr))
|
|
{
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} login=FAILED err={loginErr}");
|
|
Environment.ExitCode = 1;
|
|
return;
|
|
}
|
|
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} template=FOUND format={uploadPlan.FormatLabel}");
|
|
if (uploadPlan.Format == DbToDeviceTemplateFormat.NedoXml)
|
|
{
|
|
bool hadNedoPayload = uploadPlan.HasUploadPayload;
|
|
if (portalPhotoEnabled)
|
|
{
|
|
string photoEmployeeId = hrmsEmployee?.EmployeeId;
|
|
if (uploadPlan.TryResolveEmployeePhoto(photoEmployeeId, out string resolveErr))
|
|
{
|
|
if (string.Equals(uploadPlan.PhotoSourceLabel, "HRMS_PORTAL", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} hrms_id={photoEmployeeId} photo_source=HRMS_PORTAL photo_url={uploadPlan.PhotoSourceUrl} photo_size={uploadPlan.PhotoByteLength} width={uploadPlan.PreparedWidth} height={uploadPlan.PreparedHeight}");
|
|
}
|
|
else if (hadNedoPayload)
|
|
{
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} photo_source=HRMS_PORTAL FAILED err={resolveErr ?? "unknown"}");
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} photo_source=NEDO_XML FALLBACK");
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} nedo_photo=FOUND base64_len={uploadPlan.PhotoBase64Length} size={uploadPlan.PhotoByteLength} blob_len={uploadPlan.SourceBlobLength}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} photo_source=HRMS_PORTAL FAILED err={resolveErr ?? "unknown"}");
|
|
}
|
|
}
|
|
else if (uploadPlan.HasUploadPayload)
|
|
{
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} nedo_photo=FOUND base64_len={uploadPlan.PhotoBase64Length} size={uploadPlan.PhotoByteLength} blob_len={uploadPlan.SourceBlobLength}");
|
|
}
|
|
}
|
|
|
|
if (!uploadPlan.HasUploadPayload)
|
|
{
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} face_upload=FAILED response=no_photo_available");
|
|
Environment.ExitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (!uploadPlan.TryPrepareFacePayload(out string prepareErr))
|
|
{
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} format={uploadPlan.FormatLabel} photo=INVALID err={prepareErr}");
|
|
Environment.ExitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(uploadPlan.PrepareNote))
|
|
{
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} nedo_photo=PREPARED note={uploadPlan.PrepareNote} width={uploadPlan.PreparedWidth} height={uploadPlan.PreparedHeight} size={uploadPlan.PhotoByteLength}");
|
|
}
|
|
|
|
if (!client.TryUploadDbToDeviceTemplate(enrollId, displayName, uploadPlan, out string uploadErr))
|
|
{
|
|
if (DbToDeviceFaceErrors.TryParseDuplicateFace(uploadErr, out string existingDeviceId))
|
|
{
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} face=DUPLICATE existing_device_id={existingDeviceId}");
|
|
}
|
|
else
|
|
{
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} face_upload=FAILED response={uploadErr}");
|
|
Environment.ExitCode = 1;
|
|
}
|
|
}
|
|
else if (client.VerifyDbToDeviceTemplate(enrollId, uploadPlan, out string faceflag, out string photourl, out string verifyErr))
|
|
{
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} target={machine.MachineIp} face_upload=OK photo_source={uploadPlan.PhotoSourceLabel ?? uploadPlan.FormatLabel} push_mode={uploadPlan.PushMode} payload_len={uploadPlan.Payload?.Length ?? 0}");
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} verify faceflag={faceflag ?? ""} photourl={photourl ?? ""} face=OK");
|
|
}
|
|
else
|
|
{
|
|
writer.WriteLine($"[DB_TO_DEVICE] employee={empId} verify=FAILED faceflag={faceflag ?? ""} photourl={photourl ?? ""} err={verifyErr}");
|
|
Environment.ExitCode = 1;
|
|
}
|
|
|
|
var users = client.GetUserList(out string listErr);
|
|
var match = users.FirstOrDefault(u =>
|
|
string.Equals(u.DeviceUserId, empId, StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(u.SerialNumber, empId, StringComparison.OrdinalIgnoreCase));
|
|
writer.WriteLine($"[DB_TO_DEVICE] emp={empId} getuserlist user_exists={(match != null ? "true" : "false")} list_id={match?.DeviceUserId ?? ""} list_name={match?.Name ?? ""}");
|
|
if (!string.IsNullOrWhiteSpace(listErr))
|
|
{
|
|
writer.WriteLine($"[DB_TO_DEVICE] getuserlist_err={listErr}");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
writer.WriteLine("Diagnose failed: " + ex);
|
|
}
|
|
|
|
string output = writer.ToString();
|
|
Console.WriteLine(output);
|
|
try
|
|
{
|
|
string logPath = Path.Combine(ApplicationPaths.InternalLogs, "DbToDeviceDiag_" + empId + "_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".txt");
|
|
File.WriteAllText(logPath, output);
|
|
Console.WriteLine("Diagnostic log written: " + logPath);
|
|
}
|
|
catch
|
|
{
|
|
// ignore file write failures
|
|
}
|
|
}
|
|
|
|
// ApplyDeviceSelectionRulesIfConfigured removed: selection is now config-driven (IP allowlist/blocklist) without SITE_ID env var.
|
|
|
|
private static bool TryLoadDeviceSettings(out DeviceSettingsConfig settings, List<string> logs)
|
|
{
|
|
settings = null;
|
|
|
|
string path = ConfigurationManager.AppSettings["DEVICE_SETTINGS_PATH"];
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
{
|
|
path = "DeviceSettings.xml";
|
|
}
|
|
|
|
path = ApplicationPaths.Resolve(path);
|
|
|
|
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<AttendanceMachine> 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<Attendance> ParseAttendanceRows(string response, AttendanceMachine machine)
|
|
{
|
|
var rows = new List<Attendance>();
|
|
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<Attendance> 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<AttendanceMachineUser> 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);
|
|
}
|