Files
Ayay/SHH.CameraService/Bootstrapper.cs

314 lines
11 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 Ayay.SerilogLogs;
using Grpc.Net.Client;
using Serilog;
using SHH.CameraSdk;
using SHH.Contracts.Grpc;
using System.Net;
using System.Net.Sockets;
using System.Text.RegularExpressions;
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,调试PC;",
//"--uris", "localhost,9002,调试PC;",
// 日志中心配置 (格式: IP,Port,Desc)
"--sequris", "58.216.225.5,20026,日志处置中心;",
"--seqkey", "Shine978697953780;",
//"--seqkey", "Shine899195994250;",
// 端口策略
"--mode", "1",
"--ports", "5000,100"
};
}
var config = ServiceConfig.BuildFromArgs(args);
string pcCode = config.SeqApiKey.Replace("Shine", "");
var ops = new LogOptions
{
AppId = config.AppId,
LogRootPath = $@"D:\Logs\{config.AppId}",
// ★这里改为从 config 读取,如果没配则留空或给个默认值
SeqServerUrl = config.SeqServerUrl,
SeqApiKey = config.SeqApiKey,
PcCode = Regex.Replace(pcCode, ".{3}", "$0-").TrimEnd('-'),
MaxRetentionDays = 10,
FileSizeLimitBytes = 1024L * 1024 * 1024,
RollOnFileSizeLimit = true,
};
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($"[Core] 💀 [程序终止] {reason} (Code: {exitCode})");
}
else
{
sysLog.Information($"[Core] 👋 [程序退出] {reason}");
}
// =========================================================
// 2. 核心硬件资源释放 (关键!)
// =========================================================
// 防止 SDK 句柄残留导致下次启动无法连接相机
try
{
sysLog.Information("[Core] 正在清理 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($"[Core] 🔍 开始端口检测: 起始={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($"[Core] ⚙️ 端口自动漂移: {config.BasePort} -> {currentPort}");
}
else
{
logger.Information($"[Core] ✅ 端口检测通过: {currentPort}");
}
return currentPort;
}
logger.Debug($"[Core] ⚠️ 端口 {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("[Core] Hik、Dahua Sdk 开始预热.");
try
{
HikNativeMethods.NET_DVR_Init();
HikSdkManager.ForceWarmUp();
logger.Information("[Core] 💡Hik Sdk 预热成功.");
}
catch (Exception ex)
{
logger.Error(ex, "[Core] ⚠️ Hik Sdk 预热失败.");
}
try
{
DahuaSdkManager.ForceWarmUp();
logger.Information("[Core] Dahua Sdk 预热成功.");
}
catch (Exception ex)
{
logger.Error(ex, "[Core] ⚠️ Dahua Sdk 预热失败.");
}
}
#endregion
#region RegisterToGatewayAsync
/// <summary>
/// 向网关注册实例
/// </summary>
public static async Task RegisterToGatewayAsync(ServiceConfig config)
{
if (!config.CommandEndpoints.Any()) return;
var gRpcLog = Log.ForContext("SourceContext", LogModules.gRpc);
// // Optimized: 并发任务集合,实现多目标同时注册
var registrationTasks = config.CommandEndpoints.Select(async endpoint =>
{
string targetUrl = endpoint.Uri.Replace("tcp://", "http://");
// // Modified: 将 try-catch 移入内部,确保单个端点失败不影响其他端点
try
{
using var channel = GrpcChannel.ForAddress(targetUrl);
var client = new GatewayProvider.GatewayProviderClient(channel);
gRpcLog.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 = endpoint.Description // 携带备注信息
});
gRpcLog.Information($"[gRpc] 💡预注册成功: {targetUrl} -> {resp.Message}");
}
catch (Exception ex)
{
// // Optimized: 记录具体哪个端点失败,但不阻断流程
gRpcLog.Error($"[gRpc] ⚠️ 预注册尝试失败 ({targetUrl}): {ex.Message}");
}
});
// 等待所有注册任务完成
await Task.WhenAll(registrationTasks);
//try
//{
// var cfgEndpoints = config.CommandEndpoints;
// for(var i=0; i<cfgEndpoints.Count; i++)
// {
// // 将 tcp:// 转换为 http:// 以适配 gRpc
// string targetUrl = cfgEndpoints[i].Uri.Replace("tcp://", "http://");
// using var channel = GrpcChannel.ForAddress(targetUrl);
// var client = new GatewayProvider.GatewayProviderClient(channel);
// gRpcLog.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 = ""
// });
// gRpcLog.Information($"[gRpc] 💡预注册成功: {resp.Message}");
// }
//}
//catch (Exception ex)
//{
// gRpcLog.Error($"[gRpc] ⚠️ 预注册尝试失败: {ex.Message}");
//}
}
#endregion
}