using System.Diagnostics; using System.Text.Json; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace ThingHK; /// /// HTTP 服务层:minimal API 风格扩展方法。 /// 路由设计(见 ThingHK_GUIDE.md 第 2.3 节): /// GET /status → 冷启动就绪探测,前端轮询判断是否可订阅 /// GET /snapshot → 一次性拉取最新缓存快照 /// GET /stream → SSE 讑阅,Kernel 主动推送 SensorSnapshot /// POST /config → 运行时调整采样间隔 /// POST /shutdown → 优雅关闭(提权模式下由 Tauri 调用,普通权限无法 kill 管理员进程) /// GET / → 健康检查(200 OK) /// internal static class HttpEndpoints { public static WebApplication MapThingHKEndpoints(this WebApplication app, KernelHost kernel, CancellationTokenSource shutdownCts) { // 全局异常捕获中间件:把未处理异常写到 stderr,便于 AOT 下排错 app.Use(async (ctx, next) => { try { await next(); } catch (Exception ex) { Console.Error.WriteLine($"[HTTP] {ctx.Request.Method} {ctx.Request.Path} 异常: {ex}"); ctx.Response.StatusCode = 500; await ctx.Response.WriteAsync($"{{\"error\":\"internal\",\"message\":\"{ex.Message.Replace("\"", "\\\"")}\"}}"); } }); // 根路由:进程存活检查(curl 友好) // AOT 下必须用 Results.Json + TypeInfo,Results.Ok(object) 会走默认 options 抛 JsonTypeInfo 异常 app.MapGet("/", () => Results.Json(new HealthResponse(), ThingHKJsonContext.Default.HealthResponse)); // 冷启动就绪探测 app.MapGet("/status", () => { var hw = kernel.Hardware; var snap = kernel.Scheduler.Cache.GetLatest(); var status = new KernelStatus { Ready = hw?.Ready ?? false, IsAdmin = hw?.IsAdmin ?? false, UptimeMs = kernel.Uptime.Elapsed.TotalMilliseconds, GroupCount = snap?.Groups.Count ?? 0, SensorCount = kernel.Scheduler.Cache.SensorCount, Providers = snap?.Groups.Select(g => g.Id).ToList() ?? new List(), }; return Results.Json(status, ThingHKJsonContext.Default.KernelStatus); }); // 一次性快照 app.MapGet("/snapshot", (HttpContext ctx) => { var snap = kernel.Scheduler.Cache.GetLatest(); if (snap == null) { ctx.Response.StatusCode = 404; return Results.Json(new ErrorResponse("not_ready", "Kernel 尚未完成首轮扫描"), ThingHKJsonContext.Default.ErrorResponse); } return Results.Json(snap, ThingHKJsonContext.Default.SensorSnapshot); }); // SSE 推送流 app.MapGet("/stream", async (HttpContext ctx, CancellationToken ct) => { // 强制 text/event-stream,禁用响应缓冲(SSE 必须 flush) ctx.Response.ContentType = "text/event-stream"; ctx.Response.Headers.CacheControl = "no-cache"; ctx.Response.Headers.Connection = "keep-alive"; ctx.Response.Headers["X-Accel-Buffering"] = "no"; // 先推一次当前缓存快照,避免客户端等待一个 tick 才有数据 var initial = kernel.Scheduler.Cache.GetLatest(); if (initial != null) { await WriteSseEventAsync(ctx, initial, ct).ConfigureAwait(false); } // 订阅广播 await foreach (var snap in kernel.Scheduler.SubscribeAsync(ct).ConfigureAwait(false)) { await WriteSseEventAsync(ctx, snap, ct).ConfigureAwait(false); } }); // 运行时配置调整 // 注意:async lambda 必须返回 Task,单纯返回 IResult 会被框架当成 Task 的未等待任务,导致响应体为空 app.MapPost("/config", async Task (HttpContext ctx) => { // 先读 body 为字符串,再反序列化。 // 在 AOT 下比 DeserializeAsync 对 nullable 属性更稳定。 using var reader = new StreamReader(ctx.Request.Body); var body = await reader.ReadToEndAsync(ctx.RequestAborted); ConfigRequest? req; try { req = string.IsNullOrWhiteSpace(body) ? new ConfigRequest() : JsonSerializer.Deserialize(body, ThingHKJsonContext.Default.ConfigRequest); } 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); } kernel.Scheduler.UpdateIntervals(req.FastIntervalMs, req.SlowIntervalMs); return Results.Json(new ConfigResponse { Success = true, FastIntervalMs = kernel.Scheduler.FastIntervalMs, SlowIntervalMs = kernel.Scheduler.SlowIntervalMs, StreamIntervalMs = kernel.Scheduler.FastIntervalMs, }, 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 (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(oldConfig.Hardware), SensorTypes = new Dictionary(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; } /// /// 写一条 SSE 事件:event: snapshot\ndata: {json}\n\n /// private static async Task WriteSseEventAsync(HttpContext ctx, SensorSnapshot snap, CancellationToken ct) { await ctx.Response.WriteAsync("event: snapshot\n", ct).ConfigureAwait(false); await ctx.Response.WriteAsync("data: ", ct).ConfigureAwait(false); // 直接用流式序列化,避免大字符串分配 await JsonSerializer.SerializeAsync( ctx.Response.Body, snap, ThingHKJsonContext.Default.SensorSnapshot, ct).ConfigureAwait(false); await ctx.Response.WriteAsync("\n\n", ct).ConfigureAwait(false); await ctx.Response.Body.FlushAsync(ct).ConfigureAwait(false); } /// 可用硬件分组清单(固定列表,供前端 Dialog 渲染) private static List 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(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; } /// 可用传感器类型清单(固定列表,供前端 Dialog 渲染) private static List 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(items.Length); foreach (var (key, name) in items) { list.Add(new SensorTypeInfo { Key = key, Name = name, Enabled = cfg.IsSensorTypeEnabled(key), }); } return list; } } /// /// Kernel 宿主:持有 HardwareManager + SamplingScheduler,管理整体生命周期。 /// Program.cs 构造此对象并注入到 DI 容器供 endpoint 使用。 /// internal sealed class KernelHost : IDisposable { public HardwareManager? Hardware { get; private set; } public SamplingScheduler Scheduler { get; private set; } = null!; public Stopwatch Uptime { get; } = Stopwatch.StartNew(); /// 配置文件路径(供 /config/hardware POST 保存) public string? ConfigPath { get; private set; } 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(config); Console.Error.WriteLine($"[ThingHK] 硬件就绪: {Hardware.AllHardware.Count} 设备, 冷启动 {Hardware.ColdStartMs:F0}ms, admin={Hardware.IsAdmin}"); // 2. 启动采样调度器(会立即推送首个快照到广播 channel) Scheduler = new SamplingScheduler(Hardware, fastMs, slowMs); await Scheduler.StartAsync(); Console.Error.WriteLine($"[ThingHK] 调度器已启动: fast={Scheduler.FastIntervalMs}ms slow={Scheduler.SlowIntervalMs}ms"); } public void Dispose() { Console.Error.WriteLine("[ThingHK] Kernel 正在关闭..."); Scheduler?.Dispose(); Hardware?.Dispose(); } }