using System;
using System.IO;
using System.Threading;
namespace Aivfo.OperationLog
{
///
/// 配置热加载(G3-3 / 14§10):后台定时轮询一个 JSON 配置文件,文件变化即热应用到
/// (全局开关/级别 + 模块级开关),无需改代码重编。
///
/// "集中下发"落地方式:日志微服务 / 运维把统一配置文件下发(覆盖)到各端
/// 指向的路径即可;本组件负责读取并热生效。
///
/// 全 try 兜底:读/解析失败沿用旧配置,绝不抛、绝不影响业务。
///
internal sealed class OperationLogConfigWatcher : IDisposable
{
private readonly OperationLogOptions _options;
private readonly Action _onSelfError;
private readonly Timer _timer;
private DateTime _lastWriteUtc = DateTime.MinValue;
private long _lastLength = -1;
private volatile bool _disposed;
public OperationLogConfigWatcher(OperationLogOptions options, Action onSelfError)
{
_options = options;
_onSelfError = onSelfError;
var periodMs = Math.Max(3, _options.ConfigReloadSeconds) * 1000;
// 启动即先读一次(dueTime=0),随后周期轮询。
_timer = new Timer(Poll, null, 0, periodMs);
}
private void Poll(object state)
{
if (_disposed) return;
try
{
var path = _options.ConfigFilePath;
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) return;
// 仅在 文件大小或写时间 变化时才重读重应用(省 IO、避免无谓刷新)。
var info = new FileInfo(path);
var writeUtc = info.LastWriteTimeUtc;
var len = info.Length;
if (writeUtc == _lastWriteUtc && len == _lastLength) return;
string json = SafeReadAllText(path);
if (json == null) return;
_options.ApplyConfigJson(json);
_lastWriteUtc = writeUtc;
_lastLength = len;
_onSelfError?.Invoke($"操作日志配置已热加载: {path}");
}
catch (Exception ex)
{
try { _onSelfError?.Invoke("配置热加载失败(沿用旧配置): " + ex.Message); } catch { }
}
}
/// 容错读取:文件可能正被写,读失败返回 null(下轮再试)。
private static string SafeReadAllText(string path)
{
try
{
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var sr = new StreamReader(fs, System.Text.Encoding.UTF8))
{
return sr.ReadToEnd();
}
}
catch { return null; }
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
try { _timer?.Dispose(); } catch { /* ignore */ }
}
}
}