NewLife/X

Merge pull request #174 from NewLifeX/copilot/remove-period-and-polling

FileConfigProvider: restore Period property, dual event-driven + polling mechanism
Stone authored at 2026-02-13 02:34:44 GitHub committed at 2026-02-13 02:34:44
2fb4654
Tree
2 Parent(s) 0c69f4b + b120d59
Summary: 2 changed files with 93 additions and 14 deletions.
Modified +76 -14
Modified +17 -0
Modified +76 -14
diff --git a/NewLife.Core/Configuration/FileConfigProvider.cs b/NewLife.Core/Configuration/FileConfigProvider.cs
index b83bf99..b79f696 100644
--- a/NewLife.Core/Configuration/FileConfigProvider.cs
+++ b/NewLife.Core/Configuration/FileConfigProvider.cs
@@ -1,20 +1,30 @@
 using System.IO;
 using System.Threading;
 using NewLife.Log;
+using NewLife.Threading;
 
 namespace NewLife.Configuration;
 
 /// <summary>文件配置提供者</summary>
-/// <remarks>每个提供者实例对应一个配置文件,支持热更新</remarks>
+/// <remarks>
+/// 每个提供者实例对应一个配置文件,支持热更新。
+/// 同时使用 FileSystemWatcher 事件驱动和定时器轮询双重机制感知文件变更,
+/// 当事件驱动可用时定时器周期自动拉长作为兜底,不可用时定时器周期较短以保证及时感知。
+/// </remarks>
 public abstract class FileConfigProvider : ConfigProvider
 {
     #region 属性
     /// <summary>文件名。最高优先级,优先于模型特性指定的文件名</summary>
     public String? FileName { get; set; }
 
+    /// <summary>更新周期。默认5秒,0秒表示不做自动更新。事件驱动可用时自动拉长为60秒兜底轮询</summary>
+    public Int32 Period { get; set; } = 5;
+
     private FileSystemWatcher? _watcher;
+    private TimerX? _timer;
     private Boolean _reading;
     private DateTime _lastRefreshTime;
+    private DateTime _lastTime;
     #endregion
 
     #region 构造
@@ -24,6 +34,8 @@ public abstract class FileConfigProvider : ConfigProvider
     {
         base.Dispose(disposing);
 
+        _timer.TryDispose();
+
         if (_watcher != null)
         {
             _watcher.EnableRaisingEvents = false;
@@ -177,33 +189,65 @@ public abstract class FileConfigProvider : ConfigProvider
         if (autoReload) InitTimer();
     }
 
-    /// <summary>初始化文件监控</summary>
+    /// <summary>初始化文件监控。同时启用事件驱动和定时器轮询,事件驱动可用时定时器周期较长</summary>
     private void InitTimer()
     {
-        if (_watcher != null) return;
+        if (_watcher != null || _timer != null) return;
         lock (this)
         {
-            if (_watcher != null) return;
+            if (_watcher != null || _timer != null) return;
 
             var fileName = FileName?.GetBasePath();
             if (fileName.IsNullOrEmpty()) return;
 
-            var directory = Path.GetDirectoryName(fileName);
-            var filter = Path.GetFileName(fileName);
-
-            if (directory.IsNullOrEmpty() || filter.IsNullOrEmpty()) return;
+            var hasWatcher = false;
 
-            _watcher = new FileSystemWatcher(directory, filter)
+            // 尝试使用 FileSystemWatcher 事件驱动
+            try
             {
-                NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
-                EnableRaisingEvents = true
-            };
+                var directory = Path.GetDirectoryName(fileName);
+                var filter = Path.GetFileName(fileName);
+
+                if (!directory.IsNullOrEmpty() && !filter.IsNullOrEmpty())
+                {
+                    var watcher = new FileSystemWatcher(directory, filter)
+                    {
+                        NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
+                        EnableRaisingEvents = true
+                    };
+
+                    // 订阅文件变更事件
+                    watcher.Changed += OnFileChanged;
+                    _watcher = watcher;
+                    hasWatcher = true;
+                }
+            }
+            catch (Exception ex)
+            {
+                // FileSystemWatcher 在某些 Linux/Android 系统上可能不支持或不可靠
+                XTrace.WriteLine("FileSystemWatcher 创建失败:{0}", ex.Message);
+            }
 
-            // 订阅文件变更事件
-            _watcher.Changed += OnFileChanged;
+            // 同时启动定时器轮询,事件驱动可用时周期较长作为兜底
+            StartTimer(hasWatcher);
         }
     }
 
+    /// <summary>启动定时轮询</summary>
+    /// <param name="hasWatcher">事件驱动是否可用。可用时定时器周期拉长为60秒</param>
+    private void StartTimer(Boolean hasWatcher)
+    {
+        if (_timer != null) return;
+
+        var p = Period;
+        if (p <= 0) p = 5;
+
+        // 事件驱动可用时,定时器作为兜底,周期拉长为60秒
+        if (hasWatcher && p < 60) p = 60;
+
+        _timer = new TimerX(DoTimerRefresh, null, p * 1000, p * 1000) { Async = true };
+    }
+
     /// <summary>文件变更事件处理</summary>
     private void OnFileChanged(Object? sender, FileSystemEventArgs e)
     {
@@ -222,6 +266,24 @@ public abstract class FileConfigProvider : ConfigProvider
         }, null);
     }
 
+    /// <summary>定时刷新配置</summary>
+    /// <param name="state">状态对象</param>
+    private void DoTimerRefresh(Object? state)
+    {
+        if (_reading) return;
+        if (FileName.IsNullOrEmpty()) return;
+
+        var fileName = FileName.GetBasePath();
+        var fi = fileName.AsFile();
+        if (!fi.Exists) return;
+
+        fi.Refresh();
+        if (_lastTime.Year > 2000 && fi.LastWriteTime <= _lastTime) return;
+        _lastTime = fi.LastWriteTime;
+
+        DoRefresh();
+    }
+
     /// <summary>刷新配置</summary>
     private void DoRefresh()
     {
Modified +17 -0
diff --git a/XUnitTest.Core/Configuration/JsonConfigProviderTests.cs b/XUnitTest.Core/Configuration/JsonConfigProviderTests.cs
index 266862c..76a6464 100644
--- a/XUnitTest.Core/Configuration/JsonConfigProviderTests.cs
+++ b/XUnitTest.Core/Configuration/JsonConfigProviderTests.cs
@@ -255,4 +255,21 @@ public class JsonConfigProviderTests
 
     //    Assert.NotNull(json);
     //}
+
+    [Fact]
+    public void TestPeriodProperty()
+    {
+        var prv = new JsonConfigProvider { FileName = "Config/period_test.json" };
+
+        // 默认值应为5
+        Assert.Equal(5, prv.Period);
+
+        // 可以设置
+        prv.Period = 10;
+        Assert.Equal(10, prv.Period);
+
+        // 设置为0表示不做自动更新
+        prv.Period = 0;
+        Assert.Equal(0, prv.Period);
+    }
 }
\ No newline at end of file