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, null, DefaultFastMs, DefaultSlowMs, null); } // serve 子命令 var serveCmd = new Command("serve", "启动 HTTP 服务"); var portOpt = new Option("--port", () => DefaultPort, "监听端口"); var configOpt = new Option("--config", "硬件监控配置文件路径(JSON)"); var fastOpt = new Option("--fast-ms", () => DefaultFastMs, "快通道采样间隔(毫秒)"); var slowOpt = new Option("--slow-ms", () => DefaultSlowMs, "慢通道采样间隔(毫秒)"); var logFileOpt = new Option("--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("--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, null, DefaultFastMs, DefaultSlowMs, null)); return await root.InvokeAsync(args); } private static async Task 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"); // PawnIO:ring0 传感器读取的首选驱动(未安装且已提权时静默安装, // 避开 WinRing0 被系统阻止列表/杀软拦截导致的温度/频率缺失) string pawnIoResult = PawnIoSupport.EnsureInstalled(); Console.Error.WriteLine($"[ThingHK] PawnIO: {pawnIoResult}"); 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; } /// /// 阶段一覆盖矩阵测试入口(保留)。 /// private static async Task 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 { ["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} 个传感器有值 ===="); } } /// /// 把 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() { } } }