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
parent
f783537917
commit
672fa2a89d
|
|
@ -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 -> 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 -> 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() { }
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue