增加日志组件

This commit is contained in:
2026-01-15 18:56:39 +08:00
parent 801ffb25fd
commit 2754cdff15
13 changed files with 1153 additions and 142 deletions

View File

@@ -0,0 +1,269 @@
using System.Net.Sockets;
using System.Net;
using Ayay.SerilogLogs;
using Grpc.Net.Client;
using Serilog;
using SHH.CameraSdk;
using SHH.Contracts.Grpc;
namespace SHH.CameraService;
public static class Bootstrapper
{
#region ParseConfigAndInitLogger
/// <summary>
/// 解析参数并初始化 Serilog
/// </summary>
public static (ServiceConfig Config, LogOptions opts, bool IsDebug) ParseConfigAndInitLogger(string[] args)
{
bool isDebugArgs = args.Length == 0;
// =================================================================================
// 1. 模拟调试参数
// 原理:直接构造 string[],完全模拟 OS 传递给 Main 的参数结构,避免 Split 带来的空格解析风险
// =================================================================================
if (isDebugArgs)
{
args = new[]
{
"--appid", "CameraApp_01",
// 视频流地址 (格式: IP,Port,Type,Desc)
"--uris", "localhost,9001,video,调试PC;",
"--uris", "localhost,9002,video,调试PC-2;",
// 指令通道
"--uris", "localhost,9001,command,调试PC;",
// 日志中心配置 (格式: IP,Port,Desc)
"--sequris", "172.16.41.241,20026,日志处置中心;",
"--seqkey", "Shine899195994250;",
// 端口策略
"--mode", "1",
"--ports", "5000,100"
};
}
var config = ServiceConfig.BuildFromArgs(args);
var ops = new LogOptions
{
AppId = config.AppId,
LogRootPath = $@"D:\Logs\{config.AppId}",
// ★这里改为从 config 读取,如果没配则留空或给个默认值
SeqServerUrl = config.SeqServerUrl,
SeqApiKey = config.SeqApiKey,
MaxRetentionDays = 10,
FileSizeLimitBytes = 1024L * 1024 * 1024,
// 动态设置日志级别
ModuleLevels = new Dictionary<string, Serilog.Events.LogEventLevel>
{
// 确保 Core 模块在调试时能输出 Debug在生产时输出 Info
{ LogModules.Core, isDebugArgs ? Serilog.Events.LogEventLevel.Debug : Serilog.Events.LogEventLevel.Information },
{ LogModules.Network, Serilog.Events.LogEventLevel.Warning }
}
};
LogBootstrapper.Init(ops);
return (config, ops, isDebugArgs);
}
#endregion
#region Shutdown
/// <summary>
/// 统一的资源释放与关闭方法
/// </summary>
/// <summary>
/// 统一的资源释放与关闭方法
/// </summary>
/// <param name="reason">退出原因</param>
/// <param name="exitCode">退出码 (0=正常, 非0=异常)</param>
public static void Shutdown(string reason, int exitCode = 0)
{
// 创建一个临时的上下文 Logger防止全局 Logger 已被部分释放
var sysLog = Log.ForContext("SourceContext", LogModules.Core);
try
{
// =========================================================
// 1. 写日志
// =========================================================
if (exitCode != 0)
{
sysLog.Fatal($"💀 [程序终止] {reason} (Code: {exitCode})");
}
else
{
sysLog.Information($"👋 [程序退出] {reason}");
}
// =========================================================
// 2. 核心硬件资源释放 (关键!)
// =========================================================
// 防止 SDK 句柄残留导致下次启动无法连接相机
try
{
sysLog.Information("正在清理 Hikvision SDK 资源...");
// 如果你的项目中引用了 SDK请务必解开这行注释
HikNativeMethods.NET_DVR_Cleanup();
}
catch (Exception ex)
{
sysLog.Error(ex, "⚠️ SDK 资源清理失败");
}
// =========================================================
// 3. 日志强制落盘
// =========================================================
// Environment.Exit 是暴力退出finally 块不会执行,
// 必须手动 Flush 确保最后一条日志写入磁盘。
Log.CloseAndFlush();
}
catch
{
// 忽略所有清理过程中的错误,确保一定要执行到 Environment.Exit
}
// =========================================================
// 4. 开发环境交互 (生产环境自动跳过)
// =========================================================
// 只有在调试器挂载时才暂停,防止 Docker/Service 环境卡死
if (System.Diagnostics.Debugger.IsAttached)
{
Console.ForegroundColor = exitCode == 0 ? ConsoleColor.Green : ConsoleColor.Red;
Console.WriteLine($"\n[Debug模式] 按下任意键退出... (ExitCode: {exitCode})");
Console.ResetColor();
Console.ReadKey();
}
// =========================================================
// 5. 暴力且彻底地结束进程
// =========================================================
Environment.Exit(exitCode);
}
#endregion
#region ScanForAvailablePort
/// <summary>
/// 扫描可用端口
/// </summary>
public static int ScanForAvailablePort(ServiceConfig config, ILogger logger)
{
logger.Information($"🔍 开始端口检测: 起始={config.BasePort}, 范围={config.MaxPortRange}");
for (int i = 0; i <= config.MaxPortRange; i++)
{
int currentPort = config.BasePort + i;
if (CheckPortAvailable(currentPort))
{
if (currentPort != config.BasePort)
{
logger.Warning($"⚙️ 端口自动漂移: {config.BasePort} -> {currentPort}");
}
else
{
logger.Information($"✅ 端口检测通过: {currentPort}");
}
return currentPort;
}
logger.Debug($"⚠️ 端口 {currentPort} 被占用,尝试下一个...");
}
return -1;
}
#endregion
#region CheckPortAvailable
/// <summary>
/// 检查端口是否可用
/// </summary>
/// <param name="port"></param>
/// <returns></returns>
private static bool CheckPortAvailable(int port)
{
try
{
using var listener = new TcpListener(IPAddress.Any, port);
listener.Start();
listener.Stop();
return true;
}
catch
{
return false;
}
}
#endregion
#region WarmUpHardware
/// <summary>
/// 硬件预热
/// </summary>
public static void WarmUpHardware(ILogger logger)
{
logger.Information("Hik Sdk 开始预热.");
try
{
HikNativeMethods.NET_DVR_Init();
HikSdkManager.ForceWarmUp();
logger.Information("💡Hik Sdk 预热成功.");
}
catch (Exception ex)
{
logger.Error(ex, "⚠️ Hik Sdk 预热失败.");
}
}
#endregion
#region RegisterToGatewayAsync
/// <summary>
/// 向网关注册实例
/// </summary>
public static async Task RegisterToGatewayAsync(ServiceConfig config, ILogger logger)
{
if (!config.CommandEndpoints.Any()) return;
try
{
// 将 tcp:// 转换为 http:// 以适配 gRPC
string targetUrl = config.CommandEndpoints.First().Uri.Replace("tcp://", "http://");
using var channel = GrpcChannel.ForAddress(targetUrl);
var client = new GatewayProvider.GatewayProviderClient(channel);
logger.Information($"[Grpc] 正在执行预注册: {targetUrl}");
var resp = await client.RegisterInstanceAsync(new RegisterRequest
{
InstanceId = config.AppId,
Version = "2.0.0-grpc",
ServerIp = "127.0.0.1",
WebapiPort = config.BasePort, // 使用扫描后的新端口
StartTimeTicks = DateTime.Now.Ticks,
ProcessId = Environment.ProcessId,
Description = ""
});
logger.Information($"💡[Grpc] 预注册成功: {resp.Message}");
}
catch (Exception ex)
{
logger.Error($"⚠️ [Grpc] 预注册尝试失败: {ex.Message}");
}
}
#endregion
}