Files
Thing/ThingHK/ThingHK_GUIDE.md
T
2026-07-29 10:07:25 +08:00

412 lines
25 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}` |
| `/shutdown` | POST | 优雅关闭(提权模式下由 Tauri 调用,普通权限无法 kill 管理员进程) |
### 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 命令与事件
**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` 模式额外多出 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) | 6 个路由(含 `/shutdown` 优雅关闭)+ 全局异常中间件 + `KernelHost`(统一生命周期) |
| [rd.xml](file:///d:/Atie/Gitea/Thing/ThingHK/rd.xml) | LHB 反射根描述符,`preserve="all"` 兜底 AOT trimming |
## 7. FPS 监控方案(后续计划,暂不实施)
为扩展硬件监控模块对游戏 FPS 的支持,调研了四种实现方案。当前作为后续规划储备,暂不实施。
> 注:本节为后续规划,与第 8 节提权机制章节编号独立,不影响现有架构。
### 7.1 方案对比
| 维度 | ① DWM API | ② ETW Present | ③ PresentMon | ④ RTSS |
|------|-----------|---------------|--------------|--------|
| 依赖 | 无(系统内置) | 无(系统内置) | PresentMon Service(需安装 MSI ~30MB | RTSS(需安装) |
| 体积增量 | 0 KB | 0 KB | ~30MB+ | 外部进程 |
| 代码量 | ~60 行 | ~400 行 | ~1000+ 行(跨栈) | ~200 行(读共享内存) |
| 精度 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 1% Low | ❌ | ❌(需自行实现统计算法) | ✅ 内置 | ✅ |
| 进程级监控 | ❌ | ✅ | ✅ | ✅ |
| 全屏独占兼容 | ❌ | ✅ | ✅ | ✅ |
| 需管理员权限 | ❌ | ✅ | ✅ | ❌ |
| 桌面静止时 | 显示刷新率(60/144,误导) | FPS=0(准确) | FPS=0(准确) | FPS=0 |
| AOT 风险 | 低 | 低(P/Invoke | 中(P/Invoke C API | 低 |
| 与"最小依赖"偏好 | ✅ 完美契合 | ✅ 契合 | ❌ 冲突 | ❌ 冲突 |
### 7.2 方案 ①:DWM API(最简单,降级方案)
**原理**:调用 `DwmGetCompositionTimingInfo` 获取 DWM 合成帧计数,两次采样差值 / 时间差 = FPS。
**优点**
- 零依赖、零体积增量,`dwmapi.dll` 是 Windows 系统组件
- 实现极简(~60 行),AOT 原生友好
- 无需提权
**缺点**
- 全屏独占(FS Exclusive)模式下 DWM 不参与合成,FPS 读数为 0
- 桌面静止时仍以显示器刷新率合成,FPS 显示 60/144(误导)
- 仅全局 FPS,无法按进程过滤
**定位**:降级方案。在用户未提权或仅需粗略 FPS 时使用。
**核心 P/Invoke**
```csharp
[DllImport("dwmapi.dll", PreserveSig = false)]
static extern void DwmGetCompositionTimingInfo(IntPtr hwnd, ref DWM_TIMING_INFO pTimingInfo);
```
### 7.3 方案 ②:自己监听 ETW 的 Present 事件(推荐主力方案)
**原理**:订阅 `Microsoft-Windows-DXGI`GUID `CA11C036-0102-4A2D-A6AD-F03CEDF728C7`)的 Present 事件(Event ID 0),滑动窗口统计单位时间 Present 调用次数 = FPS。
**实现路径**:原生 P/Invoke `advapi32.dll` 的 ETW API`StartTraceW``EnableTraceEx2``OpenTrace``ProcessTrace`),零依赖。
**优点**
- 零依赖、零体积增量
- 进程级监控(按 PID 过滤前台游戏)
- 全屏独占兼容(DXGI Present 事件仍触发)
- AOT 原生友好
**缺点**
- 需管理员权限(与 ThingHK 提权体系兼容)
- 实现复杂度中高(~400 行):需手工定义 `EVENT_TRACE_PROPERTIES`/`EVENT_RECORD` 等结构体
- DXGI Present 事件 payload 无公开 schema,需参考 PresentMon 源码硬编码偏移
- `ProcessTrace` 是阻塞调用,需后台线程管理生命周期
- 会话未正确清理会残留 ETW 会话影响系统性能
**FPS 计算**:1 秒滑动窗口,500ms 清理过期时间戳,`FPS = 队列长度 / 窗口时长`
**实现步骤**
1. 新增 `EtwFpsMonitor.cs`P/Invoke + 会话管理 + FPS 计算)
2. `KernelHost` 持有实例,启动时检测管理员权限,未提权则 `Fps=null`
3. `Contracts.cs``SensorSnapshot` 增加 `Fps` 字段,递增 `SchemaVersion`
4. `SamplingScheduler.FastLoopAsync` 每 tick 调用 `SampleFps()`
5. 前端 OSD/详情页增加"FPS"特殊项
**参考**[PresentMon/IntelPresentMon/PresentData/ETW/Microsoft_Windows_DXGI.cpp](https://github.com/GameTechDev/PresentMon/blob/main/IntelPresentMon/PresentData/ETW/Microsoft_Windows_DXGI.cpp)
### 7.4 方案 ③:PresentMon Service + API(功能最全,但依赖重)
**原理**:安装 PresentMon Service(官方 MSI),ThingHK 通过 P/Invoke `PresentMonAPI2.dll` 连接 Service 获取 140+ 指标。
**部署约束(关键)**
- Service 是独立进程,不能"只丢几个 DLL"
- 官方明确警告:客户端自带 DLL 不保证与服务二进制兼容,**必须用官方 MSI 安装**
- MSI 含 CEF 运行时,安装包 ~30MB,安装后常驻进程 ~30-50MB 内存
- Service 需管理员权限
**API 用法**
```c
pmOpenSession(&hSession);
pmStartTrackingProcess(hSession, processId);
pmRegisterDynamicQuery(hSession, &hQuery, elements, 2, 1000, 0);
pmPollDynamicQuery(hQuery, processId, blob, &numSwapChains);
```
**优点**:功能最全(FPS / 1% Low / 0.1% Low / 帧时间 / GPU 延迟 / 功耗),官方维护。
**缺点**
- 与"minimal installation size, few dependencies"偏好冲突
- 实现复杂度高(~1000+ 行跨栈:C# P/Invoke + Rust 下载安装管理 + 前端)
- AOT 下 P/Invoke C API 需验证
- "游戏模式按需下载"实际是下载 MSI 静默安装,体验重
**定位**:不推荐。若用户已自行安装 PresentMon Service,可检测后切换为高精度模式(不在应用内下载安装)。
### 7.5 方案 ④:RTSS(需安装第三方软件)
**原理**RTSSRivaTuner Statistics Server)提供共享内存暴露 FPS/帧时间,ThingHK 读取共享内存。
**优点**:成熟稳定,精度高,支持 1% Low。
**缺点**
- 需用户安装 RTSS(商业软件,与 MSi Afterburner 捆绑)
- 共享内存 schema 无官方文档,需逆向
- 与"few dependencies"偏好冲突
**定位**:不推荐。仅作为已安装 RTSS 用户的可选对接。
### 7.6 推荐路线
**分两步走**
1. **先实现方案 ①(DWM API**:验证 FPS 展示链路(数据契约 → SSE → 前端 OSD)打通,0 风险、0 依赖。UI 标注"桌面合成 FPS"。
2. **升级为方案 ②(ETW Present**:替换 `Sample()` 实现,前端无改动,数据契约不变。获得精确的进程级 FPS。
**统一数据契约**(两种方案共用):
```csharp
public float? Fps { get; set; } // 实时 FPS(无游戏时为 0 或 null)
public uint? FpsProcessId { get; set; } // 目标进程(可选,0=全局)
```
**权限策略**
- 未提权:降级为 DWM API 或返回 null
- 已提权:启用 ETW 方案
### Rust 集成
| 文件 | 职责 |
|------|------|
| [monitor_kernel.rs](file:///d:/Atie/Gitea/Thing/src-tauri/src/monitor_kernel.rs) | Tauri 侧 Kernel 客户端:复制二进制、构造 `StartProcessParams`、轮询就绪、SSE 订阅、写入熔断 + 自动重连、ShellExecuteW 提权启动 |
| [lib.rs](file:///d:/Atie/Gitea/Thing/src-tauri/src/lib.rs) | 7 个 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
```
## 8. 提权机制
### 8.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](file:///d:/Atie/Gitea/Thing/src-tauri/src/monitor_kernel.rs) `start_elevated` → 停止当前 Kernel → `ShellExecuteW "runas"` 启动管理员 Kernel(隐藏控制台窗口)→ 等待 ready → 重新订阅 SSE
- **停止**:调用 Kernel 的 `POST /shutdown` 接口(普通权限无法 `TerminateProcess` 管理员进程)
- **状态查询**:通过 HTTP `/status` 判断运行状态(pid 不可用,返回 None)
### 7.3 提权模式限制
| 能力 | 普通模式 | 提权模式 |
|------|----------|----------|
| 进程管理 | ProcessManagerkill/restart/Job Object | 不受管控(跨权限级别句柄不可用) |
| 崩溃重启 | 3s 巡检 + 自动重启 | 不支持(需手动重新提权) |
| 停止方式 | `ProcessManager.stop`kill | `POST /shutdown`(优雅退出) |
| PID 查询 | 可用 | 不可用(返回 None) |
| UAC 弹窗 | 无 | 每次启动需确认 |
### 7.4 C# Kernel /shutdown 接口
[HttpEndpoints.cs](file:///d:/Atie/Gitea/Thing/ThingHK/HttpEndpoints.cs) 增加 `POST /shutdown` 路由,触发 `CancellationTokenSource.Cancel``app.RunAsync` 优雅退出。响应先返回 200,延迟 100ms 后取消,确保响应体完整发送。
## 9. 后续可选优化
1. **ProcessManager Mutex 锁阻塞问题**`check_and_cleanup` 中 800ms `sleep`(端口等待)在持有 `processes` Mutex 锁的情况下执行,会阻塞所有进程状态查询。建议将重启逻辑移出锁作用域(已知问题,与 proxy 模块共享)。
2. **免 UAC 提权**:当前每次提权需弹 UAC,可通过 Windows 任务计划程序注册"以最高权限运行"的任务实现免 UAC 启动(需一次性管理员权限注册任务)。
3. **多机型回归**AMD Ryzen / NVIDIA GPU / 笔记本场景的实际传感器覆盖度验证(需实际硬件)。
4. **Kernel 版本管理**`prepare_kernel` 只在文件不存在时复制,不会覆盖更新。建议增加版本比对自动更新。