NewLife/RainbowBridge

feat: 优化服务注册,添加平台感知的适配器支持
大石头 authored at 2026-07-03 16:39:22
ab245fc
Tree
1 Parent(s) 6b52800
Summary: 6 changed files with 178 additions and 49 deletions.
Modified +20 -7
Modified +19 -2
Modified +4 -5
Modified +71 -2
Modified +26 -5
Modified +38 -28
Modified +20 -7
diff --git a/Rainbow.Data/Services/DnsmasqManager.cs b/Rainbow.Data/Services/DnsmasqManager.cs
index 710a240..e111680 100644
--- a/Rainbow.Data/Services/DnsmasqManager.cs
+++ b/Rainbow.Data/Services/DnsmasqManager.cs
@@ -36,8 +36,8 @@ public class DnsmasqManager
 
             // 3. 确保配置目录存在
             var dir = Path.GetDirectoryName(configPath);
-            if (!String.IsNullOrEmpty(dir) && !Directory.Exists(dir))
-                await _shell.ExecuteAsync("mkdir", $"-p {dir}", true);
+            if (!String.IsNullOrEmpty(dir))
+                Directory.CreateDirectory(dir);
 
             // 4. 委托适配器生成配置、写入文件并重载服务
             var applyResult = await _adapter.ApplyConfigAsync(config);
@@ -45,7 +45,7 @@ public class DnsmasqManager
             {
                 // 失败则回滚旧配置
                 XTrace.WriteLine("[DnsmasqManager] 应用配置失败,尝试回滚");
-                await _shell.ExecuteAsync("cp", $"{backupPath} {configPath}", true);
+                try { if (File.Exists(backupPath)) File.Copy(backupPath, configPath, true); } catch (Exception ex) { XTrace.WriteLine($"[DnsmasqManager] 回滚失败: {ex.Message}"); }
                 return false;
             }
 
@@ -60,11 +60,24 @@ public class DnsmasqManager
     }
 
     /// <summary>备份配置文件</summary>
-    private async Task BackupConfigAsync(String configPath, String backupPath)
+    private Task BackupConfigAsync(String configPath, String backupPath)
     {
-        var result = await _shell.ExecuteAsync("cp", $"{configPath} {backupPath}");
-        if (!result.Success)
-            XTrace.WriteLine($"[DnsmasqManager] 备份配置失败(可能文件不存在): {result.Stderr}");
+        try
+        {
+            if (File.Exists(configPath))
+            {
+                File.Copy(configPath, backupPath, true);
+            }
+            else
+            {
+                XTrace.WriteLine("[DnsmasqManager] 备份配置跳过(源文件不存在)");
+            }
+        }
+        catch (Exception ex)
+        {
+            XTrace.WriteLine($"[DnsmasqManager] 备份配置失败: {ex.Message}");
+        }
+        return Task.CompletedTask;
     }
 
     /// <summary>从 SQLite 数据库加载全量 DHCP/DNS 配置</summary>
Modified +19 -2
diff --git a/Rainbow.Data/Services/FirewallManager.cs b/Rainbow.Data/Services/FirewallManager.cs
index 6abd174..58ba405 100644
--- a/Rainbow.Data/Services/FirewallManager.cs
+++ b/Rainbow.Data/Services/FirewallManager.cs
@@ -17,11 +17,12 @@ public class FirewallManager
     public ShellExecutor Shell { get; set; }
     public ConfigBackup Backup { get; set; }
     public ILog Log { get; set; } = Logger.Null;
+    private readonly IFirewallAdapter? _adapter;
     private TimerX _scheduleTimer;
     private readonly ConcurrentDictionary<Int32, (String StartTime, String EndTime, Int32 WeekDays)> _schedules = new();
     private Int32 _scheduling;
 
-    public FirewallManager(ShellExecutor shell = null, ConfigBackup backup = null) { Shell = shell; Backup = backup; }
+    public FirewallManager(ShellExecutor shell = null, ConfigBackup backup = null, IFirewallAdapter? adapter = null) { Shell = shell; Backup = backup; _adapter = adapter; }
 
     public void SetSchedule(Int32 ruleId, String startTime, String endTime, Int32 weekDays = 127)
     {
@@ -64,5 +65,21 @@ public class FirewallManager
 
     public IReadOnlyDictionary<Int32, (String StartTime, String EndTime, Int32 WeekDays)> GetSchedules() => _schedules;
 
-    public async Task<ShellResult> SaveRulesAsync() => ShellResult.Fail("待 Linux 环境实现");
+    public async Task<ShellResult> SaveRulesAsync()
+    {
+        if (_adapter != null)
+        {
+            var ok = await _adapter.SaveAsync();
+            return new ShellResult { ExitCode = ok ? 0 : -1, Stdout = ok ? "防火墙规则已保存" : null, Stderr = ok ? null : "保存防火墙规则失败" };
+        }
+
+        // 无适配器时回退到 shell 命令(Linux iptables-save)
+        if (Shell != null)
+        {
+            var result = await Shell.ExecuteAsync("iptables-save", "", true);
+            return result;
+        }
+
+        return ShellResult.Fail("无可用防火墙适配器");
+    }
 }
