2026-01-16 07:23:56 +08:00
|
|
|
|
using Ayay.SerilogLogs;
|
2026-01-15 18:56:39 +08:00
|
|
|
|
using Grpc.Net.Client;
|
|
|
|
|
|
using Serilog;
|
|
|
|
|
|
using SHH.CameraSdk;
|
|
|
|
|
|
using SHH.Contracts.Grpc;
|
2026-01-16 07:23:56 +08:00
|
|
|
|
using System.Net;
|
|
|
|
|
|
using System.Net.Sockets;
|
|
|
|
|
|
using System.Text.RegularExpressions;
|
2026-01-15 18:56:39 +08:00
|
|
|
|
|
|
|
|
|
|
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)
|
2026-01-21 19:03:59 +08:00
|
|
|
|
"--uris", "localhost,9001,调试PC;",
|
|
|
|
|
|
"--uris", "localhost,9002,调试PC;",
|
|
|
|
|
|
|
2026-01-15 18:56:39 +08:00
|
|
|
|
// 日志中心配置 (格式: IP,Port,Desc)
|
2026-01-17 19:17:49 +08:00
|
|
|
|
"--sequris", "58.216.225.5,20026,日志处置中心;",
|
2026-01-21 19:03:59 +08:00
|
|
|
|
"--seqkey", "Shine978697953780;",
|
|
|
|
|
|
//"--seqkey", "Shine899195994250;",
|
|
|
|
|
|
|
2026-01-15 18:56:39 +08:00
|
|
|
|
// 端口策略
|
|
|
|
|
|
"--mode", "1",
|
|
|
|
|
|
"--ports", "5000,100"
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var config = ServiceConfig.BuildFromArgs(args);
|
|
|
|
|
|
|
2026-01-16 07:23:56 +08:00
|
|
|
|
string pcCode = config.SeqApiKey.Replace("Shine", "");
|
2026-01-15 18:56:39 +08:00
|
|
|
|
var ops = new LogOptions
|
|
|
|
|
|
{
|
|
|
|
|
|
AppId = config.AppId,
|
|
|
|
|
|
LogRootPath = $@"D:\Logs\{config.AppId}",
|
|
|
|
|
|
|
|
|
|
|
|
// ★这里改为从 config 读取,如果没配则留空或给个默认值
|
|
|
|
|
|
SeqServerUrl = config.SeqServerUrl,
|
|
|
|
|
|
SeqApiKey = config.SeqApiKey,
|
2026-01-16 07:23:56 +08:00
|
|
|
|
PcCode = Regex.Replace(pcCode, ".{3}", "$0-").TrimEnd('-'),
|
2026-01-15 18:56:39 +08:00
|
|
|
|
|
|
|
|
|
|
MaxRetentionDays = 10,
|
|
|
|
|
|
FileSizeLimitBytes = 1024L * 1024 * 1024,
|
2026-01-19 07:39:59 +08:00
|
|
|
|
RollOnFileSizeLimit = true,
|
2026-01-15 18:56:39 +08:00
|
|
|
|
};
|
|
|
|
|
|
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)
|
|
|
|
|
|
{
|
2026-01-16 14:30:42 +08:00
|
|
|
|
sysLog.Fatal($"[Core] 💀 [程序终止] {reason} (Code: {exitCode})");
|
2026-01-15 18:56:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-01-16 14:30:42 +08:00
|
|
|
|
sysLog.Information($"[Core] 👋 [程序退出] {reason}");
|
2026-01-15 18:56:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// 2. 核心硬件资源释放 (关键!)
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// 防止 SDK 句柄残留导致下次启动无法连接相机
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-01-16 14:30:42 +08:00
|
|
|
|
sysLog.Information("[Core] 正在清理 Hikvision SDK 资源...");
|
2026-01-15 18:56:39 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果你的项目中引用了 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)
|
|
|
|
|
|
{
|
2026-01-16 14:30:42 +08:00
|
|
|
|
logger.Information($"[Core] 🔍 开始端口检测: 起始={config.BasePort}, 范围={config.MaxPortRange}");
|
2026-01-15 18:56:39 +08:00
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i <= config.MaxPortRange; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int currentPort = config.BasePort + i;
|
|
|
|
|
|
if (CheckPortAvailable(currentPort))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (currentPort != config.BasePort)
|
|
|
|
|
|
{
|
2026-01-16 14:30:42 +08:00
|
|
|
|
logger.Warning($"[Core] ⚙️ 端口自动漂移: {config.BasePort} -> {currentPort}");
|
2026-01-15 18:56:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-01-16 14:30:42 +08:00
|
|
|
|
logger.Information($"[Core] ✅ 端口检测通过: {currentPort}");
|
2026-01-15 18:56:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
return currentPort;
|
|
|
|
|
|
}
|
2026-01-16 14:30:42 +08:00
|
|
|
|
logger.Debug($"[Core] ⚠️ 端口 {currentPort} 被占用,尝试下一个...");
|
2026-01-15 18:56:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
|
|
|
{
|
2026-01-17 19:17:49 +08:00
|
|
|
|
logger.Information("[Core] Hik、Dahua Sdk 开始预热.");
|
2026-01-15 18:56:39 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
HikNativeMethods.NET_DVR_Init();
|
|
|
|
|
|
HikSdkManager.ForceWarmUp();
|
2026-01-16 14:30:42 +08:00
|
|
|
|
logger.Information("[Core] 💡Hik Sdk 预热成功.");
|
2026-01-15 18:56:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-01-16 14:30:42 +08:00
|
|
|
|
logger.Error(ex, "[Core] ⚠️ Hik Sdk 预热失败.");
|
2026-01-15 18:56:39 +08:00
|
|
|
|
}
|
2026-01-17 19:17:49 +08:00
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
DahuaSdkManager.ForceWarmUp();
|
|
|
|
|
|
logger.Information("[Core] Dahua Sdk 预热成功.");
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.Error(ex, "[Core] ⚠️ Dahua Sdk 预热失败.");
|
|
|
|
|
|
}
|
2026-01-15 18:56:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region RegisterToGatewayAsync
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 向网关注册实例
|
|
|
|
|
|
/// </summary>
|
2026-01-16 07:23:56 +08:00
|
|
|
|
public static async Task RegisterToGatewayAsync(ServiceConfig config)
|
2026-01-15 18:56:39 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (!config.CommandEndpoints.Any()) return;
|
2026-01-16 07:23:56 +08:00
|
|
|
|
var gRpcLog = Log.ForContext("SourceContext", LogModules.gRpc);
|
2026-01-15 18:56:39 +08:00
|
|
|
|
|
2026-01-21 19:03:59 +08:00
|
|
|
|
// // Optimized: 并发任务集合,实现多目标同时注册
|
|
|
|
|
|
var registrationTasks = config.CommandEndpoints.Select(async endpoint =>
|
2026-01-15 18:56:39 +08:00
|
|
|
|
{
|
2026-01-21 19:03:59 +08:00
|
|
|
|
string targetUrl = endpoint.Uri.Replace("tcp://", "http://");
|
|
|
|
|
|
|
|
|
|
|
|
// // Modified: 将 try-catch 移入内部,确保单个端点失败不影响其他端点
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
using var channel = GrpcChannel.ForAddress(targetUrl);
|
|
|
|
|
|
var client = new GatewayProvider.GatewayProviderClient(channel);
|
2026-01-15 18:56:39 +08:00
|
|
|
|
|
2026-01-21 19:03:59 +08:00
|
|
|
|
gRpcLog.Information($"[gRpc] 正在执行预注册: {targetUrl}");
|
2026-01-15 18:56:39 +08:00
|
|
|
|
|
2026-01-21 19:03:59 +08:00
|
|
|
|
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)
|
2026-01-15 18:56:39 +08:00
|
|
|
|
{
|
2026-01-21 19:03:59 +08:00
|
|
|
|
// // 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}");
|
|
|
|
|
|
//}
|
2026-01-15 18:56:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
}
|