独立监控核心和模块
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
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;
|
||||
private bool _ready;
|
||||
private bool _closed;
|
||||
|
||||
// 缓存"哪个硬件属于快通道"——按 hardware type 判断更稳定(不同机型传感器命名不一致)
|
||||
// 实际分频逻辑见 SamplingScheduler,此处只暴露硬件列表给调度器
|
||||
public IReadOnlyList<IHardware> AllHardware => _visitor.AllHardware;
|
||||
|
||||
public bool IsAdmin => _isAdmin;
|
||||
public double ColdStartMs => _coldStartMs;
|
||||
public bool Ready => _ready;
|
||||
|
||||
public HardwareManager(bool basic)
|
||||
{
|
||||
_isAdmin = IsRunningAsAdmin();
|
||||
_startupSw = Stopwatch.StartNew();
|
||||
|
||||
_computer = new Computer
|
||||
{
|
||||
IsCpuEnabled = true,
|
||||
IsGpuEnabled = true,
|
||||
IsMemoryEnabled = true,
|
||||
IsStorageEnabled = true,
|
||||
// basic 模式只开核心四类硬件(CPU/GPU/RAM/Storage),降低冷启动耗时
|
||||
IsMotherboardEnabled = !basic,
|
||||
IsControllerEnabled = !basic,
|
||||
IsBatteryEnabled = !basic,
|
||||
IsNetworkEnabled = !basic,
|
||||
IsPsuEnabled = !basic,
|
||||
};
|
||||
|
||||
_computer.Open();
|
||||
|
||||
// 首轮扫描:发现所有硬件(Update 前的 Sensors 通常为空,但 Hardware 列表就绪)
|
||||
_computer.Accept(_visitor);
|
||||
|
||||
// 首轮 Update:填充传感器值
|
||||
// 注意:部分传感器第二轮才有值,调度器会持续 Update
|
||||
UpdateAll();
|
||||
|
||||
_coldStartMs = _startupSw.Elapsed.TotalMilliseconds;
|
||||
_startupSw.Stop();
|
||||
_ready = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全量 Update 所有硬件。
|
||||
/// 由 SamplingScheduler 按通道分频调用。
|
||||
/// </summary>
|
||||
public void UpdateAll()
|
||||
{
|
||||
foreach (var hw in _visitor.AllHardware)
|
||||
{
|
||||
try { hw.Update(); } catch { /* 单个硬件 Update 失败不影响整体 */ }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅 Update 慢通道硬件(Storage/PSU/Battery 等)。
|
||||
/// 快通道硬件(CPU/GPU/Memory/Network)由调度器更高频调用 UpdateAll。
|
||||
/// </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
|
||||
if (_coldStartMs > 0 && snap.Timestamp > 0)
|
||||
{
|
||||
snap.ColdStartMs = Math.Round(_coldStartMs, 1);
|
||||
}
|
||||
|
||||
// 重新遍历以读取最新传感器值(visitor 缓存的是 hardware 引用,sensor 值实时)
|
||||
var groups = new Dictionary<string, SensorGroup>();
|
||||
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)
|
||||
{
|
||||
g.Sensors.Add(new SensorEntry
|
||||
{
|
||||
Id = $"{groupId}/{hw.Name}/{s.SensorType}/{s.Name}".Replace(' ', '_').ToLowerInvariant(),
|
||||
Name = s.Name,
|
||||
Type = s.SensorType.ToString().ToLowerInvariant(),
|
||||
Value = s.Value,
|
||||
Unit = UnitFor(s.SensorType),
|
||||
HardwareName = hw.Name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return snap;
|
||||
}
|
||||
|
||||
/// <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 => "%",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
private 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.UpdateAll。
|
||||
/// 使用 Channel 向 SSE 推送层广播快照(解耦:调度器不关心有几个订阅者)。
|
||||
///
|
||||
/// 调度策略:
|
||||
/// - 快通道 tick:UpdateAll(含慢通道硬件,因 UpdateAll 成本主要在 SMART,已通过慢通道分频减少调用频率)
|
||||
/// 实际优化:快通道 tick 只 Update 快通道硬件(UpdateFastOnly),慢通道单独按慢节奏 Update
|
||||
/// - 慢通道 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.UpdateAll();
|
||||
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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user