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,
PawnIoInstalled = PawnIoSupport.IsServiceInstalled(),
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();
}
}