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 */ } } } }