210 lines
8.7 KiB
C#
210 lines
8.7 KiB
C#
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");
|
||
|
||
// 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;
|
||
}
|
||
|
||
/// <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,避免污染 stdout(stdout 留给 /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() { }
|
||
}
|
||
}
|