Files
Thing/ThingHK/ThingHK_GUIDE.md
T

246 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](file:///d:/Atie/Gitea/Thing/src-tauri/src/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=8DropOldest 防慢消费者阻塞)
- **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` 判就绪 → 订阅 `/stream` SSE → 解析事件 → `emit("monitor-data")`
- **写入熔断 + 自动重连**:SSE 断开后停止转发,3 秒退避后重试;同时监听 `process-status-changed` 事件,Kernel 由 ProcessManager 自动重启恢复 Running 后主动重新订阅 SSE。
### 2.3 前端(Vue 3
- **Pinia store**[monitorStore.ts](file:///d:/Atie/Gitea/Thing/src/stores/monitorStore.ts)):管理 status/snapshot 状态,订阅 Tauri 事件,提供 `connState` 状态机(idle/loading/connected/disconnected/error)。
- **MonitorModule.vue**[MonitorModule.vue](file:///d:/Atie/Gitea/Thing/src/modules/monitor/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}` |
### 3.2 数据契约(显式版本化)
为避免 Kernel 升级后前端解析炸裂,数据 schema 显式版本化。前端按 `schemaVersion` 解析;Kernel 升级 schema 时递增版本号并提供兼容期。当前 `schemaVersion=1`
```jsonc
{
"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 命令与事件
**6 个命令**
| 命令 | 作用 |
|------|------|
| `monitor_kernel_info` | 查询 Kernel 二进制路径/是否存在/端口 |
| `monitor_status` | 查询运行状态(running/pid/ready/sensorCount/restartCount |
| `monitor_start` | 启动 Kernel + 自动订阅 SSE(命令和 setup 共用 `start_with_subscription` |
| `monitor_stop` | 停止 Kernel + 取消 SSE 订阅 + 取消自动重连监听 |
| `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` 模式额外多出 Network60 个传感器)和 Motherboard(本机 0 个,需更深驱动/权限)。
**非提权降级**LHB 访问 SMBus、部分 EC 传感器、某些 GPU 传感器需要管理员权限。默认非提权运行,覆盖大部分 CPU/GPU 温度(通过 OHM RPC/WMI 仍可读),牺牲部分主板/电压传感器。提权策略(任务计划程序免 UAC)作为可选优化。
## 5. 实现要点与经验教训
### 5.1 C# KernelNative AOT
**关键配置**[ThingHK.csproj](file:///d:/Atie/Gitea/Thing/ThingHK/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 踩坑**
1. **匿名类型无法序列化**`Results.Ok(new { ... })` 在 AOT 下抛 `JsonTypeInfo metadata ... was not provided`。所有响应改用强类型 + `Results.Json(obj, ThingHKJsonContext.Default.T)`,新增 `HealthResponse`/`ErrorResponse` 类型注册到 `[JsonSerializable]`
2. **async lambda 返回类型**`app.MapPost("/config", async (ctx) => { return Results.Json(...); })` 在 AOT 下返回空响应体。原因:async lambda 返回 `IResult` 被框架当成 `Task<IResult>` 的未等待任务。修复:显式声明 `async Task<IResult> (HttpContext ctx) =>`
3. **HardwareType 枚举变更**LHB 0.9.5 的 `HardwareType``Controller`,改为 `SuperIO` + `EmbeddedController`
4. **Computer 非 IDisposable**LHB 0.9.5 的 `Computer` 类未实现 `IDisposable``HardwareManager.Dispose()` 改用显式 `_computer.Close()`
### 5.2 Rust 集成
**关键架构决策**
1. **自动启动策略**:参照 proxy 模块的 `auto_start_on_launch` 模式,在 `setup` 中用 `tauri::async_runtime::spawn` 异步拉起 Kernel。硬件监控为被动读取、无副作用(不修改系统状态),故默认启用,无需用户配置开关。
2. **`start_with_subscription` 抽取**:将"启动进程 + 注册自动重连 + 启动 SSE 订阅"封装为单一方法,供 `monitor_start` 命令和 setup 自动启动复用,避免两条路径行为漂移。
3. **`MonitorKernel` 改为 `Clone` + `Arc<Mutex>` 共享状态**`tauri::State::inner()` 返回 `&T` 而非 `&Arc<T>`,无法直接 clone 出 `Arc<MonitorKernel>`。将 `sub_handle``listener_ids` 改为 `Arc<Mutex<...>>``MonitorKernel` 派生 `Clone`,clone 出的实例与原实例共享订阅控制状态。
4. **`sse_client``client` 分离**`client` 带 30s 超时,用于 `/status``/snapshot` 等短请求;`sse_client` 无超时,专用于 `/stream` 长连接。早期版本对 SSE 请求误用 `timeout(Duration::from_secs(0))`,导致 reqwest 立即超时失败。
5. **写入熔断 + 自动重连**`subscribe_once` 收到 SSE 断开后停止 `emit`,3 秒退避后重试;Kernel 不响应则退出循环。同时监听 `process-status-changed` 事件,Kernel 由 ProcessManager 自动重启恢复 Running 后主动重新订阅 SSE。
**Rust 踩坑**
1. **Tauri 2 async 命令必须返回 `Result<T, E>`**`AsyncCommandMustReturnResult` 未实现 for `MonitorStatus`。修复:`monitor_status` 返回类型从 `MonitorStatus` 改为 `Result<MonitorStatus, String>`
2. **Kernel `/status` 反序列化失败**C# 默认输出 PascalCase`Ready``IsAdmin`),Rust 期望 camelCase。修复:`Contracts.cs``JsonSourceGenerationOptions` 添加 `PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase`
3. **崩溃重连无并发订阅问题**:旧 SSE 循环在 `is_kernel_alive` 返回 false(端口未释放)时正确退出,`register_auto_reconnect` 启动新订阅,netstat 确认仅 1 个 ESTABLISHED 连接。
### 5.3 前端
**关键设计决策**
1. **状态机 `connState`**:基于 status + eventCount + lastEventTime 推断 5 种状态(idle/loading/connected/disconnected/error),5 秒未收到 monitor-data 事件判定为 disconnected。
2. **`findSensorValue` 多条件查找**:支持 groupId + name + hardwareName + type 四维匹配,name 用精确匹配 + includes 子串兜底。用于解决 Memory 分组有两条同名 "Memory" 传感器(Virtual/Total)的歧义。
3. **SVG sparkline 无外部依赖**:关键指标历史用 30 点环形 bufferSVG path 自绘,不引入 Chart.js(符合"最小安装、少依赖"偏好)。
4. **`schemaVersion` 守卫**store 订阅 monitor-data 时检查 `schemaVersion !== 1` 并 warn,未来 Kernel 升级 schema 时前端不会静默解析错误数据。
5. **模块卸载不停止 Kernel**`dispose` 仅取消事件订阅,不调用 `monitor_stop`。Kernel 生命周期由 ProcessManager 全局管理,与模块 UI 生命周期解耦。
6. **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 查找 |
**前端踩坑**
1. **`lucide-vue-next` 模块未找到**:项目实际使用 `@lucide/vue`(见 package.json),而非 `lucide-vue-next`
2. **`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=TruesensorCount=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` 生成 MSI31 MB+ NSIS22.3 MB)安装包。
- ThingHK.exe18.3 MB)通过 [tauri.conf.json](file:///d:/Atie/Gitea/Thing/src-tauri/tauri.conf.json) `"resources": ["binaries/*"]` 打包进安装包。
- `BaseDirectory::Resource` 在 release 模式下正确解析到 `<exe_dir>/binaries/ThingHK.exe`Tauri 2 在打包后指向 exe 所在目录,而非 `resources/` 子目录)。
- Release exe 直接运行:Kernel 自动启动成功,ready=TruesensorCount=117。
**打包踩坑**`tauri build` 在 TRAE 环境失败 `error: invalid value '1' for '--ci'`。TRAE 环境设置了 `CI=1`,导致 tauri CLI 误解析。修复:构建前清除 `$env:CI = $null`
## 6. 文件索引
### C# Kernel
| 文件 | 职责 |
|------|------|
| [ThingHK.csproj](file:///d:/Atie/Gitea/Thing/ThingHK/ThingHK.csproj) | .NET 8 AOT 配置,`Microsoft.NET.Sdk.Web` + ASP.NET Core minimal API |
| [Program.cs](file:///d:/Atie/Gitea/Thing/ThingHK/Program.cs) | `serve`/`scan` 双子命令(System.CommandLine),默认无参数等价 `serve --port 8730 --basic` |
| [Contracts.cs](file:///d:/Atie/Gitea/Thing/ThingHK/Contracts.cs) | 数据契约 + `ThingHKJsonContext`JSON 源生成,camelCase |
| [HardwareManager.cs](file:///d:/Atie/Gitea/Thing/ThingHK/HardwareManager.cs) | `HardwareManager`(封装 LHB Computer,快慢通道分类)+ `SamplingScheduler`PeriodicTimer 分频)+ `SnapshotCache`(线程安全缓存)+ `SnapshotVisitor`(递归收集) |
| [HttpEndpoints.cs](file:///d:/Atie/Gitea/Thing/ThingHK/HttpEndpoints.cs) | 5 个路由 + 全局异常中间件 + `KernelHost`(统一生命周期) |
| [rd.xml](file:///d:/Atie/Gitea/Thing/ThingHK/rd.xml) | LHB 反射根描述符,`preserve="all"` 兜底 AOT trimming |
### Rust 集成
| 文件 | 职责 |
|------|------|
| [monitor_kernel.rs](file:///d:/Atie/Gitea/Thing/src-tauri/src/monitor_kernel.rs) | Tauri 侧 Kernel 客户端:复制二进制、构造 `StartProcessParams`、轮询就绪、SSE 订阅、写入熔断 + 自动重连 |
| [lib.rs](file:///d:/Atie/Gitea/Thing/src-tauri/src/lib.rs) | 6 个 monitor 命令注册 + setup 自动启动 |
| [process_manager.rs](file:///d:/Atie/Gitea/Thing/src-tauri/src/process_manager.rs) | 通用进程管理(复用,含崩溃重启 + Job Object |
### 前端
| 文件 | 职责 |
|------|------|
| [monitorStore.ts](file:///d:/Atie/Gitea/Thing/src/stores/monitorStore.ts) | Pinia store:状态管理 + 事件订阅 + `connState` 状态机 + `findSensorValue` |
| [MonitorModule.vue](file:///d:/Atie/Gitea/Thing/src/modules/monitor/MonitorModule.vue) | 3 Tab UI(概览/详细/设置)+ 关键指标卡 + SVG sparkline + Accordion 分组 |
| [index.ts](file:///d:/Atie/Gitea/Thing/src/modules/monitor/index.ts) | 模块配置和 lifecycle 钩子 |
### 二进制
| 文件 | 说明 |
|------|------|
| [src-tauri/binaries/ThingHK.exe](file:///d:/Atie/Gitea/Thing/src-tauri/binaries/ThingHK.exe) | Kernel AOT 产物(18.3 MB),随安装包分发,运行时复制到 `{app_data_dir}/monitor/cores/ThingHK.exe` |
### 用法
```bash
# 默认 serve(无参数等价)
ThingHK.exe serve --port 8730 --basic --fast-ms 1000 --slow-ms 5000
# 传感器覆盖矩阵测试
ThingHK.exe scan --json --basic
```
## 7. 后续可选优化
以下优化不阻塞当前版本发布,可在后续迭代中考虑:
1. **ProcessManager Mutex 锁阻塞问题**`check_and_cleanup` 中 800ms `sleep`(端口等待)在持有 `processes` Mutex 锁的情况下执行,会阻塞所有进程状态查询。建议将重启逻辑移出锁作用域(已知问题,与 proxy 模块共享)。
2. **提权策略**:通过 Windows 任务计划程序实现免 UAC 管理员启动,解锁 SMBus/EC/部分 GPU 传感器。当前以普通权限运行,覆盖大部分 CPU/GPU/内存/存储温度。
3. **多机型回归**AMD Ryzen / NVIDIA GPU / 笔记本场景的实际传感器覆盖度验证(需实际硬件)。
4. **Kernel 版本管理**`prepare_kernel` 只在文件不存在时复制,不会覆盖更新。建议增加版本比对自动更新。