Modified +4 -5
diff --git a/Rainbow.Data/Services/MonitorCollector.cs b/Rainbow.Data/Services/MonitorCollector.cs
index 3fa0de7..e690fb3 100644
--- a/Rainbow.Data/Services/MonitorCollector.cs
+++ b/Rainbow.Data/Services/MonitorCollector.cs
@@ -4,20 +4,19 @@ namespace Rainbow.Services;
 public class MonitorCollector
 {
     private readonly IOSAdapter _osAdapter;
-    private INetworkStatProvider? _networkStatProvider;
+    private readonly INetworkStatProvider? _networkStatProvider;
     private Dictionary<String, NetworkStats> _lastStats = new();
     private DateTime _lastCollectTime = DateTime.MinValue;
 
     /// <summary>构造监控采集器</summary>
     /// <param name="osAdapter">操作系统适配器</param>
-    public MonitorCollector(IOSAdapter osAdapter)
+    /// <param name="networkStatProvider">网口流量统计提供者(可选,Windows 上由 DI 注入,Linux 上由 DebianAdapter 提供)</param>
+    public MonitorCollector(IOSAdapter osAdapter, INetworkStatProvider? networkStatProvider = null)
     {
         _osAdapter = osAdapter ?? throw new ArgumentNullException(nameof(osAdapter));
+        _networkStatProvider = networkStatProvider;
     }
 
-    /// <summary>注入网口流量统计提供者(可选,Linux 下由 DebianAdapter 提供)</summary>
-    public void SetNetworkStatProvider(INetworkStatProvider provider) => _networkStatProvider = provider;
-
     /// <summary>获取 CPU 使用率(委托给 IOSAdapter)</summary>
     public Double GetCpuUsage() => _osAdapter.GetCpuUsage();
 
Modified +71 -2
diff --git a/Rainbow.Data/Services/StatCollector.cs b/Rainbow.Data/Services/StatCollector.cs
index cf2b63b..a94f054 100644
--- a/Rainbow.Data/Services/StatCollector.cs
+++ b/Rainbow.Data/Services/StatCollector.cs
@@ -17,8 +17,16 @@ namespace Rainbow.Services;
 public class StatCollector
 {
     private TimerX? _timer;
+    private readonly INetworkStatProvider? _statProvider;
     private readonly ConcurrentDictionary<String, (Int64 rx, Int64 tx, DateTime time)> _lastNetStats = new();
 
+    /// <summary>构造统计采集器</summary>
+    /// <param name="statProvider">网口流量统计提供者(可选,Windows 上由 DI 注入)</param>
+    public StatCollector(INetworkStatProvider? statProvider = null)
+    {
+        _statProvider = statProvider;
+    }
+
     /// <summary>启动采集。每分钟执行一次</summary>
     public void Start()
     {
@@ -69,10 +77,16 @@ public class StatCollector
 
     #region 网口流量采集
 
-    /// <summary>从 /proc/net/dev 采集网口流量,计算差值写入 InterfaceStat</summary>
+    /// <summary>从系统采集网口流量,计算差值写入 InterfaceStat</summary>
     private void CollectInterfaceStat()
     {
-        if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return;
+        // Windows 上使用 INetworkStatProvider 采集
+        if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+        {
+            if (_statProvider != null)
+                CollectInterfaceStatFromProvider();
+            return;
+        }
 
         var lines = File.ReadAllLines("/proc/net/dev");
         var now = DateTime.Now;
@@ -143,6 +157,61 @@ public class StatCollector
         }
     }
 
+    /// <summary>通过 INetworkStatProvider 采集网口流量(Windows/macOS 等非 Linux 平台)</summary>
+    private void CollectInterfaceStatFromProvider()
+    {
+        if (_statProvider == null) return;
+
+        try
+        {
+            var stats = _statProvider.GetInterfaceStatsAsync().GetAwaiter().GetResult();
+            var now = DateTime.Now;
+            var statTime = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, 0);
+
+            foreach (var s in stats)
+            {
+                // 计算差值
+                Int64 rxDelta = s.RxBytes;
+                Int64 txDelta = s.TxBytes;
+                if (_lastNetStats.TryGetValue(s.Name, out var last) && last.time > DateTime.MinValue)
+                {
+                    rxDelta = s.RxBytes - last.rx;
+                    txDelta = s.TxBytes - last.tx;
+                    if (rxDelta < 0) rxDelta = 0;
+                    if (txDelta < 0) txDelta = 0;
+                }
+                _lastNetStats[s.Name] = (s.RxBytes, s.TxBytes, now);
+
+                // 跳过无流量的接口
+                if (rxDelta == 0 && txDelta == 0) continue;
+
+                // Windows 网口名可能过长(如 "Realtek PCIe GbE Family Controller"),截断到 20 字符
+                var ifName = s.Name;
+                if (ifName != null && ifName.Length > 20)
+                    ifName = ifName.Substring(0, 20);
+
+                var stat = new InterfaceStat
+                {
+                    InterfaceName = ifName,
+                    RxBytes = rxDelta,
+                    RxPackets = s.RxPackets > 0 ? s.RxPackets : 0,
+                    TxPackets = s.TxPackets > 0 ? s.TxPackets : 0,
+                    RxErrors = s.RxErrors,
+                    TxErrors = s.TxErrors,
+                    RxDropped = s.RxDropped,
+                    TxDropped = s.TxDropped,
+                    StatTime = statTime,
+                };
+
+                stat.Insert();
+            }
+        }
+        catch (Exception ex)
+        {
+            XTrace.WriteLine($"[StatCollector] 通过 Provider 采集网口统计失败: {ex.Message}");
+        }
+    }
+
     #endregion
 
     #region 设备流量采集
Modified +26 -5
diff --git a/Rainbow.Data/Services/WanManager.cs b/Rainbow.Data/Services/WanManager.cs
index 43d5030..95775f2 100644
--- a/Rainbow.Data/Services/WanManager.cs
+++ b/Rainbow.Data/Services/WanManager.cs
@@ -2,6 +2,7 @@ using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Linq;
+using System.Runtime.InteropServices;
 using System.Threading;
 using System.Threading.Tasks;
 using NewLife;
@@ -29,13 +30,28 @@ public class WanManager
         if (Shell == null) return ShellResult.Fail("Shell 未配置");
         var wans = WanInterface.FindAll(WanInterface._.Enable == true);
         var table = 100;
+        var isWindows = OperatingSystem.IsWindows();
+
         foreach (var wan in wans)
         {
             if (wan.Gateway.IsNullOrEmpty() || wan.Name.IsNullOrEmpty()) continue;
             wan.RouteTable = table;
-            var result = await Shell.ExecuteAsync("ip", $"route add default via {wan.Gateway} dev {wan.Name} table {table}");
-            if (!result.Success) { Log.Warn("WAN[{0}] 路由表失败:{1}", wan.Name, result.Stderr); continue; }
-            if (!wan.IP.IsNullOrEmpty()) await Shell.ExecuteAsync("ip", $"rule add from {wan.IP} table {table}");
+
+            if (isWindows)
+            {
+                // Windows: route.exe add MASK 网关
+                var metric = 256 - (table - 100); // table 越小 metric 越小(越优先)
+                var result = await Shell.ExecuteAsync("route", $"add 0.0.0.0 mask 0.0.0.0 {wan.Gateway} metric {metric} if {wan.Name}");
+                if (!result.Success) { Log.Warn("WAN[{0}] 路由表失败:{1}", wan.Name, result.Stderr); continue; }
+            }
+            else
+            {
+                // Linux: ip route add default via gateway dev interface table tableId
+                var result = await Shell.ExecuteAsync("ip", $"route add default via {wan.Gateway} dev {wan.Name} table {table}");
+                if (!result.Success) { Log.Warn("WAN[{0}] 路由表失败:{1}", wan.Name, result.Stderr); continue; }
+                if (!wan.IP.IsNullOrEmpty()) await Shell.ExecuteAsync("ip", $"rule add from {wan.IP} table {table}");
+            }
+
             wan.Update(); Log.Info("WAN[{0}] 路由表 {1} 已配置", wan.Name, table); table++;
         }
         return new ShellResult { Stdout = $"已为 {wans.Count} 个 WAN 口配置路由表" };
@@ -66,16 +82,21 @@ public class WanManager
             if (Shell == null) return;
             var expWan = new WhereExpression(); expWan &= WanInterface._.Enable == true; expWan &= WanInterface._.IsDefault == true;
             var wans = WanInterface.FindAll(expWan);
+            var isWindows = OperatingSystem.IsWindows();
             foreach (var wan in wans)
             {
                 if (wan.Gateway.IsNullOrEmpty()) continue;
-                var result = await Shell.ExecuteAsync("ping", $"-c 1 -W 3 {wan.Gateway}");
+                var pingArgs = isWindows ? $"-n 1 -w 3000 {wan.Gateway}" : $"-c 1 -W 3 {wan.Gateway}";
+                var result = await Shell.ExecuteAsync("ping", pingArgs);
                 if (result.Success) continue;
                 Log.Warn("WAN[{0}] 网关不可达,切换...", wan.Name);
                 var expFb = new WhereExpression(); expFb &= WanInterface._.Enable == true; expFb &= WanInterface._.Id != wan.Id;
                 var fallback = WanInterface.FindAll(expFb).FirstOrDefault(f => !f.IsDefault);
                 if (fallback == null) continue;
-                await Shell.ExecuteAsync("ip", $"route replace default via {fallback.Gateway} dev {fallback.Name}");
+                if (isWindows)
+                    await Shell.ExecuteAsync("route", $"add 0.0.0.0 mask 0.0.0.0 {fallback.Gateway} metric 10");
+                else
+                    await Shell.ExecuteAsync("ip", $"route replace default via {fallback.Gateway} dev {fallback.Name}");
                 wan.IsDefault = false; wan.Update(); fallback.IsDefault = true; fallback.Update();
                 Log.Info("WAN 故障切换:{0} → {1}", wan.Name, fallback.Name);
             }
Modified +38 -28
diff --git a/Rainbow.Web/Program.cs b/Rainbow.Web/Program.cs
index 9322c6d..5041b01 100644
--- a/Rainbow.Web/Program.cs
+++ b/Rainbow.Web/Program.cs
@@ -30,34 +30,6 @@ services.AddSingleton<ConfigBackup>();
 // 跨平台适配器
 var osAdapter = AdapterFactory.Create();
 services.AddSingleton<IOSAdapter>(osAdapter);
-services.AddSingleton<MonitorCollector>();
-services.AddSingleton<WanManager>();
-services.AddSingleton<PppManager>();
-services.AddSingleton<FirewallManager>();
-services.AddSingleton<StatCollector>();
-services.AddSingleton<StatAggregator>();
-services.AddSingleton<DeviceHistoryTracker>();
-services.AddSingleton<MemberQuotaService>();
-services.AddSingleton<UpgradeService>();
-
-// DHCP/DNS 适配器(按平台注册)
-services.AddSingleton<IDhcpDnsAdapter>(sp =>
-{
-    if (OperatingSystem.IsLinux())
-    {
-        var shell = sp.GetRequiredService<ShellExecutor>();
-        return new DnsmasqAdapter(shell);
-    }
-    if (OperatingSystem.IsWindows())
-        return new WindowsDhcpDnsAdapter();
-    return new DnsmasqAdapter(sp.GetRequiredService<ShellExecutor>());
-});
-
-// Dnsmasq 配置管理器(单例)
-services.AddSingleton<DnsmasqManager>();
-
-// DNS 日志解析器(单例,定时更新黑名单命中统计)
-services.AddSingleton<DnsLogParser>();
 
 // 网络管理接口(按平台注册具体实现)
 services.AddSingleton<INetworkInterfaceManager>(sp =>
@@ -72,6 +44,8 @@ services.AddSingleton<INetworkRouteManager>(sp =>
         return new WindowsRouteManager();
     return new StubNetworkRouteManager();
 });
+
+// 网口流量统计提供者(按平台注册)
 services.AddSingleton<INetworkStatProvider>(sp =>
 {
     if (OperatingSystem.IsWindows())
@@ -79,6 +53,42 @@ services.AddSingleton<INetworkStatProvider>(sp =>
     return new StubNetworkStatProvider();
 });
 
+// 防火墙适配器(按平台注册)
+services.AddSingleton<IFirewallAdapter>(sp =>
+{
+    if (OperatingSystem.IsWindows())
+        return new WindowsFirewallAdapter();
+    // Linux 上根据发行版选择 iptables/nftables
+    var shell = sp.GetRequiredService<ShellExecutor>();
+    return new IptablesAdapter(shell);
+});
+
+// DHCP/DNS 适配器(按平台注册)
+services.AddSingleton<IDhcpDnsAdapter>(sp =>
+{
+    if (OperatingSystem.IsLinux())
+    {
+        var shell = sp.GetRequiredService<ShellExecutor>();
+        return new DnsmasqAdapter(shell);
+    }
+    if (OperatingSystem.IsWindows())
+        return new WindowsDhcpDnsAdapter();
+    return new DnsmasqAdapter(sp.GetRequiredService<ShellExecutor>());
+});
+
+// 服务注册(含平台感知的注入)
+services.AddSingleton<MonitorCollector>();
+services.AddSingleton<WanManager>();
+services.AddSingleton<PppManager>();
+services.AddSingleton<FirewallManager>();
+services.AddSingleton<DnsmasqManager>();
+services.AddSingleton<DnsLogParser>();
+services.AddSingleton<StatCollector>();
+services.AddSingleton<StatAggregator>();
+services.AddSingleton<DeviceHistoryTracker>();
+services.AddSingleton<MemberQuotaService>();
+services.AddSingleton<UpgradeService>();
+
 // 拦截提示页服务(端口 9080)
 services.AddHostedService<BlockPageService>();