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 /// /// 解析参数并初始化 Serilog /// 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 { // 确保 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 /// /// 统一的资源释放与关闭方法 /// /// /// 统一的资源释放与关闭方法 /// /// 退出原因 /// 退出码 (0=正常, 非0=异常) 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 /// /// 扫描可用端口 /// 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 /// /// 检查端口是否可用 /// /// /// 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 /// /// 硬件预热 /// 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 /// /// 向网关注册实例 /// 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 }