using System.CommandLine;
using System.Diagnostics;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace ThingHK;
///
/// ThingHK 入口。
///
/// 子命令:
/// serve (默认) 启动 HTTP 服务,暴露 /status /snapshot /stream /config
/// scan 阶段一的覆盖矩阵测试(保留以便复测)
///
/// 默认(无参数)等价于 serve --port 8730 --basic。
///
internal static class Program
{
private const int DefaultPort = 8730;
private const int DefaultFastMs = 1000;
private const int DefaultSlowMs = 5000;
public static async Task Main(string[] args)
{
// 无参数 → 默认 serve(最常见用法)
if (args.Length == 0)
{
return await ServeAsync(DefaultPort, basic: true, DefaultFastMs, DefaultSlowMs);
}
// serve 子命令
var serveCmd = new Command("serve", "启动 HTTP 服务");
var portOpt = new Option("--port", () => DefaultPort, "监听端口");
var basicOpt = new Option("--basic", "仅启用核心四类硬件,降低冷启动耗时");
var fullOpt = new Option("--full", "启用全部硬件(含主板/控制器/网络等)");
var fastOpt = new Option("--fast-ms", () => DefaultFastMs, "快通道采样间隔(毫秒)");
var slowOpt = new Option("--slow-ms", () => DefaultSlowMs, "慢通道采样间隔(毫秒)");
serveCmd.AddOption(portOpt);
serveCmd.AddOption(basicOpt);
serveCmd.AddOption(fullOpt);
serveCmd.AddOption(fastOpt);
serveCmd.AddOption(slowOpt);
serveCmd.SetHandler((port, basic, full, fast, slow) =>
ServeAsync(port, basic || !full, fast, slow),
portOpt, basicOpt, fullOpt, fastOpt, slowOpt);
// scan 子命令(阶段一覆盖矩阵测试,保留)
var scanCmd = new Command("scan", "扫描一次并输出覆盖矩阵(阶段一验证用)");
var scanJsonOpt = new Option("--json", "JSON 输出");
var scanBasicOpt = new Option("--basic", "仅核心四类硬件");
scanCmd.AddOption(scanJsonOpt);
scanCmd.AddOption(scanBasicOpt);
scanCmd.SetHandler((json, basic) => ScanAsync(json, basic), scanJsonOpt, scanBasicOpt);
var root = new RootCommand("ThingHardwareKernel - Thing 硬件监控内核");
root.Add(serveCmd);
root.Add(scanCmd);
// 兼容阶段一的旧用法:ThingHK.exe --json --basic
root.SetHandler(() => ServeAsync(DefaultPort, basic: true, DefaultFastMs, DefaultSlowMs));
return await root.InvokeAsync(args);
}
private static async Task ServeAsync(int port, bool basic, int fastMs, int slowMs)
{
Console.Error.WriteLine($"[ThingHK] serve 模式: port={port} basic={basic} fast={fastMs}ms slow={slowMs}ms");
using var kernel = new KernelHost();
await kernel.StartAsync(basic, fastMs, slowMs);
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseUrls($"http://127.0.0.1:{port}/");
// 抑制 Kestrel 默认日志噪音
builder.Logging.ClearProviders();
builder.Logging.AddProvider(new StderrLoggerProvider());
var app = builder.Build();
app.MapThingHKEndpoints(kernel);
// Ctrl+C 优雅关闭
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
AppDomain.CurrentDomain.ProcessExit += (_, _) => cts.Cancel();
Console.Error.WriteLine($"[ThingHK] HTTP 服务监听: http://127.0.0.1:{port}/");
Console.Error.WriteLine("[ThingHK] 路由: GET / GET /status GET /snapshot GET /stream POST /config");
try
{
await app.RunAsync(cts.Token);
}
catch (TaskCanceledException) { /* 正常关闭 */ }
Console.Error.WriteLine("[ThingHK] 已退出");
return 0;
}
///
/// 阶段一覆盖矩阵测试入口(保留)。
///
private static async Task ScanAsync(bool json, bool basic)
{
Console.Error.WriteLine($"[ThingHK] scan 模式: json={json} basic={basic}");
using var hw = new HardwareManager(basic);
// 第二轮 Update:部分传感器首轮才初始化
hw.UpdateAll();
var snap = hw.BuildSnapshot();
if (json)
{
// 阶段一 Report 字段已合并进 SensorSnapshot,复用 ColdStartMs
Console.WriteLine(JsonSerializer.Serialize(snap, ThingHKJsonContext.Default.SensorSnapshot));
}
else
{
PrintText(snap, hw.ColdStartMs, hw.IsAdmin);
}
await Task.CompletedTask;
return snap.Groups.Count == 0 ? 3 : 0;
}
private static void PrintText(SensorSnapshot r, double coldStartMs, bool isAdmin)
{
Console.WriteLine("==== ThingHK 覆盖矩阵 ====");
Console.WriteLine($"管理员: {isAdmin} | 冷启动: {coldStartMs:F1} ms | .NET: {Environment.Version}");
Console.WriteLine($"硬件分组数: {r.Groups.Count}");
int total = 0, readable = 0;
foreach (var g in r.Groups)
{
Console.WriteLine();
Console.WriteLine($"[{g.Name}] ({g.Sensors.Count} 个传感器)");
foreach (var s in g.Sensors)
{
total++;
string val = s.Value.HasValue ? $"{s.Value.Value:F2}{(string.IsNullOrEmpty(s.Unit) ? "" : " " + s.Unit)}" : "N/A";
if (s.Value.HasValue) readable++;
Console.WriteLine($" - {s.Name,-32} {s.Type,-14} {val}");
}
}
Console.WriteLine();
Console.WriteLine($"==== 汇总: {readable}/{total} 个传感器有值 ====");
}
}
///
/// 把 ASP.NET Core 日志重定向到 stderr,避免污染 stdout(stdout 留给 /snapshot 等 JSON 响应)。
///
internal sealed class StderrLoggerProvider : ILoggerProvider
{
public ILogger CreateLogger(string categoryName) => new StderrLogger();
public void Dispose() { }
private sealed class StderrLogger : ILogger
{
public IDisposable BeginScope(TState state) where TState : notnull => NullScope.Instance;
public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Warning;
public void Log(LogLevel level, EventId id, TState state, Exception? ex, Func formatter)
{
if (!IsEnabled(level)) return;
Console.Error.WriteLine($"[kestrel] {formatter(state, ex)}");
}
}
private sealed class NullScope : IDisposable
{
public static NullScope Instance { get; } = new();
public void Dispose() { }
}
}