规范并补充日志内容
This commit is contained in:
@@ -38,8 +38,8 @@ public static class Bootstrapper
|
||||
"--uris", "localhost,9001,command,调试PC;",
|
||||
|
||||
// 日志中心配置 (格式: IP,Port,Desc)
|
||||
"--sequris", "58.216.225.5,20026,日志处置中心;",
|
||||
"--seqkey", "Shine101173874928;",
|
||||
"--sequris", "172.16.41.241,20026,日志处置中心;",
|
||||
"--seqkey", "Shine899195994250;",
|
||||
|
||||
// 端口策略
|
||||
"--mode", "1",
|
||||
@@ -100,11 +100,11 @@ public static class Bootstrapper
|
||||
// =========================================================
|
||||
if (exitCode != 0)
|
||||
{
|
||||
sysLog.Fatal($"💀 [程序终止] {reason} (Code: {exitCode})");
|
||||
sysLog.Fatal($"[Core] 💀 [程序终止] {reason} (Code: {exitCode})");
|
||||
}
|
||||
else
|
||||
{
|
||||
sysLog.Information($"👋 [程序退出] {reason}");
|
||||
sysLog.Information($"[Core] 👋 [程序退出] {reason}");
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
@@ -113,7 +113,7 @@ public static class Bootstrapper
|
||||
// 防止 SDK 句柄残留导致下次启动无法连接相机
|
||||
try
|
||||
{
|
||||
sysLog.Information("正在清理 Hikvision SDK 资源...");
|
||||
sysLog.Information("[Core] 正在清理 Hikvision SDK 资源...");
|
||||
|
||||
// 如果你的项目中引用了 SDK,请务必解开这行注释
|
||||
HikNativeMethods.NET_DVR_Cleanup();
|
||||
@@ -162,7 +162,7 @@ public static class Bootstrapper
|
||||
/// </summary>
|
||||
public static int ScanForAvailablePort(ServiceConfig config, ILogger logger)
|
||||
{
|
||||
logger.Information($"🔍 开始端口检测: 起始={config.BasePort}, 范围={config.MaxPortRange}");
|
||||
logger.Information($"[Core] 🔍 开始端口检测: 起始={config.BasePort}, 范围={config.MaxPortRange}");
|
||||
|
||||
for (int i = 0; i <= config.MaxPortRange; i++)
|
||||
{
|
||||
@@ -171,15 +171,15 @@ public static class Bootstrapper
|
||||
{
|
||||
if (currentPort != config.BasePort)
|
||||
{
|
||||
logger.Warning($"⚙️ 端口自动漂移: {config.BasePort} -> {currentPort}");
|
||||
logger.Warning($"[Core] ⚙️ 端口自动漂移: {config.BasePort} -> {currentPort}");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Information($"✅ 端口检测通过: {currentPort}");
|
||||
logger.Information($"[Core] ✅ 端口检测通过: {currentPort}");
|
||||
}
|
||||
return currentPort;
|
||||
}
|
||||
logger.Debug($"⚠️ 端口 {currentPort} 被占用,尝试下一个...");
|
||||
logger.Debug($"[Core] ⚠️ 端口 {currentPort} 被占用,尝试下一个...");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -217,16 +217,16 @@ public static class Bootstrapper
|
||||
/// </summary>
|
||||
public static void WarmUpHardware(ILogger logger)
|
||||
{
|
||||
logger.Information("Hik Sdk 开始预热.");
|
||||
logger.Information("[Core] Hik Sdk 开始预热.");
|
||||
try
|
||||
{
|
||||
HikNativeMethods.NET_DVR_Init();
|
||||
HikSdkManager.ForceWarmUp();
|
||||
logger.Information("💡Hik Sdk 预热成功.");
|
||||
logger.Information("[Core] 💡Hik Sdk 预热成功.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "⚠️ Hik Sdk 预热失败.");
|
||||
logger.Error(ex, "[Core] ⚠️ Hik Sdk 预热失败.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,13 +244,13 @@ public static class Bootstrapper
|
||||
|
||||
try
|
||||
{
|
||||
// 将 tcp:// 转换为 http:// 以适配 gRPC
|
||||
// 将 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}");
|
||||
gRpcLog.Information($"[gRpc] 正在执行预注册: {targetUrl}");
|
||||
var resp = await client.RegisterInstanceAsync(new RegisterRequest
|
||||
{
|
||||
InstanceId = config.AppId,
|
||||
@@ -261,11 +261,11 @@ public static class Bootstrapper
|
||||
ProcessId = Environment.ProcessId,
|
||||
Description = ""
|
||||
});
|
||||
gRpcLog.Information($"💡[gRPC] 预注册成功: {resp.Message}");
|
||||
gRpcLog.Information($"[gRpc] 💡预注册成功: {resp.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
gRpcLog.Error($"⚠️ [gRPC] 预注册尝试失败: {ex.Message}");
|
||||
gRpcLog.Error($"[gRpc] ⚠️ 预注册尝试失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ using SHH.Contracts.Grpc;
|
||||
namespace SHH.CameraService;
|
||||
|
||||
/// <summary>
|
||||
/// gRPC 指令分发器
|
||||
/// gRpc 指令分发器
|
||||
/// 职责:接收从 GrpcCommandReceiverWorker 传入的 Proto 消息,解析参数并路由至具体的 Handler。
|
||||
/// </summary>
|
||||
public class CommandDispatcher
|
||||
@@ -29,14 +29,14 @@ public class CommandDispatcher
|
||||
/// <summary>
|
||||
/// 执行指令分发
|
||||
/// </summary>
|
||||
/// <param name="protoMsg">从 gRPC Server Streaming 接收到的原始 Proto 指令对象</param>
|
||||
/// <param name="protoMsg">从 gRpc Server Streaming 接收到的原始 Proto 指令对象</param>
|
||||
public async Task DispatchAsync(CommandPayloadProto protoMsg)
|
||||
{
|
||||
if (protoMsg == null) return;
|
||||
|
||||
string cmdCode = protoMsg.CmdCode; // 例如 "Sync_Camera"
|
||||
_gRpcLog.Information($"[gRPC] 响应请求, 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 业务分发.");
|
||||
_gRpcLog.Debug($"[gRPC] 响应请求, {protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 业务分发 => {protoMsg}");
|
||||
_gRpcLog.Information($"[gRpc] 响应请求, 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 业务分发.");
|
||||
_gRpcLog.Debug($"[gRpc] 响应请求, {protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 业务分发 => {protoMsg}");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -51,20 +51,20 @@ public class CommandDispatcher
|
||||
// 3. 调用具体业务执行
|
||||
await handler.ExecuteAsync(token);
|
||||
|
||||
_gRpcLog.Information($"[gRPC] 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 执行成功.");
|
||||
_gRpcLog.Information($"[gRpc] 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 执行成功.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_gRpcLog.Warning($"[gRPC] 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 未找到指令处理器.");
|
||||
_gRpcLog.Warning($"[gRpc] 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 未找到指令处理器.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_gRpcLog.Error($"[gRPC] 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 执行指令处理异常: {ex.Message}.");
|
||||
_gRpcLog.Error($"[gRpc] 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 执行指令处理异常: {ex.Message}.");
|
||||
}
|
||||
|
||||
// 注意:关于 ACK (require_ack)
|
||||
// 在 NetMQ 时代需要手动回发结果,在 gRPC Server Streaming 模式下,
|
||||
// 在 NetMQ 时代需要手动回发结果,在 gRpc Server Streaming 模式下,
|
||||
// 建议通过 Unary RPC (例如另设一个 ReportCommandResult 方法) 异步上报执行结果。
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,52 @@
|
||||
using System.Diagnostics;
|
||||
using Ayay.SerilogLogs;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using SHH.CameraSdk;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SHH.CameraService;
|
||||
|
||||
/// <summary>
|
||||
/// 父进程守护服务 (BackgroundService)
|
||||
/// <para>核心逻辑:定期检查启动本服务的父进程是否存活,若父进程退出(如 UI 崩溃),则触发本服务自动退出,避免孤儿进程占用相机硬件资源。</para>
|
||||
/// </summary>
|
||||
public class ParentProcessSentinel : BackgroundService
|
||||
{
|
||||
private readonly ServiceConfig _config;
|
||||
private readonly IHostApplicationLifetime _lifetime;
|
||||
private readonly ILogger<ParentProcessSentinel> _logger;
|
||||
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
||||
|
||||
/// <summary>
|
||||
/// 使用统一的结构化日志记录器,SourceContext 设置为 Core 模块
|
||||
/// </summary>
|
||||
public ParentProcessSentinel(
|
||||
ServiceConfig config,
|
||||
IHostApplicationLifetime lifetime,
|
||||
ILogger<ParentProcessSentinel> logger)
|
||||
IHostApplicationLifetime lifetime)
|
||||
{
|
||||
_config = config;
|
||||
_lifetime = lifetime;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行后台守护逻辑
|
||||
/// </summary>
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
int pid = _config.ParentPid;
|
||||
|
||||
// 如果 PID 为 0 或负数,说明不需要守护(可能是手动启动调试)
|
||||
// 1. 验证 PID 合法性。如果 PID 为 0 或负数,可能是手动启动调试模式,不执行守护逻辑
|
||||
if (pid <= 0)
|
||||
{
|
||||
_logger.LogInformation("未指定有效的父进程 PID,守护模式已禁用。");
|
||||
_sysLog.Warning("[Sentinel] 未指定有效的父进程 PID ({ParentPid}),守护模式已禁用,服务将持续运行.", pid);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation($"父进程守护已启动,正在监控 PID: {pid}");
|
||||
_sysLog.Information("[Sentinel] 父进程守护已启动,正在监控目标 PID: {ParentPid}", pid);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
if (!IsParentRunning(pid))
|
||||
{
|
||||
_logger.LogWarning($"[ALERT] 检测到父进程 (PID:{pid}) 已退出!正在终止当前服务...");
|
||||
_sysLog.Warning("[Sentinel] ### ALERT ### 检测到父进程 (PID:{ParentPid}) 已退出!正在下发系统终止信号...", pid);
|
||||
|
||||
// 触发程序优雅退出
|
||||
_lifetime.StopApplication();
|
||||
@@ -52,6 +60,11 @@ public class ParentProcessSentinel : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 核心状态判定:通过 PID 获取进程快照并检查存活状态
|
||||
/// </summary>
|
||||
/// <param name="pid">父进程 ID</param>
|
||||
/// <returns>存活返回 True,已消亡返回 False</returns>
|
||||
private bool IsParentRunning(int pid)
|
||||
{
|
||||
try
|
||||
@@ -72,7 +85,7 @@ public class ParentProcessSentinel : BackgroundService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "检查父进程状态时发生未知错误,默认为存活");
|
||||
_sysLog.Debug("[Sentinel] 无法定位 PID 为 {ParentPid} 的进程,判定为已退出.", pid);
|
||||
return true; // 发生未知错误时,保守起见认为它还活着
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace SHH.CameraService;
|
||||
public class DeviceConfigHandler : ICommandHandler
|
||||
{
|
||||
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
||||
|
||||
private readonly CameraManager _cameraManager;
|
||||
|
||||
/// <summary>
|
||||
@@ -42,12 +43,15 @@ public class DeviceConfigHandler : ICommandHandler
|
||||
// 2. 尝试获取现有设备
|
||||
var device = _cameraManager.GetDevice(dto.Id);
|
||||
|
||||
string op = device != null ? "更新" : "新增";
|
||||
_sysLog.Warning($"[Sync] 即将{op}设备配置 => ID:{dto.Id} Name:{dto.Name} IP:{dto.IpAddress} Port:{dto.Port} Brand:{(DeviceBrand)dto.Brand} Rtsp:{dto.RtspPath}");
|
||||
_sysLog.Debug($"[Sync] 即将{op}设备配置 => ID:{dto.Id} Name:{dto.Name} IP:{dto.IpAddress} 详情:" + "{@dto}", dto, dto.AutoSubscriptions);
|
||||
|
||||
if (device != null)
|
||||
{
|
||||
// =========================================================
|
||||
// 场景 A: 设备已存在 -> 执行智能更新 (Smart Update)
|
||||
// =========================================================
|
||||
Console.WriteLine($"[Sync] 更新设备配置: {dto.Id} ({dto.Name})");
|
||||
|
||||
// 将全量配置映射为部分更新 DTO
|
||||
var updateDto = new DeviceUpdateDto
|
||||
@@ -86,7 +90,6 @@ public class DeviceConfigHandler : ICommandHandler
|
||||
// =========================================================
|
||||
// 场景 B: 设备不存在 -> 执行新增 (Add New)
|
||||
// =========================================================
|
||||
Console.WriteLine($"[Sync] 新增设备: {dto.Id} ({dto.Name})");
|
||||
|
||||
// 构造全新的设备配置
|
||||
var newConfig = new VideoSourceConfig
|
||||
@@ -126,7 +129,7 @@ public class DeviceConfigHandler : ICommandHandler
|
||||
// 情况 1: 收到“启动”指令
|
||||
if (!device.IsOnline) // 只有没在线时才点火
|
||||
{
|
||||
Console.WriteLine($"[Sync] 指令:立即启动设备 {dto.Id}");
|
||||
_sysLog.Warning($"[Sync] 设备立即启动 => ID:{dto.Id} Name:{dto.Name} IP:{dto.IpAddress} Port:{dto.Port} Brand:{(DeviceBrand)dto.Brand} Rtsp:{dto.RtspPath}");
|
||||
_ = device.StartAsync();
|
||||
}
|
||||
}
|
||||
@@ -135,7 +138,7 @@ public class DeviceConfigHandler : ICommandHandler
|
||||
// 情况 2: 收到“停止”指令 (即 ImmediateExecution = false)
|
||||
if (device.IsOnline) // 只有在线时才熄火
|
||||
{
|
||||
Console.WriteLine($"[Sync] 指令:立即停止设备 {dto.Id}");
|
||||
_sysLog.Warning($"[Sync] 设备立即停止 {dto.Id}");
|
||||
_ = device.StopAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using Grpc.Core;
|
||||
using Ayay.SerilogLogs;
|
||||
using Grpc.Core;
|
||||
using Grpc.Net.Client;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using SHH.CameraSdk;
|
||||
using SHH.Contracts;
|
||||
using SHH.Contracts.Grpc;
|
||||
@@ -10,14 +11,15 @@ using System.Collections.Concurrent;
|
||||
namespace SHH.CameraService;
|
||||
|
||||
/// <summary>
|
||||
/// 设备状态监控工作者 (gRPC 版)
|
||||
/// 职责:监控相机状态并在状态变更或心跳周期内,通过 gRPC 批量上报至所有配置的端点
|
||||
/// 设备状态监控工作者 (gRpc 版)
|
||||
/// 职责:监控相机状态并在状态变更或心跳周期内,通过 gRpc 批量上报至所有配置的端点
|
||||
/// </summary>
|
||||
public class DeviceStatusHandler : BackgroundService
|
||||
{
|
||||
private static ILogger _gRpcLog = Log.ForContext("SourceContext", LogModules.gRpc);
|
||||
|
||||
private readonly CameraManager _manager;
|
||||
private readonly ServiceConfig _config;
|
||||
private readonly ILogger<DeviceStatusHandler> _logger;
|
||||
|
||||
// 状态存储:CameraId -> 状态载荷
|
||||
private readonly ConcurrentDictionary<string, StatusEventPayload> _stateStore = new();
|
||||
@@ -27,12 +29,10 @@ public class DeviceStatusHandler : BackgroundService
|
||||
|
||||
public DeviceStatusHandler(
|
||||
CameraManager manager,
|
||||
ServiceConfig config,
|
||||
ILogger<DeviceStatusHandler> logger)
|
||||
ServiceConfig config)
|
||||
{
|
||||
_manager = manager;
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
@@ -46,7 +46,7 @@ public class DeviceStatusHandler : BackgroundService
|
||||
// 2. 订阅 SDK 状态变更事件
|
||||
_manager.OnDeviceStatusChanged += OnSdkStatusChanged;
|
||||
|
||||
_logger.LogInformation("[StatusWorker] gRPC 状态上报已启动,配置节点数: {Count}", _config.CommandEndpoints.Count);
|
||||
_gRpcLog.Information($"[gRpc] 状态上报已启动,配置节点数: {_config.CommandEndpoints.Count}");
|
||||
|
||||
// 3. 定时循环 (1秒1次检查)
|
||||
var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
|
||||
@@ -60,7 +60,7 @@ public class DeviceStatusHandler : BackgroundService
|
||||
catch (OperationCanceledException) { /* 正常退出 */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[StatusWorker] 运行异常");
|
||||
_gRpcLog.Error($"[gRpc] 状态上报运行异常");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -96,12 +96,12 @@ public class DeviceStatusHandler : BackgroundService
|
||||
{
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
// 策略: 有变更(Dirty) 或 超过5秒(强制心跳)
|
||||
bool shouldSend = _isDirty || (now - _lastSendTick > 5000);
|
||||
// 策略: 有变更(Dirty) 或 超过 2 秒(强制心跳)
|
||||
bool shouldSend = _isDirty || (now - _lastSendTick > 2000);
|
||||
|
||||
if (shouldSend && _config.CommandEndpoints.Any())
|
||||
{
|
||||
// 1. 构建 gRPC 请求包
|
||||
// 1. 构建 gRpc 请求包
|
||||
var request = new StatusBatchRequest
|
||||
{
|
||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||
@@ -129,13 +129,12 @@ public class DeviceStatusHandler : BackgroundService
|
||||
using var channel = GrpcChannel.ForAddress(grpcUrl);
|
||||
var client = new GatewayProvider.GatewayProviderClient(channel);
|
||||
|
||||
// 获取 gRPC 内部生成的服务全称
|
||||
// 获取 gRpc 内部生成的服务全称
|
||||
// 这就是客户端尝试调用的真实路径:/包名.服务名/方法名
|
||||
var methodName = "ReportStatusBatch";
|
||||
var serviceName = client.GetType().DeclaringType?.Name ?? "Unknown";
|
||||
|
||||
_logger.LogInformation("[gRPC Debug] 准备调用端点: {Url}", grpcUrl);
|
||||
_logger.LogInformation("[gRPC Debug] 客户端契约服务名: {Service}", serviceName);
|
||||
_gRpcLog.Debug("[gRpc] 准备调用端点: {Url}", grpcUrl);
|
||||
_gRpcLog.Debug("[gRpc] 客户端契约服务名: {Service}", serviceName);
|
||||
|
||||
// 执行调用
|
||||
var response = await client.ReportStatusBatchAsync(request,
|
||||
@@ -143,7 +142,8 @@ public class DeviceStatusHandler : BackgroundService
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
_logger.LogInformation("[gRPC Success] 上报成功");
|
||||
_gRpcLog.Information("[gRpc] 设备状态上报成功, 共计: {Count} 个, Url: {Url}", request.Items.Count, grpcUrl);
|
||||
_gRpcLog.Debug("[gRpc] 设备状态上报成功: {Url} Items:{Items}", grpcUrl, request.Items);
|
||||
_isDirty = false;
|
||||
_lastSendTick = Environment.TickCount64;
|
||||
}
|
||||
@@ -151,17 +151,17 @@ public class DeviceStatusHandler : BackgroundService
|
||||
catch (RpcException ex)
|
||||
{
|
||||
// 这里是关键:打印 RpcException 的详细状态
|
||||
_logger.LogError("[gRPC Error] StatusCode: {Code}, Detail: {Detail}", ex.StatusCode, ex.Status.Detail);
|
||||
_gRpcLog.Error("[gRpc] StatusCode: {Code}, Detail: {Detail}", ex.StatusCode, ex.Status.Detail);
|
||||
|
||||
// 如果是 Unimplemented,通常意味着路径不对
|
||||
if (ex.StatusCode == StatusCode.Unimplemented)
|
||||
{
|
||||
_logger.LogError("[gRPC Fix] 请检查服务端是否注册了名为 'GatewayProvider' 的服务,且其 package 声明与客户端一致。");
|
||||
_gRpcLog.Error("[gRpc] 请检查服务端是否注册了名为 'GatewayProvider' 的服务,且其 package 声明与客户端一致。");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("[gRPC Fatal] 非 RPC 异常: {Msg}", ex.Message);
|
||||
_gRpcLog.Error("[gRpc] 非 RPC 异常: {Msg}", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ using SHH.Contracts.Grpc; // 引用 Proto 生成的命名空间
|
||||
namespace SHH.CameraService
|
||||
{
|
||||
/// <summary>
|
||||
/// gRPC 指令接收后台服务
|
||||
/// gRpc 指令接收后台服务
|
||||
/// 职责:
|
||||
/// 1. 维护与 AiVideo 的 gRPC 长连接。
|
||||
/// 1. 维护与 AiVideo 的 gRpc 长连接。
|
||||
/// 2. 完成节点逻辑注册。
|
||||
/// 3. 监听 Server Streaming 指令流并移交给 Dispatcher。
|
||||
/// </summary>
|
||||
@@ -33,7 +33,7 @@ namespace SHH.CameraService
|
||||
var gRpcLog = Log.ForContext("SourceContext", LogModules.gRpc);
|
||||
|
||||
// 预留系统启动缓冲时间,确保数据库和 SDK 已就绪
|
||||
gRpcLog.Information("[gRPC] 指令接收服务启动,等待环境预热...");
|
||||
gRpcLog.Information("[gRpc] 指令接收服务启动,等待环境预热...");
|
||||
await Task.Delay(3000, stoppingToken);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
@@ -48,7 +48,7 @@ namespace SHH.CameraService
|
||||
var client = new GatewayProvider.GatewayProviderClient(channel);
|
||||
|
||||
// --- 第一步:发起节点逻辑注册 (Unary) ---
|
||||
gRpcLog.Information("[gRPC] 正在发起逻辑注册: {Url}", targetUrl);
|
||||
gRpcLog.Information("[gRpc] 正在发起逻辑注册: {Url}", targetUrl);
|
||||
var regResp = await client.RegisterInstanceAsync(new RegisterRequest
|
||||
{
|
||||
InstanceId = _config.AppId,
|
||||
@@ -59,7 +59,7 @@ namespace SHH.CameraService
|
||||
|
||||
if (regResp.Success)
|
||||
{
|
||||
gRpcLog.Information("[gRPC] 注册成功, 正在建立双向指令通道...");
|
||||
gRpcLog.Information("[gRpc] 注册成功, 正在建立双向指令通道...");
|
||||
|
||||
// --- 第二步:开启 Server Streaming 指令流 ---
|
||||
using var call = client.OpenCommandChannel(new CommandStreamRequest
|
||||
@@ -86,14 +86,14 @@ namespace SHH.CameraService
|
||||
}
|
||||
catch (RpcException ex)
|
||||
{
|
||||
gRpcLog.Debug("[gRPC] RPC 异常 (Status: {Code}): {Msg}", ex.StatusCode, ex.Message);
|
||||
gRpcLog.Debug("[gRpc] RPC 异常 (Status: {Code}): {Msg}", ex.StatusCode, ex.Message);
|
||||
|
||||
// 链路异常,进入重连等待阶段
|
||||
await Task.Delay(5000, stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
gRpcLog.Debug("[gRPC] 非预期链路异常: {Msg},5秒后尝试重连", ex.Message);
|
||||
gRpcLog.Debug("[gRpc] 非预期链路异常: {Msg},5秒后尝试重连", ex.Message);
|
||||
await Task.Delay(5000, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Ayay.SerilogLogs;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
using SHH.CameraSdk;
|
||||
using SHH.Contracts;
|
||||
|
||||
@@ -9,6 +11,8 @@ namespace SHH.CameraService
|
||||
/// </summary>
|
||||
public class RemoveCameraHandler : ICommandHandler
|
||||
{
|
||||
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
||||
|
||||
private readonly CameraManager _cameraManager;
|
||||
|
||||
/// <summary>
|
||||
@@ -50,7 +54,7 @@ namespace SHH.CameraService
|
||||
|
||||
if (deviceId <= 0)
|
||||
{
|
||||
Console.WriteLine($"[{ActionName}] 收到无效指令: ID解析失败 ({payload})");
|
||||
_sysLog.Warning($"[Sync] 收到无效指令, ID解析失败 ({payload})");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,26 +62,25 @@ namespace SHH.CameraService
|
||||
var device = _cameraManager.GetDevice(deviceId);
|
||||
if (device == null)
|
||||
{
|
||||
Console.WriteLine($"[{ActionName}] 设备 {deviceId} 已经不在管理池中,无需操作。");
|
||||
_sysLog.Warning($"[Sync] 设备 {deviceId} 已经不在管理池中,无需操作.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 安全移除
|
||||
// 这里建议增加审计日志,记录谁触发了删除(如果协议里有用户信息的话)
|
||||
device.AddAuditLog("收到远程指令:彻底移除设备");
|
||||
Console.WriteLine($"[{ActionName}] 正在安全移除设备: {deviceId} ({device.Config.Name})");
|
||||
_sysLog.Debug($"[Sync] 收到远程指令, 正在安全移除设备, ID:{deviceId} Name:{device.Config.Name} .");
|
||||
|
||||
// CameraManager 内部会:StopAsync -> DisposeAsync -> TryRemove -> SaveChanges
|
||||
await _cameraManager.RemoveDeviceAsync(deviceId);
|
||||
|
||||
Console.WriteLine($"[{ActionName}] 设备 {deviceId} 已彻底清理并从持久化库中移除。");
|
||||
_sysLog.Information($"[Sync] 收到远程指令, 设备, ID:{deviceId} Name:{device.Config.Name}已彻底清理并从持久化库中移除 .");
|
||||
|
||||
// 4. (可选) 此处可以调用 CommandDispatcher 发送 Success ACK
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 捕获异常,防止影响全局 Socket 轮询
|
||||
Console.WriteLine($"[{ActionName}] 移除设备 {deviceId} 过程中发生致命错误: {ex.Message}");
|
||||
_sysLog.Error($"[Sync] 移除设备, ID:{deviceId} 过程中发生致命错误, {ex.Message}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Ayay.SerilogLogs;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using OpenCvSharp;
|
||||
using Serilog;
|
||||
using SHH.CameraSdk; // 引用 SDK 核心
|
||||
using SHH.Contracts;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SHH.CameraService;
|
||||
|
||||
/// <summary>
|
||||
/// 图像监控采集控制器 (流媒体分发引擎)
|
||||
/// <para>功能:监听全局图像采集总线,对图像进行实时 JPG 编码,并动态分发至云端、大屏等订阅目标。</para>
|
||||
/// <para>设计模式:发布-订阅模式 + 扇出 (Fan-out) 分发。</para>
|
||||
/// </summary>
|
||||
public class ImageMonitorController : BackgroundService
|
||||
{
|
||||
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
||||
|
||||
// 注入所有注册的目标(云端、大屏等),实现动态分发
|
||||
private readonly IEnumerable<StreamTarget> _targets;
|
||||
|
||||
@@ -16,29 +25,35 @@ public class ImageMonitorController : BackgroundService
|
||||
// 如果您确实需要 100,请注意带宽压力。此处我保留您要求的 100,但建议未来调优。
|
||||
private readonly int[] _encodeParams = { (int)ImwriteFlags.JpegQuality, 100 };
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="targets"></param>
|
||||
public ImageMonitorController(IEnumerable<StreamTarget> targets)
|
||||
{
|
||||
_targets = targets;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 启动后台服务:挂载事件总线
|
||||
/// </summary>
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
Console.WriteLine("[StreamWorker] 启动流媒体采集引擎...");
|
||||
_sysLog.Information("[Core] 启动流媒体采集引擎...");
|
||||
|
||||
// =========================================================
|
||||
// 订阅逻辑:接入 "上帝模式" (God Mode)
|
||||
// =========================================================
|
||||
// 理由:NetMQ 网关需要无差别地获取所有设备的图像。
|
||||
// 理由:gRpc 需要无差别地获取所有设备的图像。
|
||||
GlobalStreamDispatcher.OnGlobalFrame += ProcessFrame;
|
||||
|
||||
//Console.WriteLine($"[StreamWorker] 已挂载至全局广播总线,正在监听 {GlobalStreamDispatcher.OnGlobalFrame?.GetInvocationList().Length ?? 0} 个订阅者...");
|
||||
_sysLog.Information($"[StreamWorker] 已挂载至全局广播总线,正在监听帧信息.");
|
||||
|
||||
var tcs = new TaskCompletionSource();
|
||||
stoppingToken.Register(() =>
|
||||
{
|
||||
// 停止时反注册,防止静态事件内存泄漏
|
||||
GlobalStreamDispatcher.OnGlobalFrame -= ProcessFrame;
|
||||
Console.WriteLine("[StreamWorker] 已断开全局广播连接");
|
||||
_sysLog.Information("[Core] 流媒体采集引擎已断开全局广播连接.");
|
||||
tcs.SetResult();
|
||||
});
|
||||
|
||||
@@ -46,9 +61,11 @@ public class ImageMonitorController : BackgroundService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [回调函数] 处理每一帧图像
|
||||
/// 注意:此方法运行在 SDK 的采集线程池中,必须极速处理,严禁阻塞!
|
||||
/// [回调函数] 处理实时帧
|
||||
/// <para>注意:此方法由 SDK 采集线程池触发,必须保持极速处理,严禁在内部执行 IO 等耗时阻塞操作。</para>
|
||||
/// </summary>
|
||||
/// <param name="deviceId">设备唯一标识 ID</param>
|
||||
/// <param name="frame">包含原始图像(InternalMat)和处理后图像(TargetMat)的帧数据</param>
|
||||
private void ProcessFrame(long deviceId, SmartFrame frame)
|
||||
{
|
||||
try
|
||||
@@ -110,20 +127,22 @@ public class ImageMonitorController : BackgroundService
|
||||
if (!ok)
|
||||
{
|
||||
// 如果这里打印,说明管道由于某种原因被关闭了(通常是程序正在退出)
|
||||
Console.WriteLine($"[DEBUG] 管道写入失败,目标: {target.Config.Name}");
|
||||
_sysLog.Warning($"[ImageMonitor] 管道写入失败,目标: {target.Config.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 极少发生的内存错误,打印日志但不抛出,避免崩溃 SDK 线程
|
||||
Console.WriteLine($"[StreamWorker] 采集处理异常: {ex.Message}");
|
||||
_sysLog.Error($"[ImageMonitor] 采集处理异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 辅助:OpenCV 内存编码
|
||||
/// 调用 OpenCV 进行内存级图片编码
|
||||
/// </summary>
|
||||
/// <param name="mat">待编码的 OpenCV Mat 矩阵</param>
|
||||
/// <returns>JPG 字节数组</returns>
|
||||
private byte[] EncodeImage(Mat mat)
|
||||
{
|
||||
// ImEncode 将 Mat 编码为一维字节数组 (托管内存)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Ayay.SerilogLogs;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Serilog;
|
||||
using SHH.CameraSdk;
|
||||
|
||||
namespace SHH.CameraService;
|
||||
@@ -65,8 +67,9 @@ public class PipelineConfigurator : IHostedService
|
||||
GlobalPipelineRouter.SetProcessor(_scale);
|
||||
|
||||
// 启动日志:打印管道组装结果,便于运维排查
|
||||
Console.WriteLine("[Pipeline] 图像处理链组装完成: ImageScaleCluster -> ImageEnhanceCluster");
|
||||
Console.WriteLine("[Pipeline] 提示:帧数据将按 '缩放 → 增强' 顺序处理,可通过 GlobalPipelineRouter 调整流程");
|
||||
Log.ForContext("SourceContext", LogModules.Core)
|
||||
.Information(@"[Pipeline] 图像处理链组装完成: ImageScaleCluster -> ImageEnhanceCluster
|
||||
提示:帧数据将按 '缩放 → 增强' 顺序处理,可通过 GlobalPipelineRouter 调整流程");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
using Google.Protobuf;
|
||||
using Ayay.SerilogLogs;
|
||||
using Google.Protobuf;
|
||||
using Grpc.Net.Client;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using SHH.Contracts.Grpc;
|
||||
|
||||
namespace SHH.CameraService;
|
||||
|
||||
/// <summary>
|
||||
/// gRPC 视频流发送工作者
|
||||
/// 职责:监听特定的 StreamTarget 队列,建立 gRPC 客户端流并持续推送图片
|
||||
/// gRpc 视频流发送工作者
|
||||
/// 职责:监听特定的 StreamTarget 队列,建立 gRpc 客户端流并持续推送图片
|
||||
/// </summary>
|
||||
public class GrpcSenderWorker : BackgroundService
|
||||
{
|
||||
private readonly StreamTarget _target;
|
||||
private readonly ILogger<GrpcSenderWorker> _logger;
|
||||
private static ILogger _gRpcLog = Log.ForContext("SourceContext", LogModules.gRpc);
|
||||
|
||||
private readonly StreamTarget _target;
|
||||
private readonly string _grpcUrl;
|
||||
|
||||
public GrpcSenderWorker(StreamTarget target, ILogger<GrpcSenderWorker> logger)
|
||||
public GrpcSenderWorker(StreamTarget target)
|
||||
{
|
||||
_target = target;
|
||||
_logger = logger;
|
||||
|
||||
// 自动适配地址:将配置的 tcp://localhost:9001 转换为 http://localhost:9001
|
||||
// 并且严格使用你验证成功的 localhost
|
||||
@@ -28,7 +29,7 @@ public class GrpcSenderWorker : BackgroundService
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation($"[gRPC Worker] 启动。目标: {_target.Config.Name}, 地址: {_grpcUrl}");
|
||||
_gRpcLog.Information($"[gRpc] 视频流发送业务启动, 目标: {_target.Config.Name}, 地址: {_grpcUrl}");
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -41,10 +42,10 @@ public class GrpcSenderWorker : BackgroundService
|
||||
// 2. 开启客户端流 (UploadVideoStream 是在 proto 中定义的)
|
||||
using var call = client.UploadVideoStream(cancellationToken: stoppingToken);
|
||||
|
||||
_logger.LogInformation($"[gRPC Worker] 已开启视频推送流: {_target.Config.Name}");
|
||||
_gRpcLog.Information($"[gRpc] 已开启视频推送流, 目标: {_target.Config.Name}, 地址: {_grpcUrl}");
|
||||
|
||||
// 3. 核心搬运循环:从内存队列 (Channel) 读取数据
|
||||
await foreach (var payload in _target.Channel.Reader.ReadAllAsync(stoppingToken))
|
||||
// 3. 核心搬运循环:从内存队列 (Channel) 读取数据
|
||||
await foreach (var payload in _target.Channel.Reader.ReadAllAsync(stoppingToken))
|
||||
{
|
||||
// 【畅通保障】检查数据时效性:丢弃超过 1 秒的积压帧
|
||||
var delay = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - payload.CaptureTimestamp;
|
||||
@@ -53,7 +54,7 @@ public class GrpcSenderWorker : BackgroundService
|
||||
continue;
|
||||
}
|
||||
|
||||
// 将业务 DTO 转换为 gRPC 原生 Request
|
||||
// 将业务 DTO 转换为 gRpc 原生 Request
|
||||
var request = new VideoFrameRequest
|
||||
{
|
||||
CameraId = payload.CameraId ?? "0",
|
||||
@@ -63,7 +64,7 @@ public class GrpcSenderWorker : BackgroundService
|
||||
HasOriginalImage = payload.HasOriginalImage,
|
||||
HasTargetImage = payload.HasTargetImage,
|
||||
|
||||
// ★ 核心:将 byte[] 转换为 gRPC 的 ByteString (高性能)
|
||||
// ★ 核心:将 byte[] 转换为 gRpc 的 ByteString (高性能)
|
||||
OriginalImageBytes = payload.OriginalImageBytes != null
|
||||
? ByteString.CopyFrom(payload.OriginalImageBytes)
|
||||
: ByteString.Empty,
|
||||
@@ -94,9 +95,9 @@ public class GrpcSenderWorker : BackgroundService
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"[gRPC Worker] 推送链路异常,5秒后重连: {ex.Message}");
|
||||
_gRpcLog.Warning($"[gRpc] 视频推送流链路异常, 目标: {_target.Config.Name}, 地址: {_grpcUrl}, 5秒后重连: {ex.Message}.");
|
||||
await Task.Delay(5000, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,13 +25,13 @@ public class Program
|
||||
// =============================================================
|
||||
// 1. 启动日志
|
||||
// =============================================================
|
||||
sysLog.Warning($"🚀 视频取流进程启动, 日志组件初始化完毕 => 进程: {opts.AppId}");
|
||||
sysLog.Warning($"[Core] 🚀 视频取流进程启动, 日志组件初始化完毕 => 进程: {opts.AppId}");
|
||||
|
||||
string argString = string.Join(" ", args);
|
||||
sysLog.Debug($"🚀 启动参数({(isDebugArgs ? "调试环境" : "生产环境")}): {argString}");
|
||||
sysLog.Debug($"[Core] 🚀 启动参数({(isDebugArgs ? "调试环境" : "生产环境")}): {argString}");
|
||||
|
||||
// =============================================================
|
||||
// 2. 硬件预热、端口扫描、gRPC链接
|
||||
// 2. 硬件预热、端口扫描、gRpc链接
|
||||
// =============================================================
|
||||
Bootstrapper.WarmUpHardware(sysLog);
|
||||
|
||||
@@ -39,13 +39,13 @@ public class Program
|
||||
int activePort = Bootstrapper.ScanForAvailablePort(config, sysLog);
|
||||
if (activePort == -1)
|
||||
{
|
||||
sysLog.Fatal("💀 无法启动:配置范围内无可用端口");
|
||||
sysLog.Fatal("[Core] 💀 无法启动:配置范围内无可用端口");
|
||||
Bootstrapper.Shutdown("无法启动:配置范围内无可用端口", exitCode: 1);
|
||||
return;
|
||||
}
|
||||
config.UpdateActualPort(activePort); // 回填端口
|
||||
|
||||
// 具体的 gRPC 链接逻辑封装在 Bootstrapper 中,保持 Main 清爽但逻辑可见
|
||||
// 具体的 gRpc 链接逻辑封装在 Bootstrapper 中,保持 Main 清爽但逻辑可见
|
||||
await Bootstrapper.RegisterToGatewayAsync(config);
|
||||
|
||||
// =============================================================
|
||||
@@ -53,7 +53,7 @@ public class Program
|
||||
// =============================================================
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// ★ 核心改动:一行代码注册所有业务 (SDK, Workers, gRPC, 视频流)
|
||||
// ★ 核心改动:一行代码注册所有业务 (SDK, Workers, gRpc, 视频流)
|
||||
builder.Services.AddCameraBusinessServices(config, sysLog);
|
||||
|
||||
// ★ 核心改动:注册 Web 基础 (Controller, Swagger, Cors)
|
||||
@@ -72,7 +72,7 @@ public class Program
|
||||
|
||||
// 启动监听
|
||||
string url = $"http://0.0.0.0:{config.BasePort}";
|
||||
sysLog.Information($"🚀 [WebApi] 服务启动,监听: {url}");
|
||||
sysLog.Information($"[WebApi] 🚀 服务启动,监听: {url}");
|
||||
|
||||
await app.RunAsync(url);
|
||||
}
|
||||
@@ -90,7 +90,9 @@ public class Program
|
||||
_ = app.Services.GetRequiredService<ConnectivitySentinel>();
|
||||
|
||||
await manager.StartAsync();
|
||||
Console.WriteLine("✅[System] 核心业务逻辑已激活。");
|
||||
|
||||
var sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
||||
sysLog.Information($"[Core] 🚀 核心业务逻辑已激活, 设备管理器已就绪.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging; // 用于泛型 ILogger<>
|
||||
using Microsoft.OpenApi.Models;
|
||||
using SHH.CameraSdk;
|
||||
|
||||
@@ -84,9 +83,12 @@ public static class ServiceCollectionExtensions
|
||||
}
|
||||
}
|
||||
|
||||
logger.Information("📋 加载视频流目标: {Count} 个", netTargets.Count);
|
||||
logger.Information("[Core] 📋 加载视频流目标: {Count} 个", netTargets.Count);
|
||||
if (netTargets.Count > 0)
|
||||
logger.Debug("🔍 视频流目标详情: {@Targets}", netTargets);
|
||||
{
|
||||
foreach (var item in netTargets)
|
||||
logger.Debug("[Core] 🔍 视频流目标详情: {@Targets}", new { item.Config });
|
||||
}
|
||||
|
||||
services.AddSingleton<IEnumerable<StreamTarget>>(netTargets);
|
||||
services.AddHostedService<ImageMonitorController>();
|
||||
@@ -96,7 +98,7 @@ public static class ServiceCollectionExtensions
|
||||
{
|
||||
// 注意:这里需要使用 Microsoft.Extensions.Logging.ILogger 来适配构造函数
|
||||
services.AddHostedService(sp =>
|
||||
new GrpcSenderWorker(target, sp.GetRequiredService<ILogger<GrpcSenderWorker>>()));
|
||||
new GrpcSenderWorker(target));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user