Files
Ayay/SHH.CameraService/Bootstrapper.cs
2026-01-16 07:23:56 +08:00

273 lines
9.0 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,video,调试PC;",
"--uris", "localhost,9002,video,调试PC-2;",
// 指令通道
"--uris", "localhost,9001,command,调试PC;",
// 日志中心配置 (格式: IP,Port,Desc)
"--sequris", "58.216.225.5,20026,日志处置中心;",
"--seqkey", "Shine101173874928;",
// 端口策略
"--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,
// 动态设置日志级别
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)
{
if (!config.CommandEndpoints.Any()) return;
var gRpcLog = Log.ForContext("SourceContext", LogModules.gRpc);
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);
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
}