独立监控核心和模块
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ThingHK;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 服务层:minimal API 风格扩展方法。
|
||||
/// 路由设计(见 ThingHK_GUIDE.md 第 2.3 节):
|
||||
/// GET /status → 冷启动就绪探测,前端轮询判断是否可订阅
|
||||
/// GET /snapshot → 一次性拉取最新缓存快照
|
||||
/// GET /stream → SSE 订阅,Kernel 主动推送 SensorSnapshot
|
||||
/// POST /config → 运行时调整采样间隔
|
||||
/// GET / → 健康检查(200 OK)
|
||||
/// </summary>
|
||||
internal static class HttpEndpoints
|
||||
{
|
||||
public static WebApplication MapThingHKEndpoints(this WebApplication app, KernelHost kernel)
|
||||
{
|
||||
// 全局异常捕获中间件:把未处理异常写到 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<string>(),
|
||||
};
|
||||
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>,单纯返回 IResult 会被框架当成 Task<IResult> 的未等待任务,导致响应体为空
|
||||
app.MapPost("/config", async Task<IResult> (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);
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写一条 SSE 事件:event: snapshot\ndata: {json}\n\n
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kernel 宿主:持有 HardwareManager + SamplingScheduler,管理整体生命周期。
|
||||
/// Program.cs 构造此对象并注入到 DI 容器供 endpoint 使用。
|
||||
/// </summary>
|
||||
internal sealed class KernelHost : IDisposable
|
||||
{
|
||||
public HardwareManager? Hardware { get; private set; }
|
||||
public SamplingScheduler Scheduler { get; private set; } = null!;
|
||||
public Stopwatch Uptime { get; } = Stopwatch.StartNew();
|
||||
|
||||
public async Task StartAsync(bool basic, int fastMs, int slowMs)
|
||||
{
|
||||
// 1. 初始化硬件层(含首轮 Update,约 5 秒)
|
||||
Hardware = new HardwareManager(basic);
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user