监控模块 优化
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 为 HardwareConfigKey(cpu/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>配置 key(cpu/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>配置 key(temperature/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
@@ -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 行为:主板的传感器实际通过 SubHardware(SuperIO/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
@@ -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", "GPU(Intel/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
@@ -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();
|
||||
|
||||
@@ -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 提权模式限制
|
||||
|
||||
| 能力 | 普通模式 | 提权模式 |
|
||||
|------|----------|----------|
|
||||
| 进程管理 | ProcessManager(kill/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` 只在文件不存在时复制,不会覆盖更新。建议增加版本比对自动更新。
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user