using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using HikvisionAttendanceService.Data;
namespace HikvisionAttendanceService;
///
/// Resolves sync targets from the attendance_machine repository without site-scope filtering.
/// Source-device selection via is unchanged.
///
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();
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 + "\"")));
}
///
/// 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).
///
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);
}
///
/// Resolves configured sync targets from DB (no scope filter), tests ISAPI connectivity, returns ready targets.
///
private List CollectTestedSyncTargets(
HikvisionAttendanceWindowsService.DeviceConfig? source,
bool hasSource,
CancellationToken ct)
{
var targets = new List();
foreach (var targetIp in _config.TargetMachineIps ?? Enumerable.Empty())
{
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())
{
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;
}
}