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 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);
_mainTask = _manager.RunAsync(_cts.Token);
@ -93,6 +95,11 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
[DataMember]
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]
public bool EnableTemplateDbToDeviceSync { get; set; }
@ -193,14 +200,29 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
[DataMember]
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>
[DataMember]
public string SourceDeviceId { get; set; } = "";
[DataMember]
public string SourceMachineIp { get; set; } = "";
/// <summary>DeviceIds to push missing users/faces onto.</summary>
[DataMember]
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>
[DataMember]
public string UserSyncFaceCacheDirectory { get; set; } = "";
@ -224,6 +246,7 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
PreferDbMachinesOverConfig = false,
EnableAttendanceDbPersistence = false,
EnableTemplateDbPersistence = false,
EnableTemplateDeviceToDbSync = false,
EnableTemplateDbToDeviceSync = false,
KeepAttendanceFileExport = true,
KeepTemplateFiles = true,
@ -249,10 +272,15 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
IsapiHttpPort = 8000,
EnableTemplateFetch = false,
TemplateFetchIntervalHours = 4,
MachineScopeMode = "CENTRAL",
ScopedMachineIps = new List<string>(),
EnableUserSync = false,
SyncIntervalMinutes = 60,
SourceDeviceId = "",
SourceMachineIp = "",
TargetDeviceIds = new List<string>(),
TargetMachineIps = new List<string>(),
SyncEmployeeIds = new List<string>(),
UserSyncFaceCacheDirectory = "",
SyncPolicies = new UserSyncPoliciesConfig()
};
@ -296,6 +324,7 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
PreferDbMachinesOverConfig = false,
EnableAttendanceDbPersistence = false,
EnableTemplateDbPersistence = false,
EnableTemplateDeviceToDbSync = false,
EnableTemplateDbToDeviceSync = false,
KeepAttendanceFileExport = true,
KeepTemplateFiles = true,
@ -321,10 +350,15 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
IsapiHttpPort = 80,
EnableTemplateFetch = false,
TemplateFetchIntervalHours = 4,
MachineScopeMode = "CENTRAL",
ScopedMachineIps = new List<string>(),
EnableUserSync = false,
SyncIntervalMinutes = 60,
SourceDeviceId = "",
SourceMachineIp = "",
TargetDeviceIds = new List<string>(),
TargetMachineIps = new List<string>(),
SyncEmployeeIds = new List<string>(),
UserSyncFaceCacheDirectory = "",
SyncPolicies = new UserSyncPoliciesConfig()
};
@ -452,11 +486,18 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
if (cfg == null)
return;
cfg.MachineScopeMode = string.IsNullOrWhiteSpace(cfg.MachineScopeMode)
? "CENTRAL"
: cfg.MachineScopeMode.Trim().ToUpperInvariant();
cfg.SourceDeviceId = DeviceIdentity.NormalizeConfigured(cfg.SourceDeviceId);
cfg.SourceMachineIp = string.IsNullOrWhiteSpace(cfg.SourceMachineIp) ? "" : cfg.SourceMachineIp.Trim();
cfg.UserSyncFaceCacheDirectory = string.IsNullOrWhiteSpace(cfg.UserSyncFaceCacheDirectory)
? ""
: cfg.UserSyncFaceCacheDirectory.Trim();
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)
cfg.TargetDeviceIds = new List<string>();
else
@ -464,6 +505,12 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
for (int i = 0; i < cfg.TargetDeviceIds.Count; 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)
return;
@ -556,10 +603,12 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
{
private readonly string _directory;
private readonly string _internalLogsDirectory;
private readonly string _attendanceJobLogsDirectory;
private readonly string _templateJobLogsDirectory;
private readonly string _userSyncJobLogsDirectory;
private readonly string _attendanceBizDirectory;
private readonly string _templateBizDirectory;
private readonly string _userSyncBizDirectory;
private readonly string _unreachableBizDirectory;
private readonly object _sync = new();
public readonly BizSessionTotals Totals = new BizSessionTotals();
public FileLogger(string directory)
{
@ -567,69 +616,163 @@ public sealed class HikvisionAttendanceWindowsService : ServiceBase
Directory.CreateDirectory(_directory);
var logsRoot = Path.Combine(_directory, "logs");
_internalLogsDirectory = Path.Combine(_directory, "internal_logs");
_attendanceJobLogsDirectory = Path.Combine(logsRoot, "attendance_logs");
_templateJobLogsDirectory = Path.Combine(logsRoot, "template_fetching_logs");
_userSyncJobLogsDirectory = Path.Combine(logsRoot, "user_sync_logs");
_attendanceBizDirectory = Path.Combine(logsRoot, "attendance_logs");
_templateBizDirectory = Path.Combine(logsRoot, "template_fetching_logs");
_userSyncBizDirectory = Path.Combine(logsRoot, "user_sync_logs");
_unreachableBizDirectory = Path.Combine(_internalLogsDirectory, "unreachable_devices");
Directory.CreateDirectory(_internalLogsDirectory);
Directory.CreateDirectory(_attendanceJobLogsDirectory);
Directory.CreateDirectory(_templateJobLogsDirectory);
Directory.CreateDirectory(_userSyncJobLogsDirectory);
Directory.CreateDirectory(_attendanceBizDirectory);
Directory.CreateDirectory(_templateBizDirectory);
Directory.CreateDirectory(_userSyncBizDirectory);
Directory.CreateDirectory(_unreachableBizDirectory);
}
public void Info(string message) => Write("INFO", message);
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);
public string LogDirectory => _directory;
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}";
var logName = "internal_logs_" + DateTime.Now.ToString("yyyy-MM-dd") + ".txt";
var path = Path.Combine(_internalLogsDirectory, logName);
if (lines == null || lines.Length == 0)
return;
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;
string prefix;
switch ((jobName ?? "").Trim().ToLowerInvariant())
if (lines == null || lines.Length == 0)
return;
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 "attendance_logs":
dir = _attendanceJobLogsDirectory;
prefix = "attendance_logs_";
case BizChannel.Attendance:
path = Path.Combine(_attendanceBizDirectory, "attendance_logs_" + DateStamp() + ".txt");
break;
case "template":
case "template_fetch":
case "template_fetching_logs":
dir = _templateJobLogsDirectory;
prefix = "template_fetching_logs_";
case BizChannel.UserSync:
path = Path.Combine(_userSyncBizDirectory, "user_sync_logs_" + DateStamp() + ".txt");
break;
case "user_sync":
case "user_sync_logs":
dir = _userSyncJobLogsDirectory;
prefix = "user_sync_logs_";
case BizChannel.Template:
path = Path.Combine(_templateBizDirectory, "template_fetching_logs_" + DateStamp() + ".txt");
break;
case BizChannel.Unreachable:
path = Path.Combine(_unreachableBizDirectory, "unreachable_devices_" + DateStamp() + ".txt");
break;
default:
dir = _internalLogsDirectory;
prefix = "internal_logs_";
path = InternalLogPath();
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)
{
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() { }
}