using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text;
namespace HikvisionAttendanceService;
/// Canonical markers for the operational timeline.
internal static class OpsMarkers
{
public const string Service = "SERVICE";
public const string Scope = "SCOPE";
public const string Connectivity = "CONNECTIVITY";
public const string Attendance = "ATTENDANCE";
public const string UserDelete = "USER_DELETE";
public const string TemplateDeviceToDb = "TEMPLATE_DEVICE_TO_DB";
public const string TemplateDbToDevice = "TEMPLATE_DB_TO_DEVICE";
}
internal enum ConnectivityFailureStage
{
None,
InvalidIp,
PingFailed,
TcpPortClosed,
SdkLoginTimeout,
InvalidCredentials,
SdkRejected,
Unknown
}
internal sealed class ConnectivityProbeResult
{
public bool Reachable { get; set; }
public bool PingChecked { get; set; }
public bool PingOk { get; set; }
public bool TcpChecked { get; set; }
public bool TcpOk { get; set; }
public ConnectivityFailureStage FailureStage { get; set; } = ConnectivityFailureStage.None;
public string FriendlyReason { get; set; } = "";
public uint SdkErrorCode { get; set; }
}
/// Tracks unreachable devices and writes a human-readable unreachable file (no SDK spam).
internal sealed class UnreachableDeviceTracker
{
private readonly ConcurrentDictionary _states =
new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
private readonly HikvisionAttendanceWindowsService.FileLogger _logger;
private int _headerWritten;
public UnreachableDeviceTracker(HikvisionAttendanceWindowsService.FileLogger logger)
{
_logger = logger;
}
public void ReportFailure(
string machineId,
string machineName,
string ip,
int port,
ConnectivityFailureStage stage,
string? friendlyReason)
{
var key = BuildKey(machineId, ip);
var reason = BizFriendlyReasons.FromStage(stage, friendlyReason);
var isNew = false;
var state = _states.AddOrUpdate(
key,
_ =>
{
isNew = true;
return new UnreachableState
{
MachineId = machineId ?? "",
MachineName = machineName ?? "",
Ip = ip ?? "",
Port = port,
Stage = stage,
Reason = reason
};
},
(_, existing) =>
{
existing.Stage = stage;
existing.Reason = reason;
if (!string.IsNullOrWhiteSpace(machineName))
existing.MachineName = machineName;
return existing;
});
if (!isNew)
return; // avoid duplicate spam for the same machine in one run
if (System.Threading.Interlocked.Exchange(ref _headerWritten, 1) == 0)
{
_logger.Biz(BizChannel.Unreachable,
"UNREACHABLE MACHINES",
"");
_logger.BizSeparator(BizChannel.Unreachable);
}
var siteLabel = "SITE " + (string.IsNullOrWhiteSpace(state.MachineId) ? "?" : state.MachineId);
var name = string.IsNullOrWhiteSpace(state.MachineName) ? "(unnamed terminal)" : state.MachineName;
_logger.Biz(BizChannel.Unreachable,
siteLabel,
"",
name,
"",
state.Ip,
"",
"Reason :",
"",
reason,
"");
_logger.BizSeparator(BizChannel.Unreachable);
_logger.OpsWarn(OpsMarkers.Connectivity,
"UNREACHABLE machine_id=" + state.MachineId + " ip=" + state.Ip + " port=" + port +
" stage=" + stage + " reason=\"" + reason + "\"");
}
public void ReportRecovered(string machineId, string machineName, string ip, int port)
{
var key = BuildKey(machineId, ip);
if (!_states.TryRemove(key, out var state))
return;
_logger.Biz(BizChannel.Unreachable,
"RECOVERED",
"",
"SITE " + (string.IsNullOrWhiteSpace(machineId) ? state.MachineId : machineId),
"",
string.IsNullOrWhiteSpace(machineName) ? state.MachineName : machineName,
"",
ip ?? state.Ip,
"",
"Reason :",
"",
"Device connectivity restored.",
"");
_logger.BizSeparator(BizChannel.Unreachable);
_logger.Ops(OpsMarkers.Connectivity,
"RECOVERED machine_id=" + (machineId ?? state.MachineId) + " ip=" + (ip ?? state.Ip) + " port=" + port);
}
private static string BuildKey(string? machineId, string? ip) =>
((machineId ?? "").Trim() + "|" + (ip ?? "").Trim()).ToLowerInvariant();
private sealed class UnreachableState
{
public string MachineId = "";
public string MachineName = "";
public string Ip = "";
public int Port;
public ConnectivityFailureStage Stage;
public string Reason = "";
}
}
/// Pre-login IP / optional ping / TCP diagnostics (does not claim ping unless checked).
internal static class ConnectivityDiagnostics
{
public static ConnectivityProbeResult ProbeBeforeLogin(string? ip, int port, int tcpTimeoutMs = 3000, bool tryPing = true)
{
var result = new ConnectivityProbeResult();
var rawIp = (ip ?? "").Trim();
if (string.IsNullOrWhiteSpace(rawIp) ||
!IPAddress.TryParse(rawIp, out var address) ||
address.Equals(IPAddress.None) ||
address.Equals(IPAddress.Any))
{
result.Reachable = false;
result.FailureStage = ConnectivityFailureStage.InvalidIp;
result.FriendlyReason = "Invalid IP address \"" + rawIp + "\".";
return result;
}
if (port <= 0 || port > 65535)
{
result.Reachable = false;
result.FailureStage = ConnectivityFailureStage.TcpPortClosed;
result.FriendlyReason = "Invalid TCP port " + port + ".";
return result;
}
if (tryPing)
{
result.PingChecked = true;
try
{
using var ping = new Ping();
var reply = ping.Send(address, 1500);
result.PingOk = reply != null && reply.Status == IPStatus.Success;
if (!result.PingOk)
{
result.Reachable = false;
result.FailureStage = ConnectivityFailureStage.PingFailed;
result.FriendlyReason = "Ping failed (" + (reply?.Status.ToString() ?? "no reply") +
"). Host did not respond to ICMP.";
// Still attempt TCP — some networks block ICMP but allow SDK port.
}
}
catch
{
// Ping not available / blocked by OS policy — do not report ping failure.
result.PingChecked = false;
result.PingOk = false;
}
}
result.TcpChecked = true;
try
{
using var client = new TcpClient();
var ar = client.BeginConnect(address, port, null, null);
bool ok = ar.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(Math.Max(500, tcpTimeoutMs)));
if (!ok)
{
try { client.Close(); } catch { /* ignore */ }
result.TcpOk = false;
result.Reachable = false;
result.FailureStage = ConnectivityFailureStage.TcpPortClosed;
result.FriendlyReason = "TCP port " + port + " is closed or blocked (connect timed out).";
return result;
}
client.EndConnect(ar);
result.TcpOk = client.Connected;
}
catch (Exception ex)
{
result.TcpOk = false;
result.Reachable = false;
result.FailureStage = ConnectivityFailureStage.TcpPortClosed;
result.FriendlyReason = "TCP port " + port + " is closed or blocked (" + ex.GetType().Name + ").";
return result;
}
if (!result.TcpOk)
{
result.Reachable = false;
result.FailureStage = ConnectivityFailureStage.TcpPortClosed;
result.FriendlyReason = "TCP port " + port + " is closed or blocked.";
return result;
}
// TCP open — login may still fail (credentials / SDK). Mark provisional reachable until login.
result.Reachable = true;
result.FailureStage = ConnectivityFailureStage.None;
result.FriendlyReason = "TCP port " + port + " is open.";
return result;
}
public static ConnectivityProbeResult ClassifySdkLoginFailure(uint sdkError, ConnectivityProbeResult? prior)
{
var result = prior ?? new ConnectivityProbeResult();
result.Reachable = false;
result.SdkErrorCode = sdkError;
switch (sdkError)
{
case 1:
case 5:
result.FailureStage = ConnectivityFailureStage.InvalidCredentials;
result.FriendlyReason = "Invalid credentials (SDK error " + sdkError + ").";
break;
case 41:
case 109:
result.FailureStage = ConnectivityFailureStage.SdkLoginTimeout;
result.FriendlyReason = "SDK login timed out (SDK error " + sdkError + ").";
break;
case 7:
result.FailureStage = ConnectivityFailureStage.TcpPortClosed;
result.FriendlyReason = "SDK connection failed — device unreachable on the SDK port (SDK error 7).";
break;
default:
result.FailureStage = ConnectivityFailureStage.SdkRejected;
result.FriendlyReason = "ISAPI/SDK rejected login (SDK error " + sdkError + ").";
break;
}
return result;
}
public static string FormatReachableLine(
string machineId,
string machineName,
string ip,
int port,
ConnectivityProbeResult probe,
bool loginOk)
{
var reachable = loginOk ? "TRUE" : "FALSE";
var sb = new StringBuilder();
sb.Append("machine_id=").Append(machineId ?? "");
sb.Append(" name=\"").Append(machineName ?? "").Append("\"");
sb.Append(" ip=").Append(ip ?? "");
sb.Append(" port=").Append(port);
sb.Append(" Reachable=").Append(reachable);
if (probe.PingChecked)
sb.Append(" ping=").Append(probe.PingOk ? "OK" : "FAIL");
if (probe.TcpChecked)
sb.Append(" tcp=").Append(probe.TcpOk ? "OPEN" : "CLOSED");
if (!loginOk)
sb.Append(" stage=").Append(probe.FailureStage).Append(" reason=\"").Append(probe.FriendlyReason).Append("\"");
else
sb.Append(" reason=\"SDK login succeeded\"");
return sb.ToString();
}
}
internal enum AttendancePersistOutcome
{
Duplicate,
SkippedNoEmployee,
UnmappedEmployee,
UnsupportedWorkerType,
DbInserted,
DbFailed,
PersistedWithoutDb
}
internal sealed class AttendanceDeviceCycleStats
{
public string DeviceId = "";
public string DeviceIp = "";
public int EventsReceived;
public int ValidPunches;
public int SystemEventsSkipped;
public int DbInserted;
public int Duplicates;
public int Failed;
public bool LastSyncUpdated;
public string CleanupResult = "SKIPPED";
public readonly System.Collections.Generic.List<(string Ip, string EmpNo, DateTime Time)> InsertedPunches =
new System.Collections.Generic.List<(string, string, DateTime)>();
}