监控模块 优化

This commit is contained in:
zhongluofeng
2026-07-24 18:16:34 +08:00
parent e8267c244f
commit 7e6149355e
15 changed files with 2148 additions and 234 deletions
+4
View File
@@ -132,4 +132,8 @@ internal sealed class ErrorResponse
[JsonSerializable(typeof(ConfigRequest[]))]
[JsonSerializable(typeof(HealthResponse))]
[JsonSerializable(typeof(ErrorResponse))]
[JsonSerializable(typeof(HardwareConfig))]
[JsonSerializable(typeof(HardwareConfigResponse))]
[JsonSerializable(typeof(HardwareConfigUpdateResponse))]
[JsonSerializable(typeof(HardwareConfigUpdateRequest))]
internal sealed partial class ThingHKJsonContext : JsonSerializerContext;
+165
View File
@@ -0,0 +1,165 @@
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ThingHK;
/// <summary>
/// 硬件监控配置:控制启用哪些硬件分组 + 过滤哪些传感器类型。
///
/// 设计要点:
/// - 硬件开关影响 Computer.IsXxxEnabled,需重启 Kernel 才生效(LHB 在 Open 时决定枚举范围)
/// - 传感器类型过滤在 BuildSnapshot 时生效,可热更新(无需重启)
/// - 默认配置仅覆盖概览页需要的指标:CPU/GPU/Memory/Storage + temp/load/power/clock/data
/// - 配置文件由 Tauri 通过 --config 参数传递路径,Kernel 负责加载/保存
/// </summary>
internal sealed class HardwareConfig
{
/// <summary>硬件分组开关,key 为 HardwareConfigKeycpu/gpu/memory/storage/...</summary>
public Dictionary<string, bool> Hardware { get; set; } = new();
/// <summary>传感器类型开关,key 为 SensorType 小写(temperature/load/power/...</summary>
public Dictionary<string, bool> SensorTypes { get; set; } = new();
/// <summary>默认配置:仅覆盖概览页需要的指标,最小化性能开销</summary>
public static HardwareConfig Default => new()
{
Hardware = new Dictionary<string, bool>
{
["cpu"] = true,
["gpu"] = true,
["memory"] = true,
["storage"] = true,
["motherboard"] = false,
["controller"] = false,
["battery"] = false,
["network"] = false,
["psu"] = false,
},
SensorTypes = new Dictionary<string, bool>
{
["temperature"] = true,
["load"] = true,
["power"] = true,
["clock"] = true,
["voltage"] = false,
["fan"] = false,
["data"] = true,
["smalldata"] = false,
["throughput"] = false,
["level"] = false,
["control"] = false,
["factor"] = false,
["frequency"] = false,
["timespan"] = false,
["energy"] = false,
["noise"] = false,
["conductivity"] = false,
["humidity"] = false,
["flow"] = false,
},
};
/// <summary>从文件加载配置,文件不存在或解析失败时返回默认配置</summary>
public static HardwareConfig Load(string? path)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path))
return Default;
try
{
var json = File.ReadAllText(path);
var cfg = JsonSerializer.Deserialize(json, ThingHKJsonContext.Default.HardwareConfig);
if (cfg == null) return Default;
// 合并默认值:确保新增的硬件/传感器类型有默认值
MergeWithDefaults(cfg);
return cfg;
}
catch
{
return Default;
}
}
/// <summary>保存配置到文件</summary>
public void Save(string path)
{
var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
var json = JsonSerializer.Serialize(this, ThingHKJsonContext.Default.HardwareConfig);
File.WriteAllText(path, json);
}
/// <summary>检查指定传感器类型是否启用</summary>
public bool IsSensorTypeEnabled(string sensorTypeLower)
=> SensorTypes.TryGetValue(sensorTypeLower, out var enabled) ? enabled : false;
/// <summary>检查指定硬件分组是否启用</summary>
public bool IsHardwareEnabled(string key)
=> Hardware.TryGetValue(key, out var enabled) ? enabled : false;
/// <summary>合并默认值:确保配置文件缺少的新增项有默认值</summary>
private static void MergeWithDefaults(HardwareConfig cfg)
{
var def = Default;
foreach (var (k, v) in def.Hardware)
if (!cfg.Hardware.ContainsKey(k)) cfg.Hardware[k] = v;
foreach (var (k, v) in def.SensorTypes)
if (!cfg.SensorTypes.ContainsKey(k)) cfg.SensorTypes[k] = v;
}
}
/// <summary>
/// /config/hardware GET 响应:返回当前配置 + 可用硬件/传感器类型清单。
/// 前端用此数据渲染配置 Dialog。
/// </summary>
internal sealed class HardwareConfigResponse
{
public HardwareConfig Config { get; set; } = new();
public List<HardwareTypeInfo> AvailableHardware { get; set; } = new();
public List<SensorTypeInfo> AvailableSensorTypes { get; set; } = new();
}
/// <summary>硬件分组元信息</summary>
internal sealed class HardwareTypeInfo
{
/// <summary>配置 keycpu/gpu/memory/...</summary>
public string Key { get; set; } = "";
/// <summary>显示名</summary>
public string Name { get; set; } = "";
/// <summary>是否已启用</summary>
public bool Enabled { get; set; }
/// <summary>是否需要管理员权限才能读取完整数据</summary>
public bool RequiresAdmin { get; set; }
}
/// <summary>传感器类型元信息</summary>
internal sealed class SensorTypeInfo
{
/// <summary>配置 keytemperature/load/...</summary>
public string Key { get; set; } = "";
/// <summary>显示名</summary>
public string Name { get; set; } = "";
/// <summary>是否已启用</summary>
public bool Enabled { get; set; }
}
/// <summary>/config/hardware POST 响应</summary>
internal sealed class HardwareConfigUpdateResponse
{
public bool Success { get; set; }
/// <summary>硬件开关是否变化(需重启 Kernel 才生效)</summary>
public bool RestartRequired { get; set; }
public string? ConfigPath { get; set; }
}
/// <summary>/config/hardware POST 请求体</summary>
internal sealed class HardwareConfigUpdateRequest
{
/// <summary>硬件开关(可选,仅传需要变更的字段)</summary>
public Dictionary<string, bool>? Hardware { get; set; }
/// <summary>传感器类型开关(可选,仅传需要变更的字段)</summary>
public Dictionary<string, bool>? SensorTypes { get; set; }
}
+31 -12
View File
@@ -26,6 +26,7 @@ internal sealed class HardwareManager : IDisposable
private readonly Stopwatch _startupSw;
private bool _ready;
private bool _closed;
private HardwareConfig _config;
// 缓存"哪个硬件属于快通道"——按 hardware type 判断更稳定(不同机型传感器命名不一致)
// 实际分频逻辑见 SamplingScheduler,此处只暴露硬件列表给调度器
@@ -34,24 +35,31 @@ internal sealed class HardwareManager : IDisposable
public bool IsAdmin => _isAdmin;
public double ColdStartMs => _coldStartMs;
public bool Ready => _ready;
public HardwareConfig Config => _config;
public HardwareManager(bool basic)
public HardwareManager(HardwareConfig config)
{
_config = config ?? HardwareConfig.Default;
_isAdmin = IsRunningAsAdmin();
_startupSw = Stopwatch.StartNew();
// LHB 行为:主板的传感器实际通过 SubHardwareSuperIO/EC)暴露,
// 而 SuperIO 作为顶层硬件时需 IsControllerEnabled 才能枚举。
// 联动策略:启用 motherboard 时自动启用 controller,避免主板勾选后无数据。
bool motherboardEnabled = _config.IsHardwareEnabled("motherboard");
bool controllerEnabled = _config.IsHardwareEnabled("controller") || motherboardEnabled;
_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,
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();
@@ -68,6 +76,12 @@ internal sealed class HardwareManager : IDisposable
_ready = true;
}
/// <summary>热更新传感器类型过滤(无需重启 Kernel,下次 BuildSnapshot 生效)</summary>
public void UpdateConfig(HardwareConfig newConfig)
{
_config = newConfig;
}
/// <summary>
/// 全量 Update 所有硬件。
/// 由 SamplingScheduler 按通道分频调用。
@@ -130,11 +144,16 @@ internal sealed class HardwareManager : IDisposable
foreach (var s in hw.Sensors)
{
// 按配置过滤传感器类型:关闭的类型不加入快照,减少传输数据量
string sensorType = s.SensorType.ToString().ToLowerInvariant();
if (!_config.IsSensorTypeEnabled(sensorType))
continue;
g.Sensors.Add(new SensorEntry
{
Id = $"{groupId}/{hw.Name}/{s.SensorType}/{s.Name}".Replace(' ', '_').ToLowerInvariant(),
Name = s.Name,
Type = s.SensorType.ToString().ToLowerInvariant(),
Type = sensorType,
Value = s.Value,
Unit = UnitFor(s.SensorType),
HardwareName = hw.Name,
+190 -4
View File
@@ -10,13 +10,14 @@ namespace ThingHK;
/// 路由设计(见 ThingHK_GUIDE.md 第 2.3 节):
/// GET /status → 冷启动就绪探测,前端轮询判断是否可订阅
/// GET /snapshot → 一次性拉取最新缓存快照
/// GET /stream → SSE 阅,Kernel 主动推送 SensorSnapshot
/// GET /stream → SSE 阅,Kernel 主动推送 SensorSnapshot
/// POST /config → 运行时调整采样间隔
/// POST /shutdown → 优雅关闭(提权模式下由 Tauri 调用,普通权限无法 kill 管理员进程)
/// GET / → 健康检查(200 OK)
/// </summary>
internal static class HttpEndpoints
{
public static WebApplication MapThingHKEndpoints(this WebApplication app, KernelHost kernel)
public static WebApplication MapThingHKEndpoints(this WebApplication app, KernelHost kernel, CancellationTokenSource shutdownCts)
{
// 全局异常捕获中间件:把未处理异常写到 stderr,便于 AOT 下排错
app.Use(async (ctx, next) =>
@@ -122,6 +123,116 @@ internal static class HttpEndpoints
}, ThingHKJsonContext.Default.ConfigResponse);
});
// 硬件监控配置:返回当前配置 + 可用硬件/传感器类型清单
app.MapGet("/config/hardware", () =>
{
var hw = kernel.Hardware;
var cfg = hw?.Config ?? HardwareConfig.Default;
var resp = new HardwareConfigResponse
{
Config = cfg,
AvailableHardware = GetAvailableHardware(cfg),
AvailableSensorTypes = GetAvailableSensorTypes(cfg),
};
return Results.Json(resp, ThingHKJsonContext.Default.HardwareConfigResponse);
});
// 硬件监控配置更新:
// - 传感器类型过滤:热更新(下次 BuildSnapshot 生效)
// - 硬件开关:需重启 Kernel 才生效(返回 restartRequired=true
app.MapPost("/config/hardware", async Task<IResult> (HttpContext ctx) =>
{
using var reader = new StreamReader(ctx.Request.Body);
var body = await reader.ReadToEndAsync(ctx.RequestAborted);
HardwareConfigUpdateRequest? req;
try
{
req = string.IsNullOrWhiteSpace(body)
? new HardwareConfigUpdateRequest()
: JsonSerializer.Deserialize(body, ThingHKJsonContext.Default.HardwareConfigUpdateRequest);
}
catch (JsonException ex)
{
ctx.Response.StatusCode = 400;
return Results.Json(new ErrorResponse("invalid_json", ex.Message), ThingHKJsonContext.Default.ErrorResponse);
}
if (req == null)
{
ctx.Response.StatusCode = 400;
return Results.Json(new ErrorResponse("invalid_body", "请求体为空"), ThingHKJsonContext.Default.ErrorResponse);
}
var hw = kernel.Hardware;
if (hw == null)
{
ctx.Response.StatusCode = 503;
return Results.Json(new ErrorResponse("not_ready", "Kernel 尚未就绪"), ThingHKJsonContext.Default.ErrorResponse);
}
var oldConfig = hw.Config;
var newConfig = new HardwareConfig
{
Hardware = new Dictionary<string, bool>(oldConfig.Hardware),
SensorTypes = new Dictionary<string, bool>(oldConfig.SensorTypes),
};
bool hardwareChanged = false;
if (req.Hardware != null)
{
foreach (var (k, v) in req.Hardware)
{
if (newConfig.Hardware.TryGetValue(k, out var oldVal) && oldVal != v)
{
newConfig.Hardware[k] = v;
hardwareChanged = true;
}
else if (!newConfig.Hardware.ContainsKey(k))
{
newConfig.Hardware[k] = v;
hardwareChanged = true;
}
}
}
if (req.SensorTypes != null)
{
foreach (var (k, v) in req.SensorTypes)
newConfig.SensorTypes[k] = v;
}
// 热更新传感器类型过滤(立即生效)
hw.UpdateConfig(newConfig);
// 保存到文件(供下次启动加载)
if (!string.IsNullOrEmpty(kernel.ConfigPath))
{
try { newConfig.Save(kernel.ConfigPath); }
catch (Exception ex) { Console.Error.WriteLine($"[ThingHK] 保存配置失败: {ex.Message}"); }
}
return Results.Json(new HardwareConfigUpdateResponse
{
Success = true,
RestartRequired = hardwareChanged,
ConfigPath = kernel.ConfigPath,
}, ThingHKJsonContext.Default.HardwareConfigUpdateResponse);
});
// 优雅关闭:触发 CancellationTokenSource.Cancel,让 app.RunAsync 退出。
// 提权模式下 Tauri(普通权限)无法 TerminateProcess 管理员进程,通过此接口让 Kernel 自行退出。
// 普通权限模式下 ProcessManager.stop 直接 kill 更快,此接口作为统一兜底。
app.MapPost("/shutdown", () =>
{
Console.Error.WriteLine("[ThingHK] 收到 /shutdown 请求,准备退出");
// 延迟取消,确保响应先返回
_ = Task.Run(async () =>
{
await Task.Delay(100);
shutdownCts.Cancel();
});
return Results.Json(new HealthResponse { Ok = true, Name = "ThingHK", Version = "0.2.0" }, ThingHKJsonContext.Default.HealthResponse);
});
return app;
}
@@ -138,6 +249,75 @@ internal static class HttpEndpoints
await ctx.Response.WriteAsync("\n\n", ct).ConfigureAwait(false);
await ctx.Response.Body.FlushAsync(ct).ConfigureAwait(false);
}
/// <summary>可用硬件分组清单(固定列表,供前端 Dialog 渲染)</summary>
private static List<HardwareTypeInfo> GetAvailableHardware(HardwareConfig cfg)
{
// (key, displayName, requiresAdmin)
var items = new (string, string, bool)[]
{
("cpu", "CPU", false),
("gpu", "GPUIntel/AMD/NVIDIA", false),
("memory", "内存", false),
("storage", "存储", true),
("motherboard", "主板", true),
("controller", "SuperIO/嵌入式控制器", true),
("battery", "电池", false),
("network", "网络", false),
("psu", "电源", true),
};
var list = new List<HardwareTypeInfo>(items.Length);
foreach (var (key, name, reqAdmin) in items)
{
list.Add(new HardwareTypeInfo
{
Key = key,
Name = name,
Enabled = cfg.IsHardwareEnabled(key),
RequiresAdmin = reqAdmin,
});
}
return list;
}
/// <summary>可用传感器类型清单(固定列表,供前端 Dialog 渲染)</summary>
private static List<SensorTypeInfo> GetAvailableSensorTypes(HardwareConfig cfg)
{
// (key, displayName)
var items = new (string, string)[]
{
("temperature", "温度"),
("load", "负载"),
("power", "功率"),
("clock", "时钟"),
("voltage", "电压"),
("fan", "风扇"),
("data", "容量"),
("smalldata", "小容量"),
("throughput", "吞吐"),
("level", "等级"),
("control", "控制"),
("factor", "因子"),
("frequency", "频率"),
("timespan", "时长"),
("energy", "能量"),
("noise", "噪声"),
("conductivity", "电导率"),
("humidity", "湿度"),
("flow", "流量"),
};
var list = new List<SensorTypeInfo>(items.Length);
foreach (var (key, name) in items)
{
list.Add(new SensorTypeInfo
{
Key = key,
Name = name,
Enabled = cfg.IsSensorTypeEnabled(key),
});
}
return list;
}
}
/// <summary>
@@ -149,11 +329,17 @@ internal sealed class KernelHost : IDisposable
public HardwareManager? Hardware { get; private set; }
public SamplingScheduler Scheduler { get; private set; } = null!;
public Stopwatch Uptime { get; } = Stopwatch.StartNew();
/// <summary>配置文件路径(供 /config/hardware POST 保存)</summary>
public string? ConfigPath { get; private set; }
public async Task StartAsync(bool basic, int fastMs, int slowMs)
public async Task StartAsync(string? configPath, int fastMs, int slowMs)
{
ConfigPath = configPath;
var config = HardwareConfig.Load(configPath);
Console.Error.WriteLine($"[ThingHK] 加载配置: {configPath ?? "()"}, 硬件={config.Hardware.Count}项 传感器类型={config.SensorTypes.Count}项");
// 1. 初始化硬件层(含首轮 Update,约 5 秒)
Hardware = new HardwareManager(basic);
Hardware = new HardwareManager(config);
Console.Error.WriteLine($"[ThingHK] 硬件就绪: {Hardware.AllHardware.Count} 设备, 冷启动 {Hardware.ColdStartMs:F0}ms, admin={Hardware.IsAdmin}");
// 2. 启动采样调度器(会立即推送首个快照到广播 channel)
+48 -16
View File
@@ -27,24 +27,24 @@ internal static class Program
// 无参数 → 默认 serve(最常见用法)
if (args.Length == 0)
{
return await ServeAsync(DefaultPort, basic: true, DefaultFastMs, DefaultSlowMs);
return await ServeAsync(DefaultPort, null, DefaultFastMs, DefaultSlowMs, null);
}
// serve 子命令
var serveCmd = new Command("serve", "启动 HTTP 服务");
var portOpt = new Option<int>("--port", () => DefaultPort, "监听端口");
var basicOpt = new Option<bool>("--basic", "仅启用核心四类硬件,降低冷启动耗时");
var fullOpt = new Option<bool>("--full", "启用全部硬件(含主板/控制器/网络等)");
var configOpt = new Option<FileInfo?>("--config", "硬件监控配置文件路径(JSON");
var fastOpt = new Option<int>("--fast-ms", () => DefaultFastMs, "快通道采样间隔(毫秒)");
var slowOpt = new Option<int>("--slow-ms", () => DefaultSlowMs, "慢通道采样间隔(毫秒)");
var logFileOpt = new Option<FileInfo?>("--log-file", "将 stderr 日志写入指定文件(提权模式下控制台隐藏时使用)");
serveCmd.AddOption(portOpt);
serveCmd.AddOption(basicOpt);
serveCmd.AddOption(fullOpt);
serveCmd.AddOption(configOpt);
serveCmd.AddOption(fastOpt);
serveCmd.AddOption(slowOpt);
serveCmd.SetHandler((port, basic, full, fast, slow) =>
ServeAsync(port, basic || !full, fast, slow),
portOpt, basicOpt, fullOpt, fastOpt, slowOpt);
serveCmd.AddOption(logFileOpt);
serveCmd.SetHandler((port, config, fast, slow, logFile) =>
ServeAsync(port, config?.FullName, fast, slow, logFile),
portOpt, configOpt, fastOpt, slowOpt, logFileOpt);
// scan 子命令(阶段一覆盖矩阵测试,保留)
var scanCmd = new Command("scan", "扫描一次并输出覆盖矩阵(阶段一验证用)");
@@ -58,17 +58,37 @@ internal static class Program
root.Add(serveCmd);
root.Add(scanCmd);
// 兼容阶段一的旧用法:ThingHK.exe --json --basic
root.SetHandler(() => ServeAsync(DefaultPort, basic: true, DefaultFastMs, DefaultSlowMs));
root.SetHandler(() => ServeAsync(DefaultPort, null, DefaultFastMs, DefaultSlowMs, null));
return await root.InvokeAsync(args);
}
private static async Task<int> ServeAsync(int port, bool basic, int fastMs, int slowMs)
private static async Task<int> ServeAsync(int port, string? configPath, int fastMs, int slowMs, FileInfo? logFile)
{
Console.Error.WriteLine($"[ThingHK] serve 模式: port={port} basic={basic} fast={fastMs}ms slow={slowMs}ms");
// 如果指定了 --log-file,将 stderr 重定向到文件(提权模式下控制台隐藏,需要日志排查崩溃)
if (logFile != null)
{
try
{
var logStream = new StreamWriter(logFile.Open(FileMode.Create, FileAccess.Write, FileShare.Read))
{
AutoFlush = true,
};
Console.SetError(logStream);
// 同时重定向 stdout(避免 ASP.NET 的某些输出到 stdout
Console.SetOut(logStream);
Console.Error.WriteLine($"[ThingHK] 日志输出到: {logFile.FullName}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[ThingHK] 无法写入日志文件 {logFile.FullName}: {ex.Message}");
}
}
Console.Error.WriteLine($"[ThingHK] serve 模式: port={port} config={configPath ?? "()"} fast={fastMs}ms slow={slowMs}ms");
using var kernel = new KernelHost();
await kernel.StartAsync(basic, fastMs, slowMs);
await kernel.StartAsync(configPath, fastMs, slowMs);
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseUrls($"http://127.0.0.1:{port}/");
@@ -78,15 +98,16 @@ internal static class Program
builder.Logging.AddProvider(new StderrLoggerProvider());
var app = builder.Build();
app.MapThingHKEndpoints(kernel);
// Ctrl+C 优雅关闭
// Ctrl+C 优雅关闭cts 同时用于 /shutdown 接口触发退出)
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
AppDomain.CurrentDomain.ProcessExit += (_, _) => cts.Cancel();
app.MapThingHKEndpoints(kernel, cts);
Console.Error.WriteLine($"[ThingHK] HTTP 服务监听: http://127.0.0.1:{port}/");
Console.Error.WriteLine("[ThingHK] 路由: GET / GET /status GET /snapshot GET /stream POST /config");
Console.Error.WriteLine("[ThingHK] 路由: GET / GET /status GET /snapshot GET /stream POST /config POST /shutdown");
try
{
@@ -103,7 +124,18 @@ internal static class Program
private static async Task<int> ScanAsync(bool json, bool basic)
{
Console.Error.WriteLine($"[ThingHK] scan 模式: json={json} basic={basic}");
using var hw = new HardwareManager(basic);
// scan 命令保留旧 basic 参数:basic=true 用默认配置,basic=false 用全部启用配置
var config = basic ? HardwareConfig.Default : new HardwareConfig
{
Hardware = new Dictionary<string, bool>
{
["cpu"] = true, ["gpu"] = true, ["memory"] = true, ["storage"] = true,
["motherboard"] = true, ["controller"] = true, ["battery"] = true,
["network"] = true, ["psu"] = true,
},
SensorTypes = HardwareConfig.Default.SensorTypes,
};
using var hw = new HardwareManager(config);
// 第二轮 Update:部分传感器首轮才初始化
hw.UpdateAll();
+42 -9
View File
@@ -57,6 +57,7 @@ Rust (Tauri App) <---HTTP / SSE (127.0.0.1:8730)---> ThingHK.exe (独立进程
| `/snapshot` | GET | 一次性拉取最新缓存快照 |
| `/stream` | GET (SSE) | 订阅推送,Kernel 按采样间隔主动 Push `SensorSnapshot` |
| `/config` | POST | 运行时调整采样间隔(`{"FastIntervalMs":500,"SlowIntervalMs":3000}` |
| `/shutdown` | POST | 优雅关闭(提权模式下由 Tauri 调用,普通权限无法 kill 管理员进程) |
### 3.2 数据契约(显式版本化)
@@ -85,14 +86,15 @@ Rust (Tauri App) <---HTTP / SSE (127.0.0.1:8730)---> ThingHK.exe (独立进程
### 3.3 Tauri 命令与事件
**6 个命令**
**7 个命令**
| 命令 | 作用 |
|------|------|
| `monitor_kernel_info` | 查询 Kernel 二进制路径/是否存在/端口 |
| `monitor_status` | 查询运行状态(running/pid/ready/sensorCount/restartCount |
| `monitor_status` | 查询运行状态(running/pid/ready/sensorCount/restartCount/elevated |
| `monitor_start` | 启动 Kernel + 自动订阅 SSE(命令和 setup 共用 `start_with_subscription` |
| `monitor_stop` | 停止 Kernel + 取消 SSE 订阅 + 取消自动重连监听 |
| `monitor_start_elevated` | 以管理员权限启动 Kernel(弹 UAC),停止走 `/shutdown` 接口 |
| `monitor_stop` | 停止 Kernel(提权模式走 `/shutdown`,普通模式走 ProcessManager |
| `monitor_get_status` | 透传 Kernel `/status` |
| `monitor_get_snapshot` | 透传 Kernel `/snapshot` |
@@ -200,15 +202,15 @@ Rust (Tauri App) <---HTTP / SSE (127.0.0.1:8730)---> ThingHK.exe (独立进程
| [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`(统一生命周期) |
| [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 |
### 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 自动启动 |
| [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 |
### 前端
@@ -235,11 +237,42 @@ ThingHK.exe serve --port 8730 --basic --fast-ms 1000 --slow-ms 5000
ThingHK.exe scan --json --basic
```
## 7. 后续可选优化
## 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](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 后取消,确保响应体完整发送。
## 8. 后续可选优化
1. **ProcessManager Mutex 锁阻塞问题**`check_and_cleanup` 中 800ms `sleep`(端口等待)在持有 `processes` Mutex 锁的情况下执行,会阻塞所有进程状态查询。建议将重启逻辑移出锁作用域(已知问题,与 proxy 模块共享)。
2. **提权策略**:通过 Windows 任务计划程序实现免 UAC 管理员启动,解锁 SMBus/EC/部分 GPU 传感器。当前以普通权限运行,覆盖大部分 CPU/GPU/内存/存储温度
2. **免 UAC 提权**:当前每次提权需弹 UAC,可通过 Windows 任务计划程序注册"以最高权限运行"的任务实现免 UAC 启动(需一次性管理员权限注册任务)
3. **多机型回归**AMD Ryzen / NVIDIA GPU / 笔记本场景的实际传感器覆盖度验证(需实际硬件)。
4. **Kernel 版本管理**`prepare_kernel` 只在文件不存在时复制,不会覆盖更新。建议增加版本比对自动更新。
+78
View File
@@ -0,0 +1,78 @@
<#
.SYNOPSIS
ThingHK Native AOT build script.
.DESCRIPTION
Builds ThingHK as Native AOT single-file executable and copies it to src-tauri/binaries/ for Tauri packaging.
Usage: run .\build.ps1 in the ThingHK/ directory.
#>
[CmdletBinding()]
param(
[string]$Configuration = "Release",
[string]$Runtime = "win-x64"
)
$ErrorActionPreference = "Stop"
# Resolve script directory: try multiple methods for compatibility
$scriptDir = $PSScriptRoot
if (-not $scriptDir) {
$cmd = $MyInvocation.MyCommand
if ($cmd -and $cmd.Path) {
$scriptDir = Split-Path -Parent $cmd.Path
}
}
if (-not $scriptDir) {
$scriptDir = (Get-Location).Path
}
$projectPath = Join-Path $scriptDir "ThingHK.csproj"
$publishDir = Join-Path $scriptDir "bin\$Configuration\net8.0-windows\$Runtime\publish"
$destDir = Join-Path $scriptDir "..\src-tauri\binaries"
$destPath = Join-Path $destDir "ThingHK.exe"
Write-Host "=== ThingHK Build Started ===" -ForegroundColor Cyan
Write-Host "Config: $Configuration | Runtime: $Runtime"
Write-Host "Project: $projectPath"
Write-Host "ScriptDir: $scriptDir"
if (-not (Test-Path $projectPath)) {
Write-Error "Project file not found: $projectPath"
exit 1
}
# 1. Clean old obj cache
$objDir = Join-Path $scriptDir "obj"
if (Test-Path $objDir) {
Write-Host "`n[1/4] Cleaning obj cache..." -ForegroundColor Yellow
Remove-Item $objDir -Recurse -Force -ErrorAction SilentlyContinue
}
# 2. dotnet publish (Native AOT)
Write-Host "`n[2/4] Running dotnet publish (Native AOT)..." -ForegroundColor Yellow
& dotnet publish $projectPath -c $Configuration -r $Runtime
if ($LASTEXITCODE -ne 0) {
Write-Error "dotnet publish failed (exit $LASTEXITCODE)"
exit $LASTEXITCODE
}
$publishedExe = Join-Path $publishDir "ThingHK.exe"
if (-not (Test-Path $publishedExe)) {
Write-Error "Build output not found: $publishedExe"
exit 1
}
# 3. Ensure destination directory exists
if (-not (Test-Path $destDir)) {
Write-Host "`n[3/4] Creating destination: $destDir" -ForegroundColor Yellow
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
}
# 4. Copy to src-tauri/binaries/
Write-Host "`n[4/4] Copying to $destPath" -ForegroundColor Yellow
Copy-Item $publishedExe $destPath -Force
$destItem = Get-Item $destPath
Write-Host "`n=== Build Complete ===" -ForegroundColor Green
Write-Host ("Output: {0}" -f $destItem.FullName)
Write-Host ("Size: {0:N2} MB" -f ($destItem.Length / 1MB))
Write-Host ("Time: {0}" -f $destItem.LastWriteTime)