增加日志组件
This commit is contained in:
269
SHH.CameraService/Bootstrapper.cs
Normal file
269
SHH.CameraService/Bootstrapper.cs
Normal 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
|
||||
}
|
||||
@@ -1,121 +1,63 @@
|
||||
using Grpc.Net.Client;
|
||||
using Ayay.SerilogLogs;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Serilog;
|
||||
using SHH.CameraSdk;
|
||||
using SHH.Contracts.Grpc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace SHH.CameraService;
|
||||
|
||||
public class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// 主程序
|
||||
/// </summary>
|
||||
/// <param name="args"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
// 2. 硬件预热 (静态方法保留)
|
||||
HikNativeMethods.NET_DVR_Init();
|
||||
HikSdkManager.ForceWarmUp();
|
||||
|
||||
// 1. [核心环境] 必须在所有网络操作前开启
|
||||
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
|
||||
|
||||
// 2. 模拟/解析配置
|
||||
if (args.Length == 0)
|
||||
{
|
||||
string serviceArgs = "--appid CameraApp_01 " +
|
||||
"--uris localhost,9001,video,调试PC; " +
|
||||
"--uris localhost,9001,command,调试PC; " +
|
||||
"--mode 1 --ports 5000,100";
|
||||
args = serviceArgs.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
var config = ServiceConfig.BuildFromArgs(args);
|
||||
// 2. 解析配置与初始化日志
|
||||
var (config, opts, isDebugArgs) = Bootstrapper.ParseConfigAndInitLogger(args);
|
||||
var sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
||||
|
||||
// =============================================================
|
||||
// 3. 【强行复刻成功逻辑】 在 Web 容器启动前直接执行注册
|
||||
// 1. 启动日志
|
||||
// =============================================================
|
||||
if (config.CommandEndpoints.Any())
|
||||
{
|
||||
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);
|
||||
|
||||
Console.WriteLine($"[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 = "Camera Service"
|
||||
});
|
||||
Console.WriteLine($"[gRPC] 预注册成功: {resp.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[gRPC] 预注册尝试失败 (不影响启动): {ex.Message}");
|
||||
}
|
||||
}
|
||||
sysLog.Warning($"🚀 视频取流进程启动, 日志组件初始化完毕 => 进程: {opts.AppId}");
|
||||
|
||||
string argString = string.Join(" ", args);
|
||||
sysLog.Debug($"🚀 启动参数({(isDebugArgs ? "调试环境" : "生产环境")}): {argString}");
|
||||
|
||||
// =============================================================
|
||||
// 4. 构建 Web 主机环境
|
||||
// 2. 硬件预热、端口扫描、gRPC链接
|
||||
// =============================================================
|
||||
Bootstrapper.WarmUpHardware(sysLog);
|
||||
|
||||
// 端口自动扫描 (必须做,否则端口冲突)
|
||||
int activePort = Bootstrapper.ScanForAvailablePort(config, sysLog);
|
||||
if (activePort == -1)
|
||||
{
|
||||
sysLog.Fatal("💀 无法启动:配置范围内无可用端口");
|
||||
Bootstrapper.Shutdown("无法启动:配置范围内无可用端口", exitCode: 1);
|
||||
return;
|
||||
}
|
||||
config.UpdateActualPort(activePort); // 回填端口
|
||||
|
||||
// 具体的 gRPC 链接逻辑封装在 Bootstrapper 中,保持 Main 清爽但逻辑可见
|
||||
await Bootstrapper.RegisterToGatewayAsync(config, sysLog);
|
||||
|
||||
// =============================================================
|
||||
// 3. 构建 Web 主机环境
|
||||
// =============================================================
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// 基础业务单例注册
|
||||
builder.Services.AddSingleton(config);
|
||||
builder.Services.AddSingleton<ProcessingConfigManager>();
|
||||
builder.Services.AddSingleton(sp => new ImageScaleCluster(4, sp.GetRequiredService<ProcessingConfigManager>()));
|
||||
builder.Services.AddSingleton(sp => new ImageEnhanceCluster(4, sp.GetRequiredService<ProcessingConfigManager>()));
|
||||
builder.Services.AddHostedService<PipelineConfigurator>();
|
||||
// ★ 核心改动:一行代码注册所有业务 (SDK, Workers, gRPC, 视频流)
|
||||
builder.Services.AddCameraBusinessServices(config, sysLog);
|
||||
|
||||
// 接入 SDK 核心逻辑
|
||||
builder.Services.AddCameraSdk(config.NumericId);
|
||||
builder.Services.AddHostedService<CameraEngineWorker>();
|
||||
|
||||
// ★ 注册 gRPC 版本的状态监控工作者 (不讲道理,直接注册)
|
||||
builder.Services.AddHostedService<DeviceStatusHandler>();
|
||||
builder.Services.AddHostedService<ParentProcessSentinel>();
|
||||
builder.Services.AddHostedService<GatewayService>();
|
||||
|
||||
// =============================================================
|
||||
// 5. 视频流 Target 注册 (gRPC 模式)
|
||||
// =============================================================
|
||||
var netTargets = new List<StreamTarget>();
|
||||
if (config.VideoEndpoints != null)
|
||||
{
|
||||
foreach (var cfgVideo in config.VideoEndpoints)
|
||||
{
|
||||
netTargets.Add(new StreamTarget(new PushTargetConfig
|
||||
{
|
||||
Name = cfgVideo.Description,
|
||||
Endpoint = cfgVideo.Uri,
|
||||
QueueCapacity = 10,
|
||||
}));
|
||||
}
|
||||
}
|
||||
builder.Services.AddSingleton<IEnumerable<StreamTarget>>(netTargets);
|
||||
builder.Services.AddHostedService<ImageMonitorController>();
|
||||
|
||||
// 为每个 Target 绑定一个 gRPC 流发送者
|
||||
foreach (var target in netTargets)
|
||||
{
|
||||
builder.Services.AddHostedService(sp =>
|
||||
new GrpcSenderWorker(target, sp.GetRequiredService<ILogger<GrpcSenderWorker>>()));
|
||||
}
|
||||
|
||||
// 注册指令分发 (不再使用 NetMQ 的 CommandClientWorker)
|
||||
builder.Services.AddSingleton<InterceptorPipeline>();
|
||||
builder.Services.AddSingleton<CommandDispatcher>();
|
||||
builder.Services.AddSingleton<ICommandHandler, DeviceConfigHandler>();
|
||||
builder.Services.AddSingleton<ICommandHandler, RemoveCameraHandler>();
|
||||
|
||||
ConfigureWebServices(builder, config);
|
||||
// ★ 核心改动:注册 Web 基础 (Controller, Swagger, Cors)
|
||||
builder.Services.AddWebSupport(config);
|
||||
|
||||
// =============================================================
|
||||
// 6. 启动服务
|
||||
@@ -123,51 +65,66 @@ public class Program
|
||||
var app = builder.Build();
|
||||
|
||||
// 激活 SDK 管理器并启动业务点火
|
||||
await StartBusinessLogic(app);
|
||||
await StartBusinessLogic(app, sysLog);
|
||||
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", $"SHH Gateway #{config.AppId}"));
|
||||
// ★ 核心改动:配置 HTTP 管道 (Swagger, MapControllers 等)
|
||||
app.ConfigurePipeline(config);
|
||||
|
||||
app.MapGet("/", () => $"SHH Gateway {config.AppId} is running (gRPC Mode).");
|
||||
app.UseCors("AllowAll");
|
||||
app.MapControllers();
|
||||
// 启动监听
|
||||
string url = $"http://0.0.0.0:{config.BasePort}";
|
||||
sysLog.Information($"🚀 [WebApi] 服务启动,监听: {url}");
|
||||
|
||||
Console.WriteLine($"[System] 正在启动 Web 服务,监听端口: {config.BasePort}");
|
||||
await app.RunAsync($"http://0.0.0.0:{config.BasePort}");
|
||||
await app.RunAsync(url);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 激活单例并启动相机管理器
|
||||
/// </summary>
|
||||
static async Task StartBusinessLogic(WebApplication app)
|
||||
/// <param name="app"></param>
|
||||
/// <param name="logger"></param>
|
||||
static async Task StartBusinessLogic(WebApplication app, Serilog.ILogger logger)
|
||||
{
|
||||
var manager = app.Services.GetRequiredService<CameraManager>();
|
||||
|
||||
// 激活哨兵
|
||||
_ = app.Services.GetRequiredService<ConnectivitySentinel>();
|
||||
|
||||
await manager.StartAsync();
|
||||
Console.WriteLine("[System] 核心业务逻辑已激活。");
|
||||
Console.WriteLine("✅[System] 核心业务逻辑已激活。");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册 Web API 支持
|
||||
/// </summary>
|
||||
static void ConfigureWebServices(WebApplicationBuilder builder, ServiceConfig cfg)
|
||||
{
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowAll", p => p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
|
||||
});
|
||||
|
||||
builder.Services.AddControllers(options =>
|
||||
{
|
||||
options.Filters.Add<UserActionFilter>();
|
||||
})
|
||||
.AddApplicationPart(typeof(CamerasController).Assembly)
|
||||
.AddApplicationPart(typeof(MonitorController).Assembly);
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = $"SHH Gateway #{cfg.AppId}", Version = "v1" });
|
||||
});
|
||||
}
|
||||
}
|
||||
/*
|
||||
🚀 启动/发布 程序启动、服务预热、开始监听
|
||||
🏁 结束/终点 批量任务全部完成、程序正常退出
|
||||
🔄 重试/循环 正在重试连接、定时任务触发、同步数据中
|
||||
⏳ 等待/耗时 长耗时操作开始、排队中
|
||||
💤 休眠/闲置 线程挂起、服务进入待机模式
|
||||
🌐 网络/HTTP HTTP 请求、API 调用、Web 服务
|
||||
🔌 连接 数据库连接建立、Socket 连接
|
||||
📡 信号/广播 发送 MQ 消息、广播通知、取流
|
||||
💾 存储/磁盘 写入文件、数据库落盘、缓存读写
|
||||
🔒 安全/锁 加密、解密、登录成功、获取分布式锁
|
||||
⚙️ 配置/系统 加载配置、系统底层操作
|
||||
🐞 Bug/调试 捕捉到的异常、临时调试信息
|
||||
🧪 测试/实验 单元测试环境、灰度测试代码
|
||||
🔍 搜索/检查 查询数据库、检查文件是否存在
|
||||
💡 提示/发现 逻辑分支提示、参数值打印
|
||||
🔴 红:致命/严重 (Fatal/Error)
|
||||
🟡 黄:警告 (Warning)
|
||||
🟢 绿:正常/成功 (Info/Success)
|
||||
🔵 蓝:数据/网络 (Data/Network)
|
||||
⚪ 灰:细节/忽略 (Debug/Verbose)
|
||||
✅ Check Mark Button \u{2705} ✅
|
||||
🆗 OK Button \u{1F197} 🆗
|
||||
🔚 END Arrow (逻辑结束) \u{1F51A} 🔚
|
||||
💯 Hundred Points (完美结束) \u{1F4AF} 💯
|
||||
🛑 Stop Sign (最强提示) \u{1F6D1} 🛑
|
||||
⛔ No Entry (禁止/中断) \u{26D4} ⛔
|
||||
🚫 Prohibited (非法操作终止) \u{1F6AB} 🚫
|
||||
⏹️ Stop Button (播放器风格) \u{23F9} ⏹
|
||||
❌ Cross Mark (任务失败结束) \u{274C} ❌
|
||||
💀 Skull (进程被 Kill) \u{1F480} 💀
|
||||
⚰️ Coffin (彻底销毁) \u{26B0} ⚰
|
||||
👻 Ghost (变成孤儿进程) \u{1F47B} 👻
|
||||
*/
|
||||
@@ -22,6 +22,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ayay.SerilogLogs\Ayay.SerilogLogs.csproj" />
|
||||
<ProjectReference Include="..\SHH.CameraSdk\SHH.CameraSdk.csproj" />
|
||||
<ProjectReference Include="..\SHH.Contracts.Grpc\SHH.Contracts.Grpc.csproj" />
|
||||
<ProjectReference Include="..\SHH.Contracts\SHH.Contracts.csproj" />
|
||||
|
||||
155
SHH.CameraService/Utils/ServiceCollectionExtensions.cs
Normal file
155
SHH.CameraService/Utils/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging; // 用于泛型 ILogger<>
|
||||
using Microsoft.OpenApi.Models;
|
||||
using SHH.CameraSdk;
|
||||
|
||||
// 确保 namespace 与 Program.cs 的引用一致
|
||||
namespace SHH.CameraService;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
// ========================================================================
|
||||
// 1. 核心聚合方法 (对应 Program.cs 中的调用)
|
||||
// ========================================================================
|
||||
|
||||
#region AddCameraBusinessServices
|
||||
|
||||
/// <summary>
|
||||
/// [聚合] 注册所有核心业务 (包含逻辑处理、SDK、Gateway、视频流)
|
||||
/// </summary>
|
||||
public static void AddCameraBusinessServices(this IServiceCollection services, ServiceConfig config, Serilog.ILogger logger)
|
||||
{
|
||||
// 1.1 注册基础业务逻辑
|
||||
services.AddBusinessServices(config);
|
||||
|
||||
// 1.2 注册视频流相关 (因为需要 Logger,所以单独传)
|
||||
services.AddStreamTargets(config, logger);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AddBusinessServices
|
||||
|
||||
/// <summary>
|
||||
/// 集中注册 SDK、Workers、Gateway 等纯业务服务
|
||||
/// </summary>
|
||||
private static void AddBusinessServices(this IServiceCollection services, ServiceConfig config)
|
||||
{
|
||||
// 基础单例
|
||||
services.AddSingleton(config);
|
||||
services.AddSingleton<ProcessingConfigManager>();
|
||||
|
||||
// 图像处理集群
|
||||
services.AddSingleton(sp => new ImageScaleCluster(4, sp.GetRequiredService<ProcessingConfigManager>()));
|
||||
services.AddSingleton(sp => new ImageEnhanceCluster(4, sp.GetRequiredService<ProcessingConfigManager>()));
|
||||
services.AddHostedService<PipelineConfigurator>();
|
||||
|
||||
// SDK 与核心引擎
|
||||
services.AddCameraSdk(config.NumericId);
|
||||
services.AddHostedService<CameraEngineWorker>();
|
||||
|
||||
// 监控与网关
|
||||
services.AddHostedService<DeviceStatusHandler>();
|
||||
services.AddHostedService<ParentProcessSentinel>();
|
||||
services.AddHostedService<GatewayService>();
|
||||
|
||||
// 指令分发系统
|
||||
services.AddSingleton<InterceptorPipeline>();
|
||||
services.AddSingleton<CommandDispatcher>();
|
||||
services.AddSingleton<ICommandHandler, DeviceConfigHandler>();
|
||||
services.AddSingleton<ICommandHandler, RemoveCameraHandler>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AddStreamTargets
|
||||
|
||||
/// <summary>
|
||||
/// 注册视频流目标 (StreamTargets & GrpcSender)
|
||||
/// </summary>
|
||||
private static void AddStreamTargets(this IServiceCollection services, ServiceConfig config, Serilog.ILogger logger)
|
||||
{
|
||||
var netTargets = new List<StreamTarget>();
|
||||
if (config.VideoEndpoints != null)
|
||||
{
|
||||
foreach (var cfgVideo in config.VideoEndpoints)
|
||||
{
|
||||
netTargets.Add(new StreamTarget(new PushTargetConfig
|
||||
{
|
||||
Name = cfgVideo.Description,
|
||||
Endpoint = cfgVideo.Uri,
|
||||
QueueCapacity = 10,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
logger.Information("📋 加载视频流目标: {Count} 个", netTargets.Count);
|
||||
|
||||
services.AddSingleton<IEnumerable<StreamTarget>>(netTargets);
|
||||
services.AddHostedService<ImageMonitorController>();
|
||||
|
||||
// 动态注册 Sender Worker
|
||||
foreach (var target in netTargets)
|
||||
{
|
||||
// 注意:这里需要使用 Microsoft.Extensions.Logging.ILogger 来适配构造函数
|
||||
services.AddHostedService(sp =>
|
||||
new GrpcSenderWorker(target, sp.GetRequiredService<ILogger<GrpcSenderWorker>>()));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// ========================================================================
|
||||
// 2. Web 支持与管道 (Program.cs 调用的另外两个方法)
|
||||
// ========================================================================
|
||||
|
||||
#region AddWebSupport
|
||||
|
||||
/// <summary>
|
||||
/// 注册 Controller, Swagger, Cors
|
||||
/// </summary>
|
||||
public static void AddWebSupport(this IServiceCollection services, ServiceConfig config)
|
||||
{
|
||||
services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowAll", p => p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
|
||||
});
|
||||
|
||||
// 注册 Controller 并添加过滤器
|
||||
services.AddControllers(options =>
|
||||
{
|
||||
options.Filters.Add<UserActionFilter>();
|
||||
})
|
||||
.AddApplicationPart(typeof(CamerasController).Assembly) // 确保能扫描到 Controller
|
||||
.AddApplicationPart(typeof(MonitorController).Assembly);
|
||||
|
||||
services.AddEndpointsApiExplorer();
|
||||
services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = $"SHH Gateway #{config.AppId}", Version = "v1" });
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ConfigurePipeline
|
||||
|
||||
/// <summary>
|
||||
/// 配置 HTTP 中间件管道
|
||||
/// </summary>
|
||||
public static void ConfigurePipeline(this WebApplication app, ServiceConfig config)
|
||||
{
|
||||
// 开启 Swagger
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", $"SHH Gateway #{config.AppId}"));
|
||||
|
||||
// 简单健康检查端点
|
||||
app.MapGet("/", () => $"SHH Gateway {config.AppId} is running.");
|
||||
|
||||
app.UseCors("AllowAll");
|
||||
app.MapControllers();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user