Add ops markers and unreachable-device tracking for internal logs
Add structured [SERVICE] / [ATTENDANCE] / [USER_DELETE] / template markers and unreachable-device reporting used by internal diagnostic summariesmain
parent
e9438ac1dc
commit
f783537917
|
|
@ -0,0 +1,338 @@
|
|||
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;
|
||||
|
||||
/// <summary>Canonical markers for the operational timeline.</summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>Tracks unreachable devices and writes a human-readable unreachable file (no SDK spam).</summary>
|
||||
internal sealed class UnreachableDeviceTracker
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, UnreachableState> _states =
|
||||
new ConcurrentDictionary<string, UnreachableState>(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 = "";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Pre-login IP / optional ping / TCP diagnostics (does not claim ping unless checked).</summary>
|
||||
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,
|
||||
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)>();
|
||||
}
|
||||
Loading…
Reference in New Issue