Files
Thing/ThingHK/Program.cs
T

173 lines
6.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.CommandLine;
using System.Diagnostics;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace ThingHK;
/// <summary>
/// ThingHK 入口。
///
/// 子命令:
/// serve (默认) 启动 HTTP 服务,暴露 /status /snapshot /stream /config
/// scan 阶段一的覆盖矩阵测试(保留以便复测)
///
/// 默认(无参数)等价于 serve --port 8730 --basic。
/// </summary>
internal static class Program
{
private const int DefaultPort = 8730;
private const int DefaultFastMs = 1000;
private const int DefaultSlowMs = 5000;
public static async Task<int> 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<int>("--port", () => DefaultPort, "监听端口");
var basicOpt = new Option<bool>("--basic", "仅启用核心四类硬件,降低冷启动耗时");
var fullOpt = new Option<bool>("--full", "启用全部硬件(含主板/控制器/网络等)");
var fastOpt = new Option<int>("--fast-ms", () => DefaultFastMs, "快通道采样间隔(毫秒)");
var slowOpt = new Option<int>("--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<bool>("--json", "JSON 输出");
var scanBasicOpt = new Option<bool>("--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<int> 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;
}
/// <summary>
/// 阶段一覆盖矩阵测试入口(保留)。
/// </summary>
private static async Task<int> 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} 个传感器有值 ====");
}
}
/// <summary>
/// 把 ASP.NET Core 日志重定向到 stderr,避免污染 stdoutstdout 留给 /snapshot 等 JSON 响应)。
/// </summary>
internal sealed class StderrLoggerProvider : ILoggerProvider
{
public ILogger CreateLogger(string categoryName) => new StderrLogger();
public void Dispose() { }
private sealed class StderrLogger : ILogger
{
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Warning;
public void Log<TState>(LogLevel level, EventId id, TState state, Exception? ex, Func<TState, Exception?, string> 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() { }
}
}