458 lines
18 KiB
C#
458 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Threading;
|
|
using HikvisionAttendanceService.Data;
|
|
|
|
namespace HikvisionAttendanceService;
|
|
|
|
/// <summary>
|
|
/// Resolves sync targets from the attendance_machine repository without site-scope filtering.
|
|
/// Source-device selection via <see cref="ResolveRuntimeDevices"/> is unchanged.
|
|
/// </summary>
|
|
internal sealed partial class HikvisionAttendanceManager
|
|
{
|
|
// Prefer universal paths — AccessControl/capabilities returns 404 on many terminals.
|
|
private static readonly string[] TargetIsapiProbePaths =
|
|
{
|
|
"/ISAPI/System/deviceInfo",
|
|
"/ISAPI/AccessControl/UserInfo/capabilities?format=json",
|
|
"/ISAPI/Intelligent/FDLib?format=json",
|
|
"/ISAPI/AccessControl/capabilities?format=json"
|
|
};
|
|
|
|
private bool TryResolveSyncTargetByIp(string requestedIp, out HikvisionAttendanceWindowsService.DeviceConfig? device)
|
|
{
|
|
device = null;
|
|
var wantedIp = (requestedIp ?? "").Trim();
|
|
if (wantedIp.Length == 0)
|
|
{
|
|
LogTargetResolve(wantedIp, found: false, machineId: "", port: 0, machineType: "", note: "empty IP");
|
|
return false;
|
|
}
|
|
|
|
if (_attendanceMachineRepository == null || !_config.EnableDbIntegration)
|
|
{
|
|
device = ResolveDeviceConfigByIp(wantedIp);
|
|
var found = device != null && !string.IsNullOrWhiteSpace(device.Ip);
|
|
LogTargetResolve(
|
|
wantedIp,
|
|
found,
|
|
device?.DeviceId ?? "",
|
|
device?.Port ?? 0,
|
|
"CONFIG_FALLBACK",
|
|
found ? "resolved from runtime config (DB integration disabled)" : "Target Device Not Found");
|
|
return found;
|
|
}
|
|
|
|
if (!_attendanceMachineRepository.TryGetMachineByIp(wantedIp, out var row, out var dbErr))
|
|
{
|
|
LogTargetResolve(wantedIp, found: false, machineId: "", port: 0, machineType: "",
|
|
note: string.IsNullOrWhiteSpace(dbErr) ? "Target Device Not Found" : "Target lookup failed: " + dbErr);
|
|
_logger.Warn("UserSync: Target Device Not Found. TargetMachineIp=\"" + wantedIp + "\".");
|
|
return false;
|
|
}
|
|
|
|
if (!IsAttendanceMachineActive(row!))
|
|
{
|
|
LogTargetResolve(
|
|
wantedIp,
|
|
found: false,
|
|
row!.MachineId,
|
|
row.PortNumber,
|
|
row.MachineType,
|
|
"Target device exists but machine_status is not active");
|
|
_logger.Warn("UserSync: target inactive in DB. TargetMachineIp=\"" + wantedIp +
|
|
"\" machine_id=" + row.MachineId + " machine_status=\"" + row.MachineStatus + "\".");
|
|
return false;
|
|
}
|
|
|
|
if (!TryBuildDeviceConfigFromAttendanceMachine(row!, out device, out var buildErr))
|
|
{
|
|
LogTargetResolve(wantedIp, found: false, row!.MachineId, row.PortNumber, row.MachineType, buildErr);
|
|
_logger.Warn("UserSync: target found but could not be built. TargetMachineIp=\"" + wantedIp +
|
|
"\" reason=\"" + buildErr + "\".");
|
|
return false;
|
|
}
|
|
|
|
LogTargetResolve(
|
|
wantedIp,
|
|
found: true,
|
|
device!.DeviceId,
|
|
device.Port,
|
|
row!.MachineType,
|
|
"resolved from attendance_machine");
|
|
return true;
|
|
}
|
|
|
|
private bool TryResolveSyncTargetByMachineId(string requestedMachineId, out HikvisionAttendanceWindowsService.DeviceConfig? device)
|
|
{
|
|
device = null;
|
|
var wantedId = (requestedMachineId ?? "").Trim();
|
|
if (wantedId.Length == 0)
|
|
{
|
|
LogTargetResolve("", found: false, machineId: wantedId, port: 0, machineType: "", note: "empty machine_id");
|
|
return false;
|
|
}
|
|
|
|
if (_attendanceMachineRepository == null || !_config.EnableDbIntegration)
|
|
{
|
|
device = ResolveDeviceConfig(wantedId);
|
|
var found = device != null && !string.IsNullOrWhiteSpace(device.Ip);
|
|
LogTargetResolve(
|
|
device?.Ip ?? "",
|
|
found,
|
|
wantedId,
|
|
device?.Port ?? 0,
|
|
"CONFIG_FALLBACK",
|
|
found ? "resolved from runtime config (DB integration disabled)" : "Target Device Not Found");
|
|
return found;
|
|
}
|
|
|
|
if (!_attendanceMachineRepository.TryGetMachineByMachineId(wantedId, out var row, out var dbErr))
|
|
{
|
|
LogTargetResolve("", found: false, machineId: wantedId, port: 0, machineType: "",
|
|
note: string.IsNullOrWhiteSpace(dbErr) ? "Target Device Not Found" : "Target lookup failed: " + dbErr);
|
|
_logger.Warn("UserSync: Target Device Not Found. TargetDeviceId=\"" + wantedId + "\".");
|
|
return false;
|
|
}
|
|
|
|
if (!IsAttendanceMachineActive(row!))
|
|
{
|
|
LogTargetResolve(
|
|
row!.MachineIp,
|
|
found: false,
|
|
row.MachineId,
|
|
row.PortNumber,
|
|
row.MachineType,
|
|
"Target device exists but machine_status is not active");
|
|
_logger.Warn("UserSync: target inactive in DB. TargetDeviceId=\"" + wantedId +
|
|
"\" machine_status=\"" + row.MachineStatus + "\".");
|
|
return false;
|
|
}
|
|
|
|
if (!TryBuildDeviceConfigFromAttendanceMachine(row!, out device, out var buildErr))
|
|
{
|
|
LogTargetResolve(row!.MachineIp, found: false, row.MachineId, row.PortNumber, row.MachineType, buildErr);
|
|
return false;
|
|
}
|
|
|
|
LogTargetResolve(device!.Ip, found: true, device.DeviceId, device.Port, row!.MachineType,
|
|
"resolved from attendance_machine");
|
|
return true;
|
|
}
|
|
|
|
private bool TryBuildDeviceConfigFromAttendanceMachine(
|
|
AttendanceMachineRow machine,
|
|
out HikvisionAttendanceWindowsService.DeviceConfig? device,
|
|
out string error)
|
|
{
|
|
device = null;
|
|
error = "";
|
|
if (string.IsNullOrWhiteSpace(machine.MachineIp))
|
|
{
|
|
error = "machine_ip empty in attendance_machine";
|
|
return false;
|
|
}
|
|
|
|
var configDevices = _config.Devices ?? new List<HikvisionAttendanceWindowsService.DeviceConfig>();
|
|
var credentialFallback = configDevices.FirstOrDefault(d =>
|
|
!string.IsNullOrWhiteSpace(d.Username) && !string.IsNullOrWhiteSpace(d.Password));
|
|
var matchingCfg = configDevices.FirstOrDefault(x =>
|
|
string.Equals((x.Ip ?? "").Trim(), machine.MachineIp.Trim(), StringComparison.OrdinalIgnoreCase));
|
|
var creds = matchingCfg ?? credentialFallback;
|
|
if (creds == null || string.IsNullOrWhiteSpace(creds.Username) || string.IsNullOrWhiteSpace(creds.Password))
|
|
{
|
|
error = "no username/password in serviceconfig Devices for target ip=" + machine.MachineIp;
|
|
return false;
|
|
}
|
|
|
|
var deviceId = !string.IsNullOrWhiteSpace(machine.MachineId)
|
|
? machine.MachineId.Trim()
|
|
: (!string.IsNullOrWhiteSpace(machine.MachineName) ? machine.MachineName.Trim() : machine.MachineIp.Trim());
|
|
var port = machine.PortNumber > 0 ? machine.PortNumber : (creds.Port > 0 ? creds.Port : 8000);
|
|
|
|
device = new HikvisionAttendanceWindowsService.DeviceConfig
|
|
{
|
|
DeviceId = deviceId,
|
|
Ip = machine.MachineIp.Trim(),
|
|
Port = port,
|
|
Username = creds.Username,
|
|
Password = creds.Password,
|
|
FingerPrintReaderNo = creds.FingerPrintReaderNo,
|
|
FaceReaderNo = creds.FaceReaderNo,
|
|
GatewayDoorIndex = creds.GatewayDoorIndex,
|
|
Model = string.IsNullOrWhiteSpace(machine.MachineName) ? (creds.Model ?? "") : machine.MachineName,
|
|
SerialNumber = creds.SerialNumber,
|
|
FirmwareVersion = creds.FirmwareVersion,
|
|
SubnetMask = creds.SubnetMask,
|
|
DefaultGateway = creds.DefaultGateway
|
|
};
|
|
return true;
|
|
}
|
|
|
|
private static bool IsAttendanceMachineActive(AttendanceMachineRow machine)
|
|
{
|
|
var status = (machine.MachineStatus ?? "").Trim();
|
|
return string.Equals(status, "active", StringComparison.OrdinalIgnoreCase) || status == "1";
|
|
}
|
|
|
|
private void LogTargetResolve(string requestedIp, bool found, string machineId, int port, string machineType, string note)
|
|
{
|
|
_logger.Ops("[TARGET_RESOLVE]",
|
|
"requestedIp=" + (requestedIp ?? "") +
|
|
" found=" + (found ? "true" : "false") +
|
|
" machineId=" + (machineId ?? "") +
|
|
" port=" + port +
|
|
" type=" + (machineType ?? "") +
|
|
(string.IsNullOrWhiteSpace(note) ? "" : (" note=\"" + note + "\"")));
|
|
}
|
|
|
|
/// <summary>
|
|
/// ISAPI HTTP connectivity probe (Digest) on the HTTP port — not ICMP ping.
|
|
/// Tries several endpoints because firmware/models omit some capabilities URLs (404 is not offline).
|
|
/// </summary>
|
|
private bool TryTestTargetIsapiConnectivity(
|
|
HikvisionAttendanceWindowsService.DeviceConfig device,
|
|
out string outcome,
|
|
out string detail)
|
|
{
|
|
outcome = "";
|
|
detail = "";
|
|
int isapiPort = IsapiPort;
|
|
var ip = (device.Ip ?? "").Trim();
|
|
var requestLabel = "GET ISAPI probe (" + TargetIsapiProbePaths.Length + " paths)";
|
|
|
|
var tcpProbe = ConnectivityDiagnostics.ProbeBeforeLogin(ip, isapiPort, tcpTimeoutMs: 5000, tryPing: false);
|
|
if (tcpProbe.TcpChecked && !tcpProbe.TcpOk)
|
|
{
|
|
outcome = "Connection Failed";
|
|
detail = tcpProbe.FriendlyReason;
|
|
LogTargetTest(requestLabel, outcome, detail);
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
using var handler = BuildDigestHandler(device);
|
|
using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(15) };
|
|
|
|
string lastNonAuthFailure = "";
|
|
var sawHttpResponse = false;
|
|
|
|
foreach (var path in TargetIsapiProbePaths)
|
|
{
|
|
var url = "http://" + ip + ":" + isapiPort + path;
|
|
using var response = client.GetAsync(url).GetAwaiter().GetResult();
|
|
sawHttpResponse = true;
|
|
|
|
var bodySnippet = "";
|
|
try
|
|
{
|
|
var body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
|
if (!string.IsNullOrWhiteSpace(body))
|
|
bodySnippet = body.Length > 120 ? body.Substring(0, 120) : body;
|
|
}
|
|
catch
|
|
{
|
|
// ignore body read errors
|
|
}
|
|
|
|
var pathLabel = "GET " + path;
|
|
if (response.StatusCode == HttpStatusCode.Unauthorized ||
|
|
response.StatusCode == HttpStatusCode.Forbidden)
|
|
{
|
|
outcome = "Authentication Failed";
|
|
detail = pathLabel + " → HTTP " + (int)response.StatusCode + " " + response.ReasonPhrase;
|
|
LogTargetTest(pathLabel, outcome, detail);
|
|
return false;
|
|
}
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
outcome = "OK";
|
|
detail = pathLabel + " → HTTP " + (int)response.StatusCode;
|
|
LogTargetTest(pathLabel, outcome, detail);
|
|
return true;
|
|
}
|
|
|
|
lastNonAuthFailure = pathLabel + " → HTTP " + (int)response.StatusCode + " " + response.ReasonPhrase +
|
|
(string.IsNullOrWhiteSpace(bodySnippet) ? "" : (" body=\"" + bodySnippet + "\""));
|
|
LogTargetTest(pathLabel, "probe_miss", lastNonAuthFailure);
|
|
}
|
|
|
|
// Device answered HTTP but probe URLs are unsupported on this firmware.
|
|
// Allow sync to proceed; real UserInfo / FaceDataRecord calls will report real errors.
|
|
if (sawHttpResponse)
|
|
{
|
|
outcome = "OK";
|
|
detail = "device reachable on HTTP " + isapiPort +
|
|
"; probe URLs unsupported (last: " + lastNonAuthFailure + ")";
|
|
LogTargetTest(requestLabel, outcome, detail);
|
|
return true;
|
|
}
|
|
|
|
outcome = "Connection Failed";
|
|
detail = string.IsNullOrWhiteSpace(lastNonAuthFailure)
|
|
? "No HTTP response from ISAPI probes."
|
|
: lastNonAuthFailure;
|
|
LogTargetTest(requestLabel, outcome, detail);
|
|
return false;
|
|
}
|
|
catch (TaskCanceledException ex)
|
|
{
|
|
outcome = "Connection Failed";
|
|
detail = "ISAPI request timed out: " + ex.Message;
|
|
LogTargetTest(requestLabel, outcome, detail);
|
|
return false;
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
outcome = "Connection Failed";
|
|
detail = ex.Message;
|
|
LogTargetTest(requestLabel, outcome, detail);
|
|
return false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
outcome = "Connection Failed";
|
|
detail = ex.GetType().Name + ": " + ex.Message;
|
|
LogTargetTest(requestLabel, outcome, detail);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void LogTargetTest(string request, string outcome, string detail)
|
|
{
|
|
_logger.Ops("[TARGET_TEST]",
|
|
"request=" + request +
|
|
" outcome=" + outcome +
|
|
(string.IsNullOrWhiteSpace(detail) ? "" : (" detail=\"" + detail + "\"")));
|
|
}
|
|
|
|
private void LogDbToDeviceResult(
|
|
string employeeNo,
|
|
string createUser,
|
|
string faceUpload,
|
|
string verification,
|
|
string finalResult)
|
|
{
|
|
_logger.Ops("[DB_TO_DEVICE]",
|
|
"employee=" + employeeNo +
|
|
" createUser=" + createUser +
|
|
" faceUpload=" + faceUpload +
|
|
" verification=" + verification +
|
|
" finalResult=" + finalResult);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves configured sync targets from DB (no scope filter), tests ISAPI connectivity, returns ready targets.
|
|
/// </summary>
|
|
private List<HikvisionAttendanceWindowsService.DeviceConfig> CollectTestedSyncTargets(
|
|
HikvisionAttendanceWindowsService.DeviceConfig? source,
|
|
bool hasSource,
|
|
CancellationToken ct)
|
|
{
|
|
var targets = new List<HikvisionAttendanceWindowsService.DeviceConfig>();
|
|
foreach (var targetIp in _config.TargetMachineIps ?? Enumerable.Empty<string>())
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
if (!TryResolveSyncTargetByIp(targetIp, out var device) || device == null)
|
|
continue;
|
|
|
|
if (targets.Any(x => string.Equals(x.Ip, device.Ip, StringComparison.OrdinalIgnoreCase)))
|
|
continue;
|
|
|
|
if (hasSource &&
|
|
string.Equals((device.Ip ?? "").Trim(), (source!.Ip ?? "").Trim(), StringComparison.OrdinalIgnoreCase) &&
|
|
!_config.EnableTemplateDbToDeviceSync)
|
|
{
|
|
_logger.Diag("user_sync",
|
|
"target skipped — same as source IP and DB->device restore disabled. TargetMachineIp=\"" + targetIp + "\".");
|
|
continue;
|
|
}
|
|
|
|
if (!TryTestTargetIsapiConnectivity(device, out var testOutcome, out var testDetail))
|
|
{
|
|
_logger.Warn("UserSync: target connectivity failed. TargetMachineIp=\"" + targetIp +
|
|
"\" machine_id=" + device.DeviceId + " outcome=\"" + testOutcome +
|
|
"\" detail=\"" + testDetail + "\".");
|
|
var failTitle = testOutcome == "Authentication Failed"
|
|
? "Authentication Failed."
|
|
: "Connection Failed.";
|
|
var failLines = new[]
|
|
{
|
|
"MACHINE " + (device.DeviceId ?? ""),
|
|
"",
|
|
failTitle,
|
|
"",
|
|
"Machine IP : " + (device.Ip ?? ""),
|
|
"ISAPI HTTP Port : " + IsapiPort,
|
|
"Detail : " + (string.IsNullOrWhiteSpace(testDetail) ? "(none)" : testDetail),
|
|
""
|
|
};
|
|
_logger.Biz(BizChannel.UserSync, failLines);
|
|
_logger.BizSeparator(BizChannel.UserSync);
|
|
if (_config.EnableInitialDepartmentSync)
|
|
{
|
|
_logger.Biz(BizChannel.DepartmentalSync, failLines);
|
|
_logger.BizSeparator(BizChannel.DepartmentalSync);
|
|
}
|
|
if (_config.EnableTemplateDbToDeviceSync)
|
|
{
|
|
_logger.Biz(BizChannel.Template,
|
|
"Target Device :",
|
|
"",
|
|
(device.Ip ?? ""),
|
|
"",
|
|
failTitle,
|
|
"",
|
|
"Detail : " + (string.IsNullOrWhiteSpace(testDetail) ? "(none)" : testDetail),
|
|
"");
|
|
_logger.BizSeparator(BizChannel.Template);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
targets.Add(device);
|
|
}
|
|
|
|
foreach (var tid in _config.TargetDeviceIds ?? Enumerable.Empty<string>())
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
if (!TryResolveSyncTargetByMachineId(tid, out var device) || device == null)
|
|
continue;
|
|
|
|
if (targets.Any(x =>
|
|
string.Equals(x.Ip, device.Ip, StringComparison.OrdinalIgnoreCase) ||
|
|
DeviceIdentity.CanonicalLookupKey(x.DeviceId) == DeviceIdentity.CanonicalLookupKey(device.DeviceId)))
|
|
continue;
|
|
|
|
if (hasSource &&
|
|
DeviceIdentity.CanonicalLookupKey(device.DeviceId) == DeviceIdentity.CanonicalLookupKey(source!.DeviceId))
|
|
{
|
|
if (!_config.EnableTemplateDbToDeviceSync)
|
|
{
|
|
_logger.Warn("UserSync: target skipped — same as source. deviceId=\"" + device.DeviceId + "\".");
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!TryTestTargetIsapiConnectivity(device, out var testOutcome, out var testDetail))
|
|
{
|
|
_logger.Warn("UserSync: target connectivity failed. TargetDeviceId=\"" + tid +
|
|
"\" machine_id=" + device.DeviceId + " outcome=\"" + testOutcome +
|
|
"\" detail=\"" + testDetail + "\".");
|
|
continue;
|
|
}
|
|
|
|
targets.Add(device);
|
|
}
|
|
|
|
return targets;
|
|
}
|
|
}
|