474 lines
17 KiB
C#
474 lines
17 KiB
C#
using System.Collections.Concurrent;
|
||
using System.Diagnostics;
|
||
using System.Threading.Channels;
|
||
using LibreHardwareMonitor.Hardware;
|
||
|
||
namespace ThingHK;
|
||
|
||
/// <summary>
|
||
/// 硬件访问层:封装 LibreHardwareMonitor Computer,
|
||
/// 负责硬件发现、分层 Update(快/慢通道)、快照构建。
|
||
///
|
||
/// 设计要点:
|
||
/// - Computer 非 IDisposable(LHB 0.9.5),用 Close() 显式释放
|
||
/// - 传感器按 SensorType 分快/慢通道:
|
||
/// * 快通道(temp/load/clock/power/voltage/throughput):高频,1s
|
||
/// * 慢通道(data/smalldata/factor/level/control/timing/energy 等):低频,5s
|
||
/// 原因:SMART 查询会阻塞,混在一起会拖慢整体;data 类(如内存使用量)变化缓慢
|
||
/// - Update 只作用于硬件设备,传感器读取由 visitor 遍历收集(LHB 设计)
|
||
/// </summary>
|
||
internal sealed class HardwareManager : IDisposable
|
||
{
|
||
private readonly Computer _computer;
|
||
private readonly SnapshotVisitor _visitor = new();
|
||
private readonly bool _isAdmin;
|
||
private readonly double _coldStartMs;
|
||
private readonly Stopwatch _startupSw;
|
||
// 传感器 ID 缓存:避免每秒为每个传感器重复拼接字符串(减少 GC 压力)
|
||
private readonly Dictionary<ISensor, string> _sensorIdCache = new();
|
||
private bool _coldStartSent;
|
||
private bool _ready;
|
||
private bool _closed;
|
||
private HardwareConfig _config;
|
||
|
||
// 缓存"哪个硬件属于快通道"——按 hardware type 判断更稳定(不同机型传感器命名不一致)
|
||
// 实际分频逻辑见 SamplingScheduler,此处只暴露硬件列表给调度器
|
||
public IReadOnlyList<IHardware> AllHardware => _visitor.AllHardware;
|
||
|
||
public bool IsAdmin => _isAdmin;
|
||
public double ColdStartMs => _coldStartMs;
|
||
public bool Ready => _ready;
|
||
public HardwareConfig Config => _config;
|
||
|
||
public HardwareManager(HardwareConfig config)
|
||
{
|
||
_config = config ?? HardwareConfig.Default;
|
||
_isAdmin = IsRunningAsAdmin();
|
||
_startupSw = Stopwatch.StartNew();
|
||
|
||
// LHB 行为:主板的传感器实际通过 SubHardware(SuperIO/EC)暴露,
|
||
// 而 SuperIO 作为顶层硬件时需 IsControllerEnabled 才能枚举。
|
||
// 联动策略:启用 motherboard 时自动启用 controller,避免主板勾选后无数据。
|
||
bool motherboardEnabled = _config.IsHardwareEnabled("motherboard");
|
||
bool controllerEnabled = _config.IsHardwareEnabled("controller") || motherboardEnabled;
|
||
|
||
_computer = new Computer
|
||
{
|
||
IsCpuEnabled = _config.IsHardwareEnabled("cpu"),
|
||
IsGpuEnabled = _config.IsHardwareEnabled("gpu"),
|
||
IsMemoryEnabled = _config.IsHardwareEnabled("memory"),
|
||
IsStorageEnabled = _config.IsHardwareEnabled("storage"),
|
||
IsMotherboardEnabled = motherboardEnabled,
|
||
IsControllerEnabled = controllerEnabled,
|
||
IsBatteryEnabled = _config.IsHardwareEnabled("battery"),
|
||
IsNetworkEnabled = _config.IsHardwareEnabled("network"),
|
||
IsPsuEnabled = _config.IsHardwareEnabled("psu"),
|
||
};
|
||
|
||
_computer.Open();
|
||
|
||
// 首轮扫描:发现所有硬件(Update 前的 Sensors 通常为空,但 Hardware 列表就绪)
|
||
_computer.Accept(_visitor);
|
||
|
||
// 首轮 Update:填充传感器值
|
||
// 注意:部分传感器第二轮才有值,调度器会持续 Update
|
||
UpdateAll();
|
||
|
||
_coldStartMs = _startupSw.Elapsed.TotalMilliseconds;
|
||
_startupSw.Stop();
|
||
_ready = true;
|
||
}
|
||
|
||
/// <summary>热更新传感器类型过滤(无需重启 Kernel,下次 BuildSnapshot 生效)</summary>
|
||
public void UpdateConfig(HardwareConfig newConfig)
|
||
{
|
||
_config = newConfig;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 全量 Update 所有硬件。
|
||
/// 仅用于构造函数首轮填充,运行期由调度器分频调用 UpdateFastOnly/UpdateSlowOnly。
|
||
/// </summary>
|
||
public void UpdateAll()
|
||
{
|
||
foreach (var hw in _visitor.AllHardware)
|
||
{
|
||
try { hw.Update(); } catch { /* 单个硬件 Update 失败不影响整体 */ }
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 仅 Update 快通道硬件(CPU/GPU/Memory/Network 等)。
|
||
/// 由调度器快通道 tick 高频调用,避免 SMART 等重查询拖慢采样节奏。
|
||
/// </summary>
|
||
public void UpdateFastOnly()
|
||
{
|
||
foreach (var hw in _visitor.AllHardware)
|
||
{
|
||
if (!IsSlowHardware(hw.HardwareType))
|
||
{
|
||
try { hw.Update(); } catch { /* 单个硬件 Update 失败不影响整体 */ }
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 仅 Update 慢通道硬件(Storage/PSU/Battery 等)。
|
||
/// 快通道硬件(CPU/GPU/Memory/Network)由调度器更高频调用 UpdateFastOnly。
|
||
/// </summary>
|
||
public void UpdateSlowOnly()
|
||
{
|
||
foreach (var hw in _visitor.AllHardware)
|
||
{
|
||
if (IsSlowHardware(hw.HardwareType))
|
||
{
|
||
try { hw.Update(); } catch { /* ignore */ }
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构建 SensorSnapshot:遍历当前所有传感器,不触发 Update。
|
||
/// 调用者应先 Update 再 Snapshot,避免读到旧值。
|
||
/// </summary>
|
||
public SensorSnapshot BuildSnapshot()
|
||
{
|
||
var snap = new SensorSnapshot
|
||
{
|
||
SchemaVersion = 1,
|
||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||
IsAdmin = _isAdmin,
|
||
Ready = _ready,
|
||
};
|
||
|
||
// 首个快照带上冷启动耗时,后续为 0(修复:此前每个快照都携带 ColdStartMs)
|
||
if (_coldStartMs > 0 && !_coldStartSent)
|
||
{
|
||
snap.ColdStartMs = Math.Round(_coldStartMs, 1);
|
||
_coldStartSent = true;
|
||
}
|
||
|
||
// 重新遍历以读取最新传感器值(visitor 缓存的是 hardware 引用,sensor 值实时)
|
||
var groups = new Dictionary<string, SensorGroup>();
|
||
|
||
// 为已启用但 LHB 未枚举到的硬件类型预创建空分组,
|
||
// 确保前端能显示"已启用但无数据"的硬件(而不是直接隐藏,让用户误以为配置未生效)
|
||
EnsureGroupsForEnabledHardware(groups, snap);
|
||
|
||
foreach (var hw in _visitor.AllHardware)
|
||
{
|
||
string groupId = hw.HardwareType.ToString().ToLowerInvariant();
|
||
string groupName = hw.HardwareType.ToString();
|
||
if (!groups.TryGetValue(groupId, out var g))
|
||
{
|
||
g = new SensorGroup { Id = groupId, Name = groupName };
|
||
groups[groupId] = g;
|
||
snap.Groups.Add(g);
|
||
}
|
||
|
||
foreach (var s in hw.Sensors)
|
||
{
|
||
// 按配置过滤传感器类型:关闭的类型不加入快照,减少传输数据量
|
||
string sensorType = s.SensorType.ToString().ToLowerInvariant();
|
||
if (!_config.IsSensorTypeEnabled(sensorType))
|
||
continue;
|
||
|
||
g.Sensors.Add(new SensorEntry
|
||
{
|
||
Id = GetSensorId(s, groupId, hw.Name),
|
||
Name = s.Name,
|
||
Type = sensorType,
|
||
Value = s.Value,
|
||
Unit = UnitFor(s.SensorType),
|
||
HardwareName = hw.Name,
|
||
});
|
||
}
|
||
}
|
||
|
||
return snap;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取传感器稳定 ID(带缓存)。
|
||
/// ID 格式:{groupId}/{hwName}/{sensorType}/{sensorName},空格转下划线、小写化。
|
||
/// 缓存命中直接返回,未命中(新传感器)计算后入缓存。
|
||
/// </summary>
|
||
private string GetSensorId(ISensor s, string groupId, string hwName)
|
||
{
|
||
if (!_sensorIdCache.TryGetValue(s, out var id))
|
||
{
|
||
id = $"{groupId}/{hwName}/{s.SensorType}/{s.Name}".Replace(' ', '_').ToLowerInvariant();
|
||
_sensorIdCache[s] = id;
|
||
}
|
||
return id;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 为已启用但 LHB 未枚举到的硬件类型预创建空分组。
|
||
/// 场景:用户在设置中勾选了主板/电池/电源等,但 LHB 在当前权限或机型下检测不到对应硬件,
|
||
/// 此时仍创建空分组让前端显示"已启用但无数据",避免用户误以为配置未生效。
|
||
/// GPU 特殊处理:LHB 会枚举到 GpuIntel/GpuAmd/GpuNvidia 之一,不预创建通用分组。
|
||
/// </summary>
|
||
private void EnsureGroupsForEnabledHardware(Dictionary<string, SensorGroup> groups, SensorSnapshot snap)
|
||
{
|
||
// (configKey, groupId, groupName)
|
||
// 注意:controller 对应 SuperIO 和 EmbeddedController 两个 HardwareType
|
||
var mapping = new (string, string, string)[]
|
||
{
|
||
("cpu", "cpu", "CPU"),
|
||
("memory", "memory", "Memory"),
|
||
("storage", "storage", "Storage"),
|
||
("motherboard", "motherboard", "Motherboard"),
|
||
("controller", "superio", "SuperIO"),
|
||
("controller", "embeddedcontroller", "EmbeddedController"),
|
||
("battery", "battery", "Battery"),
|
||
("network", "network", "Network"),
|
||
("psu", "psu", "Psu"),
|
||
};
|
||
|
||
foreach (var (key, groupId, groupName) in mapping)
|
||
{
|
||
if (_config.IsHardwareEnabled(key) && !groups.ContainsKey(groupId))
|
||
{
|
||
var g = new SensorGroup { Id = groupId, Name = groupName };
|
||
groups[groupId] = g;
|
||
snap.Groups.Add(g);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断硬件是否属于慢通道(低频 Update 即可)。
|
||
/// 快通道:CPU/GPU/Memory/Network(变化快、查询轻量)
|
||
/// 慢通道:Storage/PSU/Motherboard/Battery/SuperIO/EmbeddedController(查询重或变化慢)
|
||
/// </summary>
|
||
public static bool IsSlowHardware(HardwareType t) => t switch
|
||
{
|
||
HardwareType.Storage => true,
|
||
HardwareType.Psu => true,
|
||
HardwareType.Motherboard => true,
|
||
HardwareType.Battery => true,
|
||
HardwareType.SuperIO => true,
|
||
HardwareType.EmbeddedController => true,
|
||
_ => false,
|
||
};
|
||
|
||
private static string UnitFor(SensorType t) => t switch
|
||
{
|
||
SensorType.Temperature => "°C",
|
||
SensorType.Load => "%",
|
||
SensorType.Power => "W",
|
||
SensorType.Voltage => "V",
|
||
SensorType.Fan => "RPM",
|
||
SensorType.Clock => "MHz",
|
||
SensorType.Data => "GB",
|
||
SensorType.SmallData => "MB",
|
||
SensorType.Frequency => "Hz",
|
||
SensorType.Throughput => "B/s",
|
||
SensorType.Level => "%",
|
||
SensorType.Factor => "",
|
||
SensorType.Control => "%",
|
||
SensorType.Flow => "L/h",
|
||
SensorType.TimeSpan => "s",
|
||
SensorType.Energy => "mWh",
|
||
SensorType.Noise => "dBA",
|
||
SensorType.Conductivity => "µS/cm",
|
||
SensorType.Humidity => "%",
|
||
_ => "",
|
||
};
|
||
|
||
internal static bool IsRunningAsAdmin()
|
||
{
|
||
try
|
||
{
|
||
using var identity = System.Security.Principal.WindowsIdentity.GetCurrent();
|
||
var principal = new System.Security.Principal.WindowsPrincipal(identity);
|
||
return principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator);
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
if (_closed) return;
|
||
_closed = true;
|
||
try { _computer.Close(); } catch { /* Close 在 AOT 下偶有反射清理异常 */ }
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 遍历 Computer,收集所有 Hardware(含 SubHardware)。
|
||
/// LHB 的 Visit 会递归到 SubHardware。
|
||
/// 引用缓存:visitor 持有 hardware 引用,传感器值通过 hardware.Sensors 实时读取。
|
||
/// </summary>
|
||
internal sealed class SnapshotVisitor : IVisitor
|
||
{
|
||
public List<IHardware> AllHardware { get; } = new();
|
||
|
||
public void VisitComputer(IComputer computer)
|
||
{
|
||
foreach (var hw in computer.Hardware) hw.Accept(this);
|
||
}
|
||
|
||
public void VisitHardware(IHardware hardware)
|
||
{
|
||
AllHardware.Add(hardware);
|
||
foreach (var sub in hardware.SubHardware) sub.Accept(this);
|
||
}
|
||
|
||
public void VisitSensor(ISensor sensor) { }
|
||
public void VisitParameter(IParameter parameter) { }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 采样调度器:按快/慢通道分频驱动 HardwareManager 的分层 Update。
|
||
/// 使用 Channel 向 SSE 推送层广播快照(解耦:调度器不关心有几个订阅者)。
|
||
///
|
||
/// 调度策略:
|
||
/// - 快通道 tick:UpdateFastOnly(仅 CPU/GPU/Memory/Network 等轻量硬件),
|
||
/// 避免 SMART 等重查询每秒执行拖慢采样节奏
|
||
/// - 慢通道 tick:UpdateSlowOnly(仅 Storage/PSU/Motherboard 等)
|
||
/// - 快通道每个 tick 结束后构建快照并广播(慢通道更新后的值随下一帧带出)
|
||
/// </summary>
|
||
internal sealed class SamplingScheduler : IDisposable
|
||
{
|
||
private readonly HardwareManager _hw;
|
||
private readonly CancellationTokenSource _cts = new();
|
||
private readonly Channel<SensorSnapshot> _broadcast;
|
||
private readonly SnapshotCache _cache;
|
||
|
||
private int _fastIntervalMs = 1000;
|
||
private int _slowIntervalMs = 5000;
|
||
|
||
public SnapshotCache Cache => _cache;
|
||
public int FastIntervalMs => _fastIntervalMs;
|
||
public int SlowIntervalMs => _slowIntervalMs;
|
||
|
||
public SamplingScheduler(HardwareManager hw, int fastIntervalMs, int slowIntervalMs)
|
||
{
|
||
_hw = hw;
|
||
_fastIntervalMs = Math.Max(200, fastIntervalMs);
|
||
_slowIntervalMs = Math.Max(_fastIntervalMs, slowIntervalMs);
|
||
// unbounded channel:快照丢失风险 < 内存爆涨风险(消费慢时丢弃最旧的)
|
||
// 这里用 bounded + DropOldest:保证 SSE 慢消费者不阻塞调度器
|
||
_broadcast = Channel.CreateBounded<SensorSnapshot>(new BoundedChannelOptions(8)
|
||
{
|
||
FullMode = BoundedChannelFullMode.DropOldest,
|
||
SingleReader = false,
|
||
SingleWriter = true,
|
||
});
|
||
_cache = new SnapshotCache();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动调度循环。立即推送首个快照(已由 HardwareManager 构造时 Update 过)。
|
||
/// </summary>
|
||
public Task StartAsync()
|
||
{
|
||
// 首个快照(含 ColdStartMs)
|
||
var first = _hw.BuildSnapshot();
|
||
_cache.Update(first);
|
||
_broadcast.Writer.TryWrite(first);
|
||
|
||
// 快/慢通道并行循环
|
||
_ = Task.Run(() => FastLoopAsync(_cts.Token));
|
||
_ = Task.Run(() => SlowLoopAsync(_cts.Token));
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
private async Task FastLoopAsync(CancellationToken ct)
|
||
{
|
||
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_fastIntervalMs));
|
||
while (await timer.WaitForNextTickAsync(ct).ConfigureAwait(false))
|
||
{
|
||
try
|
||
{
|
||
_hw.UpdateFastOnly();
|
||
var snap = _hw.BuildSnapshot();
|
||
_cache.Update(snap);
|
||
_broadcast.Writer.TryWrite(snap);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.Error.WriteLine($"[Scheduler] 快通道异常: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
|
||
private async Task SlowLoopAsync(CancellationToken ct)
|
||
{
|
||
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_slowIntervalMs));
|
||
while (await timer.WaitForNextTickAsync(ct).ConfigureAwait(false))
|
||
{
|
||
try
|
||
{
|
||
// 慢通道单独 Update,避免依赖快通道的 UpdateAll
|
||
// 注:下一次快通道 tick 构建的快照会包含本次慢通道更新后的值
|
||
_hw.UpdateSlowOnly();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.Error.WriteLine($"[Scheduler] 慢通道异常: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 订阅快照流。每个 SSE 客户端调用一次。
|
||
/// 返回的 IAsyncEnumerable 会在调度器停止或订阅者取消时结束。
|
||
/// </summary>
|
||
public IAsyncEnumerable<SensorSnapshot> SubscribeAsync(CancellationToken ct) =>
|
||
_broadcast.Reader.ReadAllAsync(ct);
|
||
|
||
public void UpdateIntervals(int? fastMs, int? slowMs)
|
||
{
|
||
// 注意:PeriodicTimer 已启动后无法修改间隔,下次重启 Kernel 才生效。
|
||
// 阶段二简化处理:记录新值,实际生效需重启。阶段三若需热更新可重建 timer。
|
||
if (fastMs.HasValue && fastMs.Value >= 200) _fastIntervalMs = fastMs.Value;
|
||
if (slowMs.HasValue && slowMs.Value >= _fastIntervalMs) _slowIntervalMs = slowMs.Value;
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
_cts.Cancel();
|
||
_broadcast.Writer.TryComplete();
|
||
_cts.Dispose();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 快照缓存:存储最新快照,供 GET /snapshot 直接返回,避免触发底层重扫描。
|
||
/// 线程安全:读写均加锁,快照对象本身不可变(每次 Update 替换引用)。
|
||
/// </summary>
|
||
internal sealed class SnapshotCache
|
||
{
|
||
private readonly object _lock = new();
|
||
private SensorSnapshot? _latest;
|
||
private int _sensorCount;
|
||
|
||
public void Update(SensorSnapshot snap)
|
||
{
|
||
lock (_lock)
|
||
{
|
||
_latest = snap;
|
||
_sensorCount = snap.Groups.Sum(g => g.Sensors.Count);
|
||
}
|
||
}
|
||
|
||
public SensorSnapshot? GetLatest()
|
||
{
|
||
lock (_lock)
|
||
{
|
||
return _latest;
|
||
}
|
||
}
|
||
|
||
public int SensorCount
|
||
{
|
||
get { lock (_lock) return _sensorCount; }
|
||
}
|
||
}
|