19 KiB
ThingHK — Thing 硬件监控内核
1. 架构
采用微内核架构,将硬件监控功能剥离为独立进程 ThingHK.exe(C# + LibreHardwareMonitor)。Tauri 主进程(Rust)仅作为"消费者",通过本地 HTTP + SSE 获取标准化数据,实现硬件层与业务层彻底解耦。
核心数据流:
Rust (Tauri App) <---HTTP / SSE (127.0.0.1:8730)---> ThingHK.exe (独立进程)
↓ emit("monitor-data")
前端 MonitorModule.vue
设计取舍:
- 进程隔离而非进程内宿主:LHB 或驱动崩溃不会拖垮 Tauri 主进程;独立进程 + Job Object 既隔离崩溃又复用项目现有架构。
- C# + LibreHardwareMonitor 选型:Rust 侧硬件监控生态薄弱(
sysinfo无 GPU 温度/主板电压,wmi慢且字段不全,自实现 NVAPI/ADL/SMBus 等于重写 LHB)。LHB 是业界事实标准,覆盖 CPU/GPU/盘/主板/USB,可靠性最高。 - 复用 ProcessManager:项目已有 process_manager.rs 完整实现了生命周期管理(启动/停止/心跳/崩溃重启/Job Object),ThingHK 不重建这套逻辑,仅作为"数据消费者"。
- HTTP 而非 Named Pipe:数据量极小(传感器快照每秒 1-2 次、几 KB 量级),Named Pipe 的性能优势用不上;与项目技术栈一致(Cargo.toml 已有
reqwest,mihomo 也走 HTTP REST);调试可用浏览器/curl;跨平台抽象免费。
2. 组件与职责
2.1 ThingHK 独立进程(C#)
- 角色:硬件数据中台。
- 核心组件:
- 数据采集层:以 LibreHardwareMonitorLib 为唯一数据源(LHB 已统一封装 NVAPI/ADL/SMBus/SMART 等来源)。
- SamplingScheduler:按传感器类型分层分频,避免慢查询拖累快通道。
- 快通道(CPU/GPU/Memory/Network):
PeriodicTimer默认 1000ms,UpdateAll()全量刷新 - 慢通道(Storage/PSU/Motherboard/Battery/SuperIO/EmbeddedController):独立
PeriodicTimer默认 5000ms,UpdateSlowOnly()仅刷新慢硬件 - 两通道并行运行,每次快通道 tick 后构建快照并广播到
Channel<SensorSnapshot>(bounded=8,DropOldest 防慢消费者阻塞)
- 快通道(CPU/GPU/Memory/Network):
- SnapshotCache:线程安全快照缓存,UI 请求时直接返回缓存数据,避免触发底层硬件重扫描。
- HTTP 服务层:ASP.NET Core minimal API,暴露
127.0.0.1:8730本地 HTTP 服务。
2.2 Tauri 主进程(Rust)
- 角色:管理者(复用 ProcessManager)与消费者。
- 职责:
- 生命周期管理:复用
ProcessManager,通过start_with_subscription拉起 Kernel。崩溃自动重启、Job Object 异常清理、stop_all退出清理均已内置。 - 数据接收:作为 HTTP 客户端,轮询
/status判就绪 → 订阅/streamSSE → 解析事件 →emit("monitor-data")。 - 写入熔断 + 自动重连:SSE 断开后停止转发,3 秒退避后重试;同时监听
process-status-changed事件,Kernel 由 ProcessManager 自动重启恢复 Running 后主动重新订阅 SSE。
- 生命周期管理:复用
2.3 前端(Vue 3)
- Pinia store(monitorStore.ts):管理 status/snapshot 状态,订阅 Tauri 事件,提供
connState状态机(idle/loading/connected/disconnected/error)。 - MonitorModule.vue(MonitorModule.vue):3 个 Tab(概览/详细/设置),关键指标卡 + SVG sparkline 历史曲线(无外部图表库)+ Accordion 分组 + 断线/降级态提示。
3. 通信协议与数据契约
3.1 路由
| 路由 | 方法 | 说明 |
|---|---|---|
/ |
GET | 健康检查 |
/status |
GET | 冷启动就绪探测,前端轮询判断是否可订阅 |
/snapshot |
GET | 一次性拉取最新缓存快照 |
/stream |
GET (SSE) | 订阅推送,Kernel 按采样间隔主动 Push SensorSnapshot |
/config |
POST | 运行时调整采样间隔({"FastIntervalMs":500,"SlowIntervalMs":3000}) |
/shutdown |
POST | 优雅关闭(提权模式下由 Tauri 调用,普通权限无法 kill 管理员进程) |
3.2 数据契约(显式版本化)
为避免 Kernel 升级后前端解析炸裂,数据 schema 显式版本化。前端按 schemaVersion 解析;Kernel 升级 schema 时递增版本号并提供兼容期。当前 schemaVersion=1。
{
"schemaVersion": 1,
"timestamp": 1730000000000,
"coldStartMs": 4906,
"isAdmin": true,
"ready": true,
"groups": [
{
"id": "cpu",
"name": "CPU",
"sensors": [
{ "id": "...", "name": "CPU Package", "type": "temperature", "hardwareName": "Intel Core i5-10400", "value": 52.3, "unit": "°C" }
]
}
]
}
JSON 字段命名:双方均 camelCase。C# 端 JsonSourceGenerationOptions 设置 PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase;Rust 端 #[serde(rename_all = "camelCase")]。
3.3 Tauri 命令与事件
7 个命令:
| 命令 | 作用 |
|---|---|
monitor_kernel_info |
查询 Kernel 二进制路径/是否存在/端口 |
monitor_status |
查询运行状态(running/pid/ready/sensorCount/restartCount/elevated) |
monitor_start |
启动 Kernel + 自动订阅 SSE(命令和 setup 共用 start_with_subscription) |
monitor_start_elevated |
以管理员权限启动 Kernel(弹 UAC),停止走 /shutdown 接口 |
monitor_stop |
停止 Kernel(提权模式走 /shutdown,普通模式走 ProcessManager) |
monitor_get_status |
透传 Kernel /status |
monitor_get_snapshot |
透传 Kernel /snapshot |
5 个事件:monitor-data(快照)、monitor-ready、monitor-loading、monitor-disconnected、monitor-error。
4. 传感器覆盖度
在管理员权限下,--basic 模式(仅开 CPU/GPU/RAM/Storage)读取两轮后的结果:
| 硬件分组 | 传感器数 | 覆盖项 |
|---|---|---|
| CPU | 47 | 每核每线程负载、每核温度、TjMax、时钟、Package/Cores/Memory/Platform 功率、每核电压、Bus Speed |
| Memory | 42 | 虚拟/物理内存使用量、可用、负载;两条 DDR4 SPD 时序 + 容量 |
| GpuIntel | 17 | GPU 功率、D3D 共享内存总量/已用、D3D 3D/VideoDecode/Copy/VideoProcessing 各引擎负载 |
| Storage | 11 | 温度、Power on count/hours、已用/可用/总空间、Read/Write/Total Activity、Read/Write Rate |
| 合计 | 117 | 100% 有值 |
full 模式额外多出 Network(60 个传感器)和 Motherboard(本机 0 个,需更深驱动/权限)。
非提权降级:LHB 访问 SMBus、部分 EC 传感器、某些 GPU 传感器需要管理员权限。默认非提权运行,覆盖大部分 CPU/GPU 温度(通过 OHM RPC/WMI 仍可读),牺牲部分主板/电压传感器。提权策略(任务计划程序免 UAC)作为可选优化。
5. 实现要点与经验教训
5.1 C# Kernel(Native AOT)
关键配置(ThingHK.csproj):
<TargetFramework>net8.0-windows</TargetFramework>(LHB 在 Windows 下最稳)<PublishAot>true</PublishAot><TrimmerRootDescriptor>rd.xml</TrimmerRootDescriptor>(LHB 反射根描述,preserve="all"兜底)- JSON 使用
[JsonSourceGeneration]消除 IL3050/IL2026 警告
AOT 体积:18.3 MB(含 ASP.NET Core runtime),无需 .NET Runtime,符合"最小安装"偏好。
AOT 踩坑:
- 匿名类型无法序列化:
Results.Ok(new { ... })在 AOT 下抛JsonTypeInfo metadata ... was not provided。所有响应改用强类型 +Results.Json(obj, ThingHKJsonContext.Default.T),新增HealthResponse/ErrorResponse类型注册到[JsonSerializable]。 - async lambda 返回类型:
app.MapPost("/config", async (ctx) => { return Results.Json(...); })在 AOT 下返回空响应体。原因:async lambda 返回IResult被框架当成Task<IResult>的未等待任务。修复:显式声明async Task<IResult> (HttpContext ctx) =>。 - HardwareType 枚举变更:LHB 0.9.5 的
HardwareType无Controller,改为SuperIO+EmbeddedController。 - Computer 非 IDisposable:LHB 0.9.5 的
Computer类未实现IDisposable,HardwareManager.Dispose()改用显式_computer.Close()。
5.2 Rust 集成
关键架构决策:
- 自动启动策略:参照 proxy 模块的
auto_start_on_launch模式,在setup中用tauri::async_runtime::spawn异步拉起 Kernel。硬件监控为被动读取、无副作用(不修改系统状态),故默认启用,无需用户配置开关。 start_with_subscription抽取:将"启动进程 + 注册自动重连 + 启动 SSE 订阅"封装为单一方法,供monitor_start命令和 setup 自动启动复用,避免两条路径行为漂移。MonitorKernel改为Clone+Arc<Mutex>共享状态:tauri::State::inner()返回&T而非&Arc<T>,无法直接 clone 出Arc<MonitorKernel>。将sub_handle和listener_ids改为Arc<Mutex<...>>,MonitorKernel派生Clone,clone 出的实例与原实例共享订阅控制状态。sse_client与client分离:client带 30s 超时,用于/status、/snapshot等短请求;sse_client无超时,专用于/stream长连接。早期版本对 SSE 请求误用timeout(Duration::from_secs(0)),导致 reqwest 立即超时失败。- 写入熔断 + 自动重连:
subscribe_once收到 SSE 断开后停止emit,3 秒退避后重试;Kernel 不响应则退出循环。同时监听process-status-changed事件,Kernel 由 ProcessManager 自动重启恢复 Running 后主动重新订阅 SSE。
Rust 踩坑:
- Tauri 2 async 命令必须返回
Result<T, E>:AsyncCommandMustReturnResult未实现 forMonitorStatus。修复:monitor_status返回类型从MonitorStatus改为Result<MonitorStatus, String>。 - Kernel
/status反序列化失败:C# 默认输出 PascalCase(Ready、IsAdmin),Rust 期望 camelCase。修复:Contracts.cs的JsonSourceGenerationOptions添加PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase。 - 崩溃重连无并发订阅问题:旧 SSE 循环在
is_kernel_alive返回 false(端口未释放)时正确退出,register_auto_reconnect启动新订阅,netstat 确认仅 1 个 ESTABLISHED 连接。
5.3 前端
关键设计决策:
- 状态机
connState:基于 status + eventCount + lastEventTime 推断 5 种状态(idle/loading/connected/disconnected/error),5 秒未收到 monitor-data 事件判定为 disconnected。 findSensorValue多条件查找:支持 groupId + name + hardwareName + type 四维匹配,name 用精确匹配 + includes 子串兜底。用于解决 Memory 分组有两条同名 "Memory" 传感器(Virtual/Total)的歧义。- SVG sparkline 无外部依赖:关键指标历史用 30 点环形 buffer,SVG path 自绘,不引入 Chart.js(符合"最小安装、少依赖"偏好)。
schemaVersion守卫:store 订阅 monitor-data 时检查schemaVersion !== 1并 warn,未来 Kernel 升级 schema 时前端不会静默解析错误数据。- 模块卸载不停止 Kernel:
dispose仅取消事件订阅,不调用monitor_stop。Kernel 生命周期由 ProcessManager 全局管理,与模块 UI 生命周期解耦。 - Ready 边沿触发拉取快照:watch
store.status?.ready从 false→true 时主动fetchSnapshot(),避免 Kernel 冷启动就绪后 UI 等待下一个 SSE tick 才有数据。
传感器名匹配(通过 curl /snapshot 验证实际传感器名):
| 指标 | 查找逻辑 | 说明 |
|---|---|---|
| CPU 封装温度 | name: 'CPU Package', type: 'temperature' |
— |
| CPU 总负载 | name: 'CPU Total', type: 'load' |
— |
| 内存负载 | name: 'Memory', hardwareName: 'Total Memory', type: 'load' |
Memory 分组有两条同名传感器(Virtual/Total),需用 hardwareName 消歧 |
| Intel GPU 3D 负载 | name: 'D3D 3D' ?? name: 'GPU Core' ?? type: 'load' |
LHB 实际输出名是 "D3D 3D",无 "GPU" 前缀 |
| 存储温度 | type: 'temperature' |
用 type 查找 |
前端踩坑:
lucide-vue-next模块未找到:项目实际使用@lucide/vue(见 package.json),而非lucide-vue-next。connState === 'idle'类型比较错误:vue-tsc 报 TS2367。v-else-if 已处理 idle 分支,v-else 块内 connState 类型被收窄为非 idle,=== 'idle'永假。修复:移除该比较。
5.4 稳定性与打包
冷启动:约 5 秒(computer.Open() 驱动加载占绝大部分,Update() 本身约 200ms)。前端 Loading 超时阈值设为 10 秒,轮询间隔 500ms。
崩溃恢复验证:kill ThingHK 进程后,ProcessManager 3s 巡检检测崩溃 → 800ms 端口等待 → spawn 新 Kernel → register_auto_reconnect 监听 process-status-changed(running) 重新订阅 SSE。旧 SSE 循环正确退出,无并发订阅问题。重启后 ready=True,sensorCount=117(与崩溃前一致)。
退出清理验证:
- 正常退出(quit_app / Ctrl+C):
stop_all()同步 kill + 3s 超时 wait,无残留。 - 异常崩溃(kill thing.exe):Windows Job Object(
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)兜底自动清理,无残留。
打包验证:
tauri build生成 MSI(31 MB)+ NSIS(22.3 MB)安装包。- ThingHK.exe(18.3 MB)通过 tauri.conf.json
"resources": ["binaries/*"]打包进安装包。 BaseDirectory::Resource在 release 模式下正确解析到<exe_dir>/binaries/ThingHK.exe(Tauri 2 在打包后指向 exe 所在目录,而非resources/子目录)。- Release exe 直接运行:Kernel 自动启动成功,ready=True,sensorCount=117。
打包踩坑:tauri build 在 TRAE 环境失败 error: invalid value '1' for '--ci'。TRAE 环境设置了 CI=1,导致 tauri CLI 误解析。修复:构建前清除 $env:CI = $null。
6. 文件索引
C# Kernel
| 文件 | 职责 |
|---|---|
| ThingHK.csproj | .NET 8 AOT 配置,Microsoft.NET.Sdk.Web + ASP.NET Core minimal API |
| Program.cs | serve/scan 双子命令(System.CommandLine),默认无参数等价 serve --port 8730 --basic |
| Contracts.cs | 数据契约 + ThingHKJsonContext(JSON 源生成,camelCase) |
| HardwareManager.cs | HardwareManager(封装 LHB Computer,快慢通道分类)+ SamplingScheduler(PeriodicTimer 分频)+ SnapshotCache(线程安全缓存)+ SnapshotVisitor(递归收集) |
| HttpEndpoints.cs | 6 个路由(含 /shutdown 优雅关闭)+ 全局异常中间件 + KernelHost(统一生命周期) |
| rd.xml | LHB 反射根描述符,preserve="all" 兜底 AOT trimming |
Rust 集成
| 文件 | 职责 |
|---|---|
| monitor_kernel.rs | Tauri 侧 Kernel 客户端:复制二进制、构造 StartProcessParams、轮询就绪、SSE 订阅、写入熔断 + 自动重连、ShellExecuteW 提权启动 |
| lib.rs | 7 个 monitor 命令注册 + setup 自动启动 |
| process_manager.rs | 通用进程管理(复用,含崩溃重启 + Job Object) |
前端
| 文件 | 职责 |
|---|---|
| monitorStore.ts | Pinia store:状态管理 + 事件订阅 + connState 状态机 + findSensorValue |
| MonitorModule.vue | 3 Tab UI(概览/详细/设置)+ 关键指标卡 + SVG sparkline + Accordion 分组 |
| index.ts | 模块配置和 lifecycle 钩子 |
二进制
| 文件 | 说明 |
|---|---|
| src-tauri/binaries/ThingHK.exe | Kernel AOT 产物(18.3 MB),随安装包分发,运行时复制到 {app_data_dir}/monitor/cores/ThingHK.exe |
用法
# 默认 serve(无参数等价)
ThingHK.exe serve --port 8730 --basic --fast-ms 1000 --slow-ms 5000
# 传感器覆盖矩阵测试
ThingHK.exe scan --json --basic
7. 提权机制
7.1 背景
LibreHardwareMonitor 访问 CPU MSR(温度/时钟)、存储 SMART、SMBus、EC 传感器需要管理员权限。普通权限下可读传感器受限:
| 权限 | 分组 | 传感器数 | 可读项 |
|---|---|---|---|
| 普通 | cpu/memory/gpuintel | 62 | CPU 负载/功率、内存、GPU 负载(温度/时钟全 null) |
| 管理员 | cpu/memory/gpuintel/storage | 117 | 完整温度/时钟/电压/存储 SMART |
7.2 实现方案
采用 ShellExecuteW "runas" 弹 UAC 提权启动 Kernel,架构上与普通模式分离:
- 启动:monitor_kernel.rs
start_elevated→ 停止当前 Kernel →ShellExecuteW "runas"启动管理员 Kernel(隐藏控制台窗口)→ 等待 ready → 重新订阅 SSE - 停止:调用 Kernel 的
POST /shutdown接口(普通权限无法TerminateProcess管理员进程) - 状态查询:通过 HTTP
/status判断运行状态(pid 不可用,返回 None)
7.3 提权模式限制
| 能力 | 普通模式 | 提权模式 |
|---|---|---|
| 进程管理 | ProcessManager(kill/restart/Job Object) | 不受管控(跨权限级别句柄不可用) |
| 崩溃重启 | 3s 巡检 + 自动重启 | 不支持(需手动重新提权) |
| 停止方式 | ProcessManager.stop(kill) |
POST /shutdown(优雅退出) |
| PID 查询 | 可用 | 不可用(返回 None) |
| UAC 弹窗 | 无 | 每次启动需确认 |
7.4 C# Kernel /shutdown 接口
HttpEndpoints.cs 增加 POST /shutdown 路由,触发 CancellationTokenSource.Cancel 让 app.RunAsync 优雅退出。响应先返回 200,延迟 100ms 后取消,确保响应体完整发送。
8. 后续可选优化
- ProcessManager Mutex 锁阻塞问题:
check_and_cleanup中 800mssleep(端口等待)在持有processesMutex 锁的情况下执行,会阻塞所有进程状态查询。建议将重启逻辑移出锁作用域(已知问题,与 proxy 模块共享)。 - 免 UAC 提权:当前每次提权需弹 UAC,可通过 Windows 任务计划程序注册"以最高权限运行"的任务实现免 UAC 启动(需一次性管理员权限注册任务)。
- 多机型回归:AMD Ryzen / NVIDIA GPU / 笔记本场景的实际传感器覆盖度验证(需实际硬件)。
- Kernel 版本管理:
prepare_kernel只在文件不存在时复制,不会覆盖更新。建议增加版本比对自动更新。