监控模块 优化
This commit is contained in:
+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)
|
||||
|
||||
Reference in New Issue
Block a user