Files
Thing/ThingHK/Program.cs
T
2026-07-24 18:16:34 +08:00

205 lines
8.3 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, null, DefaultFastMs, DefaultSlowMs, null);
}
// serve 子命令
var serveCmd = new Command("serve", "启动 HTTP 服务");
var portOpt = new Option<int>("--port", () => DefaultPort, "监听端口");
var configOpt = new Option<FileInfo?>("--config", "硬件监控配置文件路径(JSON");
var fastOpt = new Option<int>("--fast-ms", () => DefaultFastMs, "快通道采样间隔(毫秒)");
var slowOpt = new Option<int>("--slow-ms", () => DefaultSlowMs, "慢通道采样间隔(毫秒)");
var logFileOpt = new Option<FileInfo?>("--log-file", "将 stderr 日志写入指定文件(提权模式下控制台隐藏时使用)");
serveCmd.AddOption(portOpt);
serveCmd.AddOption(configOpt);
serveCmd.AddOption(fastOpt);
serveCmd.AddOption(slowOpt);
serveCmd.AddOption(logFileOpt);
serveCmd.SetHandler((port, config, fast, slow, logFile) =>
ServeAsync(port, config?.FullName, fast, slow, logFile),
portOpt, configOpt, fastOpt, slowOpt, logFileOpt);
// 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, null, DefaultFastMs, DefaultSlowMs, null));
return await root.InvokeAsync(args);
}
private static async Task<int> ServeAsync(int port, string? configPath, int fastMs, int slowMs, FileInfo? logFile)
{
// 如果指定了 --log-file,将 stderr 重定向到文件(提权模式下控制台隐藏,需要日志排查崩溃)
if (logFile != null)
{
try
{
var logStream = new StreamWriter(logFile.Open(FileMode.Create, FileAccess.Write, FileShare.Read))
{
AutoFlush = true,
};
Console.SetError(logStream);
// 同时重定向 stdout(避免 ASP.NET 的某些输出到 stdout
Console.SetOut(logStream);
Console.Error.WriteLine($"[ThingHK] 日志输出到: {logFile.FullName}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[ThingHK] 无法写入日志文件 {logFile.FullName}: {ex.Message}");
}
}
Console.Error.WriteLine($"[ThingHK] serve 模式: port={port} config={configPath ?? "(默认)"} fast={fastMs}ms slow={slowMs}ms");
using var kernel = new KernelHost();
await kernel.StartAsync(configPath, 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();
// Ctrl+C 优雅关闭(cts 同时用于 /shutdown 接口触发退出)
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
AppDomain.CurrentDomain.ProcessExit += (_, _) => cts.Cancel();
app.MapThingHKEndpoints(kernel, cts);
Console.Error.WriteLine($"[ThingHK] HTTP 服务监听: http://127.0.0.1:{port}/");
Console.Error.WriteLine("[ThingHK] 路由: GET / GET /status GET /snapshot GET /stream POST /config POST /shutdown");
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}");
// scan 命令保留旧 basic 参数:basic=true 用默认配置,basic=false 用全部启用配置
var config = basic ? HardwareConfig.Default : new HardwareConfig
{
Hardware = new Dictionary<string, bool>
{
["cpu"] = true, ["gpu"] = true, ["memory"] = true, ["storage"] = true,
["motherboard"] = true, ["controller"] = true, ["battery"] = true,
["network"] = true, ["psu"] = true,
},
SensorTypes = HardwareConfig.Default.SensorTypes,
};
using var hw = new HardwareManager(config);
// 第二轮 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() { }
}
}