Wire two-tier FileLogger channels and template sync direction flags

Extend FileLogger with Biz/Ops/Diag writers and service start/stop blocks; add EnableTemplateDeviceToDbSync / EnableTemplateDbToDeviceSync so only the enabled template direction is logged and run.
main
SYED MUSTUFA AHMED NAQVI 2026-07-30 13:18:17 +05:00
parent f783537917
commit 672fa2a89d
1 changed files with 185 additions and 42 deletions

View File

@ -39,6 +39,8 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
var config = HikvisionServiceConfig.Load(configPath); var config = HikvisionServiceConfig.Load(configPath);
var logger = new FileLogger(config.LogDirectory); var logger = new FileLogger(config.LogDirectory);
logger.Ops(OpsMarkers.Service, "WINDOWS SERVICE OnStart time=" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") +
" config=\"" + configPath + "\"");
_manager = new HikvisionAttendanceManager(config, logger); _manager = new HikvisionAttendanceManager(config, logger);
_mainTask = _manager.RunAsync(_cts.Token); _mainTask = _manager.RunAsync(_cts.Token);
@ -93,6 +95,11 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
[DataMember] [DataMember]
public bool EnableTemplateDbPersistence { get; set; } public bool EnableTemplateDbPersistence { get; set; }
/// <summary>When true, pull face templates from device and save to DB (DEVICE -&gt; DATABASE). Prefer only one template direction enabled at a time.</summary>
[DataMember]
public bool EnableTemplateDeviceToDbSync { get; set; }
/// <summary>When true, push face templates from DB to device (DATABASE -&gt; DEVICE). Prefer only one template direction enabled at a time.</summary>
[DataMember] [DataMember]
public bool EnableTemplateDbToDeviceSync { get; set; } public bool EnableTemplateDbToDeviceSync { get; set; }
@ -193,14 +200,29 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
[DataMember] [DataMember]
public int TemplateFetchIntervalHours { get; set; } = 4; public int TemplateFetchIntervalHours { get; set; } = 4;
[DataMember]
public string MachineScopeMode { get; set; } = "CENTRAL";
[DataMember]
public List<string> ScopedMachineIps { get; set; } = new List<string>();
/// <summary>DeviceId (from <see cref="DeviceConfig.DeviceId"/>) to treat as the authoritative user source.</summary> /// <summary>DeviceId (from <see cref="DeviceConfig.DeviceId"/>) to treat as the authoritative user source.</summary>
[DataMember] [DataMember]
public string SourceDeviceId { get; set; } = ""; public string SourceDeviceId { get; set; } = "";
[DataMember]
public string SourceMachineIp { get; set; } = "";
/// <summary>DeviceIds to push missing users/faces onto.</summary> /// <summary>DeviceIds to push missing users/faces onto.</summary>
[DataMember] [DataMember]
public List<string> TargetDeviceIds { get; set; } = new List<string>(); public List<string> TargetDeviceIds { get; set; } = new List<string>();
[DataMember]
public List<string> TargetMachineIps { get; set; } = new List<string>();
[DataMember]
public List<string> SyncEmployeeIds { get; set; } = new List<string>();
/// <summary>Optional directory to persist downloaded source face images for auditing or re-upload. Empty = skip file save.</summary> /// <summary>Optional directory to persist downloaded source face images for auditing or re-upload. Empty = skip file save.</summary>
[DataMember] [DataMember]
public string UserSyncFaceCacheDirectory { get; set; } = ""; public string UserSyncFaceCacheDirectory { get; set; } = "";
@ -224,6 +246,7 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
PreferDbMachinesOverConfig = false, PreferDbMachinesOverConfig = false,
EnableAttendanceDbPersistence = false, EnableAttendanceDbPersistence = false,
EnableTemplateDbPersistence = false, EnableTemplateDbPersistence = false,
EnableTemplateDeviceToDbSync = false,
EnableTemplateDbToDeviceSync = false, EnableTemplateDbToDeviceSync = false,
KeepAttendanceFileExport = true, KeepAttendanceFileExport = true,
KeepTemplateFiles = true, KeepTemplateFiles = true,
@ -249,10 +272,15 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
IsapiHttpPort = 8000, IsapiHttpPort = 8000,
EnableTemplateFetch = false, EnableTemplateFetch = false,
TemplateFetchIntervalHours = 4, TemplateFetchIntervalHours = 4,
MachineScopeMode = "CENTRAL",
ScopedMachineIps = new List<string>(),
EnableUserSync = false, EnableUserSync = false,
SyncIntervalMinutes = 60, SyncIntervalMinutes = 60,
SourceDeviceId = "", SourceDeviceId = "",
SourceMachineIp = "",
TargetDeviceIds = new List<string>(), TargetDeviceIds = new List<string>(),
TargetMachineIps = new List<string>(),
SyncEmployeeIds = new List<string>(),
UserSyncFaceCacheDirectory = "", UserSyncFaceCacheDirectory = "",
SyncPolicies = new UserSyncPoliciesConfig() SyncPolicies = new UserSyncPoliciesConfig()
}; };
@ -296,6 +324,7 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
PreferDbMachinesOverConfig = false, PreferDbMachinesOverConfig = false,
EnableAttendanceDbPersistence = false, EnableAttendanceDbPersistence = false,
EnableTemplateDbPersistence = false, EnableTemplateDbPersistence = false,
EnableTemplateDeviceToDbSync = false,
EnableTemplateDbToDeviceSync = false, EnableTemplateDbToDeviceSync = false,
KeepAttendanceFileExport = true, KeepAttendanceFileExport = true,
KeepTemplateFiles = true, KeepTemplateFiles = true,
@ -321,10 +350,15 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
IsapiHttpPort = 80, IsapiHttpPort = 80,
EnableTemplateFetch = false, EnableTemplateFetch = false,
TemplateFetchIntervalHours = 4, TemplateFetchIntervalHours = 4,
MachineScopeMode = "CENTRAL",
ScopedMachineIps = new List<string>(),
EnableUserSync = false, EnableUserSync = false,
SyncIntervalMinutes = 60, SyncIntervalMinutes = 60,
SourceDeviceId = "", SourceDeviceId = "",
SourceMachineIp = "",
TargetDeviceIds = new List<string>(), TargetDeviceIds = new List<string>(),
TargetMachineIps = new List<string>(),
SyncEmployeeIds = new List<string>(),
UserSyncFaceCacheDirectory = "", UserSyncFaceCacheDirectory = "",
SyncPolicies = new UserSyncPoliciesConfig() SyncPolicies = new UserSyncPoliciesConfig()
}; };
@ -452,11 +486,18 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
if (cfg == null) if (cfg == null)
return; return;
cfg.MachineScopeMode = string.IsNullOrWhiteSpace(cfg.MachineScopeMode)
? "CENTRAL"
: cfg.MachineScopeMode.Trim().ToUpperInvariant();
cfg.SourceDeviceId = DeviceIdentity.NormalizeConfigured(cfg.SourceDeviceId); cfg.SourceDeviceId = DeviceIdentity.NormalizeConfigured(cfg.SourceDeviceId);
cfg.SourceMachineIp = string.IsNullOrWhiteSpace(cfg.SourceMachineIp) ? "" : cfg.SourceMachineIp.Trim();
cfg.UserSyncFaceCacheDirectory = string.IsNullOrWhiteSpace(cfg.UserSyncFaceCacheDirectory) cfg.UserSyncFaceCacheDirectory = string.IsNullOrWhiteSpace(cfg.UserSyncFaceCacheDirectory)
? "" ? ""
: cfg.UserSyncFaceCacheDirectory.Trim(); : cfg.UserSyncFaceCacheDirectory.Trim();
cfg.SyncPolicies ??= new UserSyncPoliciesConfig(); cfg.SyncPolicies ??= new UserSyncPoliciesConfig();
cfg.ScopedMachineIps ??= new List<string>();
for (int i = 0; i < cfg.ScopedMachineIps.Count; i++)
cfg.ScopedMachineIps[i] = (cfg.ScopedMachineIps[i] ?? "").Trim();
if (cfg.TargetDeviceIds == null) if (cfg.TargetDeviceIds == null)
cfg.TargetDeviceIds = new List<string>(); cfg.TargetDeviceIds = new List<string>();
else else
@ -464,6 +505,12 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
for (int i = 0; i < cfg.TargetDeviceIds.Count; i++) for (int i = 0; i < cfg.TargetDeviceIds.Count; i++)
cfg.TargetDeviceIds[i] = DeviceIdentity.NormalizeConfigured(cfg.TargetDeviceIds[i]); cfg.TargetDeviceIds[i] = DeviceIdentity.NormalizeConfigured(cfg.TargetDeviceIds[i]);
} }
cfg.TargetMachineIps ??= new List<string>();
for (int i = 0; i < cfg.TargetMachineIps.Count; i++)
cfg.TargetMachineIps[i] = (cfg.TargetMachineIps[i] ?? "").Trim();
cfg.SyncEmployeeIds ??= new List<string>();
for (int i = 0; i < cfg.SyncEmployeeIds.Count; i++)
cfg.SyncEmployeeIds[i] = (cfg.SyncEmployeeIds[i] ?? "").Trim();
if (cfg.Devices == null) if (cfg.Devices == null)
return; return;
@ -556,10 +603,12 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
{ {
private readonly string _directory; private readonly string _directory;
private readonly string _internalLogsDirectory; private readonly string _internalLogsDirectory;
private readonly string _attendanceJobLogsDirectory; private readonly string _attendanceBizDirectory;
private readonly string _templateJobLogsDirectory; private readonly string _templateBizDirectory;
private readonly string _userSyncJobLogsDirectory; private readonly string _userSyncBizDirectory;
private readonly string _unreachableBizDirectory;
private readonly object _sync = new(); private readonly object _sync = new();
public readonly BizSessionTotals Totals = new BizSessionTotals();
public FileLogger(string directory) public FileLogger(string directory)
{ {
@ -567,69 +616,163 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
Directory.CreateDirectory(_directory); Directory.CreateDirectory(_directory);
var logsRoot = Path.Combine(_directory, "logs"); var logsRoot = Path.Combine(_directory, "logs");
_internalLogsDirectory = Path.Combine(_directory, "internal_logs"); _internalLogsDirectory = Path.Combine(_directory, "internal_logs");
_attendanceJobLogsDirectory = Path.Combine(logsRoot, "attendance_logs"); _attendanceBizDirectory = Path.Combine(logsRoot, "attendance_logs");
_templateJobLogsDirectory = Path.Combine(logsRoot, "template_fetching_logs"); _templateBizDirectory = Path.Combine(logsRoot, "template_fetching_logs");
_userSyncJobLogsDirectory = Path.Combine(logsRoot, "user_sync_logs"); _userSyncBizDirectory = Path.Combine(logsRoot, "user_sync_logs");
_unreachableBizDirectory = Path.Combine(_internalLogsDirectory, "unreachable_devices");
Directory.CreateDirectory(_internalLogsDirectory); Directory.CreateDirectory(_internalLogsDirectory);
Directory.CreateDirectory(_attendanceJobLogsDirectory); Directory.CreateDirectory(_attendanceBizDirectory);
Directory.CreateDirectory(_templateJobLogsDirectory); Directory.CreateDirectory(_templateBizDirectory);
Directory.CreateDirectory(_userSyncJobLogsDirectory); Directory.CreateDirectory(_userSyncBizDirectory);
Directory.CreateDirectory(_unreachableBizDirectory);
} }
public void Info(string message) => Write("INFO", message); public string LogDirectory => _directory;
public void Warn(string message) => Write("WARN", message);
public void Error(string message, Exception? ex = null) => Write("ERROR", message + (ex is null ? "" : $" | {ex}"));
public void JobInfo(string jobName, string message) => WriteJob(jobName, "INFO", message);
public void JobWarn(string jobName, string message) => WriteJob(jobName, "WARN", message);
public void JobError(string jobName, string message) => WriteJob(jobName, "ERROR", message);
private void Write(string level, string message) public void Info(string message) => WriteInternal("INFO", message);
public void Warn(string message) => WriteInternal("WARN", message);
public void Error(string message, Exception? ex = null) =>
WriteInternal("ERROR", message + (ex is null ? "" : $" | {ex}"));
/// <summary>Technical diagnostics only — never written to business logs/.</summary>
public void JobInfo(string jobName, string message) => WriteInternalDiag(jobName, "INFO", message);
public void JobWarn(string jobName, string message) => WriteInternalDiag(jobName, "WARN", message);
public void JobError(string jobName, string message) => WriteInternalDiag(jobName, "ERROR", message);
public void Diag(string jobName, string message) => WriteInternalDiag(jobName, "INFO", message);
public void Ops(string marker, string message) =>
WriteInternal("INFO", "[" + (marker ?? "OPS") + "] " + (message ?? ""));
public void OpsWarn(string marker, string message) =>
WriteInternal("WARN", "[" + (marker ?? "OPS") + "] " + (message ?? ""));
public void OpsError(string marker, string message) =>
WriteInternal("ERROR", "[" + (marker ?? "OPS") + "] " + (message ?? ""));
public void Banner(params string[] lines)
{ {
var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{level}] {message}{Environment.NewLine}"; if (lines == null || lines.Length == 0)
var logName = "internal_logs_" + DateTime.Now.ToString("yyyy-MM-dd") + ".txt"; return;
var path = Path.Combine(_internalLogsDirectory, logName);
lock (_sync) lock (_sync)
{ {
File.AppendAllText(path, line); var sb = new StringBuilder();
var stamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
sb.Append(stamp).Append(" [INFO] ============================================================").Append(Environment.NewLine);
foreach (var line in lines)
sb.Append(stamp).Append(" [INFO] ").Append(line ?? "").Append(Environment.NewLine);
sb.Append(stamp).Append(" [INFO] ============================================================").Append(Environment.NewLine);
File.AppendAllText(InternalLogPath(), sb.ToString());
} }
} }
private void WriteJob(string jobName, string level, string message) // ---- Business / operations logs (human-readable, no SDK) ----
public void Biz(BizChannel channel, params string[] lines)
{ {
string dir; if (lines == null || lines.Length == 0)
string prefix; return;
switch ((jobName ?? "").Trim().ToLowerInvariant()) var sb = new StringBuilder();
foreach (var line in lines)
sb.AppendLine(line ?? "");
AppendBiz(channel, sb.ToString());
}
public void BizBlank(BizChannel channel) => AppendBiz(channel, Environment.NewLine);
public void BizSeparator(BizChannel channel) =>
AppendBiz(channel, "------------------------------------------------------------" + Environment.NewLine + Environment.NewLine);
public void WriteBizServiceStarted()
{
Totals.StartedAt = DateTime.Now;
var block =
"------------------------------------------------------------" + Environment.NewLine +
"Service started at " + BizFriendlyReasons.FormatTime(Totals.StartedAt) + Environment.NewLine +
"------------------------------------------------------------" + Environment.NewLine +
Environment.NewLine;
AppendBiz(BizChannel.Attendance, block);
AppendBiz(BizChannel.UserSync, block);
AppendBiz(BizChannel.Template, block);
AppendBiz(BizChannel.Unreachable, block);
}
public void WriteBizServiceStopped()
{
var stopped =
Environment.NewLine +
"------------------------------------------------------------" + Environment.NewLine +
"Service stopped at " + BizFriendlyReasons.FormatTime(DateTime.Now) + Environment.NewLine +
Environment.NewLine +
Totals.FormatSummaryBlock() +
Environment.NewLine;
AppendBiz(BizChannel.Attendance, stopped);
AppendBiz(BizChannel.UserSync, stopped);
AppendBiz(BizChannel.Template, stopped);
AppendBiz(BizChannel.Unreachable, stopped);
}
public void WriteBizDatabaseFailed(string mysqlErrorCode, string friendlyReason)
{
Biz(BizChannel.Attendance,
"Database connection failed.",
"",
"Reason :",
"",
string.IsNullOrWhiteSpace(mysqlErrorCode) ? "[MySQL Error]" : "[MySQL Error " + mysqlErrorCode + "]",
"",
string.IsNullOrWhiteSpace(friendlyReason) ? "Unable to connect to MySQL server." : friendlyReason,
"",
"Attendance synchronization cannot continue.",
"");
}
private void AppendBiz(BizChannel channel, string text)
{
string path;
switch (channel)
{ {
case "attendance": case BizChannel.Attendance:
case "attendance_logs": path = Path.Combine(_attendanceBizDirectory, "attendance_logs_" + DateStamp() + ".txt");
dir = _attendanceJobLogsDirectory;
prefix = "attendance_logs_";
break; break;
case "template": case BizChannel.UserSync:
case "template_fetch": path = Path.Combine(_userSyncBizDirectory, "user_sync_logs_" + DateStamp() + ".txt");
case "template_fetching_logs":
dir = _templateJobLogsDirectory;
prefix = "template_fetching_logs_";
break; break;
case "user_sync": case BizChannel.Template:
case "user_sync_logs": path = Path.Combine(_templateBizDirectory, "template_fetching_logs_" + DateStamp() + ".txt");
dir = _userSyncJobLogsDirectory; break;
prefix = "user_sync_logs_"; case BizChannel.Unreachable:
path = Path.Combine(_unreachableBizDirectory, "unreachable_devices_" + DateStamp() + ".txt");
break; break;
default: default:
dir = _internalLogsDirectory; path = InternalLogPath();
prefix = "internal_logs_";
break; break;
} }
var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{level}] {message}{Environment.NewLine}";
var path = Path.Combine(dir, prefix + DateTime.Now.ToString("yyyy-MM-dd") + ".txt");
lock (_sync) lock (_sync)
{ {
File.AppendAllText(path, line); File.AppendAllText(path, text, Encoding.UTF8);
} }
} }
private static string DateStamp() => DateTime.Now.ToString("yyyy-MM-dd");
private string InternalLogPath() =>
Path.Combine(_internalLogsDirectory, "internal_logs_" + DateStamp() + ".txt");
private void WriteInternal(string level, string message)
{
var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{level}] {message}{Environment.NewLine}";
lock (_sync)
{
File.AppendAllText(InternalLogPath(), line);
}
}
private void WriteInternalDiag(string jobName, string level, string message)
{
var tag = (jobName ?? "diag").Trim().ToUpperInvariant();
if (tag.Length == 0) tag = "DIAG";
WriteInternal(level, "[DIAG][" + tag + "] " + message);
}
public void Dispose() { } public void Dispose() { }
} }