规范并补充日志内容

This commit is contained in:
2026-01-16 14:30:42 +08:00
parent 4e0bb33ce2
commit fd6a82eb4e
28 changed files with 325 additions and 537 deletions

View File

@@ -83,7 +83,7 @@ namespace Ayay.SerilogLogs
// 定义通用模板:包含了 SourceContext (模块名), TraceId, AppId // 定义通用模板:包含了 SourceContext (模块名), TraceId, AppId
// 示例: 2026-01-15 12:00:01 [INF] [Algorithm] [Dev01] 计算完成 // 示例: 2026-01-15 12:00:01 [INF] [Algorithm] [Dev01] 计算完成
string outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] [{AppId}] {Message:lj}{NewLine}{Exception}"; string outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}";
// 3.1 控制台输出 (开发调试用) // 3.1 控制台输出 (开发调试用)
builder.WriteTo.Async(a => a.Console( builder.WriteTo.Async(a => a.Console(
@@ -167,6 +167,9 @@ namespace Ayay.SerilogLogs
// -------------------------------------------------------- // --------------------------------------------------------
Log.Logger = builder.CreateLogger(); Log.Logger = builder.CreateLogger();
// 支持 Emoji 显示
Console.OutputEncoding = Encoding.UTF8;
// -------------------------------------------------------- // --------------------------------------------------------
// 5. 启动后台清理任务 (LogCleaner) // 5. 启动后台清理任务 (LogCleaner)
// -------------------------------------------------------- // --------------------------------------------------------

View File

@@ -10,7 +10,7 @@
public static string Core { get; } = "Core"; // 系统主逻辑/启动关闭 public static string Core { get; } = "Core"; // 系统主逻辑/启动关闭
public static string Network { get; } = "Network"; // 底层网络通讯 (TCP/UDP) public static string Network { get; } = "Network"; // 底层网络通讯 (TCP/UDP)
public static string WebApi { get; } = "WebAPI"; // 对外 HTTP 接口 public static string WebApi { get; } = "WebAPI"; // 对外 HTTP 接口
public static string gRpc { get; } = "gRPC"; // 对外 gRPC 接口 public static string gRpc { get; } = "gRpc"; // 对外 gRpc 接口
public static string WebSocket { get; } = "WebSocket"; // 实时通讯 public static string WebSocket { get; } = "WebSocket"; // 实时通讯
public static string Ping { get; } = "Ping"; // 心跳/Ping包 (通常量大且不重要) public static string Ping { get; } = "Ping"; // 心跳/Ping包 (通常量大且不重要)

View File

@@ -1,28 +0,0 @@
namespace SHH.CameraSdk;
public interface IStorageService
{
// 1. 基础属性
int ProcessId { get; }
// 2. 设备配置管理
// 保存:接收 VideoSourceConfig 集合
Task SaveDevicesAsync(IEnumerable<VideoSourceConfig> configs);
// 加载:返回 VideoSourceConfig 列表
Task<List<VideoSourceConfig>> LoadDevicesAsync();
// 3. 系统日志
// 记录系统操作 (如 POST /api/cameras)
Task AppendSystemLogAsync(string action, string ip, string path);
// 获取系统日志
Task<List<string>> GetSystemLogsAsync(int count);
// 4. 设备审计日志
// 记录单设备日志 (统一使用 int deviceId)
Task AppendDeviceLogAsync(int deviceId, string message);
// 获取单设备日志
Task<List<string>> GetDeviceLogsAsync(int deviceId, int count);
}

View File

@@ -14,16 +14,14 @@ public class MonitorController : ControllerBase
#region --- (Dependency Injection) --- #region --- (Dependency Injection) ---
private readonly CameraManager _cameraManager; private readonly CameraManager _cameraManager;
private readonly IStorageService _storage; // [新增] 存储服务引用
private readonly ProcessingConfigManager _configManager; private readonly ProcessingConfigManager _configManager;
/// <summary> /// <summary>
/// 构造函数:注入 CameraManager 和 IStorageService /// 构造函数:注入 CameraManager 和 IStorageService
/// </summary> /// </summary>
public MonitorController(CameraManager cameraManager, IStorageService storage, ProcessingConfigManager configManager) public MonitorController(CameraManager cameraManager, ProcessingConfigManager configManager)
{ {
_cameraManager = cameraManager; _cameraManager = cameraManager;
_storage = storage;
_configManager = configManager; _configManager = configManager;
} }
@@ -131,7 +129,6 @@ public class MonitorController : ControllerBase
// [修正] 改为从 StorageService 读取文件日志 // [修正] 改为从 StorageService 读取文件日志
// 这样即使重启程序,历史日志也能查到 // 这样即使重启程序,历史日志也能查到
var logs = await _storage.GetDeviceLogsAsync((int)id, 50);
return Ok(new return Ok(new
{ {
@@ -151,27 +148,11 @@ public class MonitorController : ControllerBase
device.Width, device.Width,
device.Height device.Height
}, },
// [关键] 持久化日志
AuditLogs = logs
}); });
} }
#endregion #endregion
/// <summary>
/// 获取系统操作日志(读取最新的 50 条)
/// </summary>
[HttpGet("system-logs")]
public async Task<IActionResult> GetSystemLogs()
{
// [修正] 彻底废弃手动读文件,改用 Service
// Service 内部会自动处理锁、路径 (App_Data/Process_X/system.log) 和异常
var logs = await _storage.GetSystemLogsAsync(50);
return Ok(logs);
}
[HttpPost("update-processing")] [HttpPost("update-processing")]
public IActionResult UpdateProcessing([FromBody] UpdateProcessingRequest request) public IActionResult UpdateProcessing([FromBody] UpdateProcessingRequest request)
{ {

View File

@@ -1,4 +1,6 @@
using OpenCvSharp; using Ayay.SerilogLogs;
using OpenCvSharp;
using Serilog;
namespace SHH.CameraSdk; namespace SHH.CameraSdk;
@@ -13,6 +15,8 @@ public class FrameConsumer : IDisposable
{ {
#region --- (Private Resources & States) --- #region --- (Private Resources & States) ---
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
/// <summary> 帧缓冲队列容量1仅存储最新一帧保证零延迟渲染 </summary> /// <summary> 帧缓冲队列容量1仅存储最新一帧保证零延迟渲染 </summary>
/// <remarks> BlockingCollection 封装线程安全操作GetConsumingEnumerable 支持取消令牌 </remarks> /// <remarks> BlockingCollection 封装线程安全操作GetConsumingEnumerable 支持取消令牌 </remarks>
private readonly BlockingCollection<SmartFrame> _frameBuffer = new(1); private readonly BlockingCollection<SmartFrame> _frameBuffer = new(1);
@@ -49,7 +53,7 @@ public class FrameConsumer : IDisposable
// 启动长期运行的渲染任务,提升线程调度优先级 // 启动长期运行的渲染任务,提升线程调度优先级
_renderTask = Task.Factory.StartNew(RenderLoop, TaskCreationOptions.LongRunning); _renderTask = Task.Factory.StartNew(RenderLoop, TaskCreationOptions.LongRunning);
Console.WriteLine($"[Consumer] 渲染线程启动成功,窗口名称: {_windowName}"); _sysLog.Information($"[Consumer] 渲染线程启动成功,窗口名称: {_windowName}");
} }
/// <summary> /// <summary>
@@ -75,7 +79,7 @@ public class FrameConsumer : IDisposable
residualFrame.Dispose(); residualFrame.Dispose();
} }
Console.WriteLine($"[Consumer] 渲染线程已停止,窗口: {_windowName}"); _sysLog.Information($"[Consumer] 渲染线程已停止,窗口: {_windowName}");
} }
#endregion #endregion

View File

@@ -1,4 +1,7 @@
namespace SHH.CameraSdk; using Ayay.SerilogLogs;
using Serilog;
namespace SHH.CameraSdk;
/// <summary> /// <summary>
/// [管理层] 视频源总控管理器 (V3.5 持久化集成版) /// [管理层] 视频源总控管理器 (V3.5 持久化集成版)
@@ -8,6 +11,8 @@ public class CameraManager : IDisposable, IAsyncDisposable
{ {
#region --- 1. (Fields & States) --- #region --- 1. (Fields & States) ---
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
/// <summary> 全局设备实例池线程安全Key = 设备唯一标识 </summary> /// <summary> 全局设备实例池线程安全Key = 设备唯一标识 </summary>
private readonly ConcurrentDictionary<long, BaseVideoSource> _cameraPool = new(); private readonly ConcurrentDictionary<long, BaseVideoSource> _cameraPool = new();
@@ -26,17 +31,13 @@ public class CameraManager : IDisposable, IAsyncDisposable
/// </summary> /// </summary>
private volatile bool _isEngineStarted = false; private volatile bool _isEngineStarted = false;
// [新增] 存储服务引用
private readonly IStorageService _storage;
#endregion #endregion
#region --- (Constructor) --- #region --- (Constructor) ---
// [修改] 注入 IStorageService // [修改] 注入 IStorageService
public CameraManager(IStorageService storage) public CameraManager()
{ {
_storage = storage;
} }
#endregion #endregion
@@ -59,19 +60,17 @@ public class CameraManager : IDisposable, IAsyncDisposable
{ {
// 如果添加失败ID冲突由于 device 还没被使用,直接释放掉 // 如果添加失败ID冲突由于 device 还没被使用,直接释放掉
device.DisposeAsync().AsTask().Wait(); device.DisposeAsync().AsTask().Wait();
_sysLog.Warning($"[Core] 设备 ID:{config.Id} 已存在");
_sysLog.Debug($"[Core] 设备 ID:{config.Id} => 明细:" + "{@cfg}.", config);
throw new InvalidOperationException($"设备 ID {config.Id} 已存在"); throw new InvalidOperationException($"设备 ID {config.Id} 已存在");
} }
// 动态激活逻辑:引擎已启动时,新设备直接标记为运行状态 // 动态激活逻辑:引擎已启动时,新设备直接标记为运行状态
if (_isEngineStarted) if (_isEngineStarted)
{
device.IsRunning = true; device.IsRunning = true;
} }
// [新增] 自动保存到文件
SaveChanges();
}
/// <summary> /// <summary>
/// 根据设备ID获取指定的视频源实例 /// 根据设备ID获取指定的视频源实例
/// </summary> /// </summary>
@@ -94,7 +93,7 @@ public class CameraManager : IDisposable, IAsyncDisposable
if (_cameraPool.TryRemove(id, out var device)) if (_cameraPool.TryRemove(id, out var device))
{ {
// 记录日志 // 记录日志
Console.WriteLine($"[Manager] 正在移除设备 {id}..."); _sysLog.Information("[Core] 正在移除设备, ID {0} ", id);
// 1. 停止物理连接 // 1. 停止物理连接
await device.StopAsync(); await device.StopAsync();
@@ -102,10 +101,7 @@ public class CameraManager : IDisposable, IAsyncDisposable
// 2. 释放资源 // 2. 释放资源
await device.DisposeAsync(); await device.DisposeAsync();
Console.WriteLine($"[Manager] 设备 {id} 已彻底移除"); _sysLog.Warning("[Core] 设备已彻底移除, ID {0} ", id);
// [新增] 自动保存到文件
SaveChanges();
} }
} }
@@ -125,36 +121,7 @@ public class CameraManager : IDisposable, IAsyncDisposable
if (_isEngineStarted) return; if (_isEngineStarted) return;
// ========================================================= // =========================================================
// 1. [新增] 从文件加载设备配置 // 1. 全局驱动环境预初始化
// =========================================================
try
{
Console.WriteLine("[Manager] 正在检查本地配置文件...");
var savedConfigs = await _storage.LoadDevicesAsync();
int loadedCount = 0;
foreach (var config in savedConfigs)
{
// 防止ID冲突虽然文件里理论上不重复
if (!_cameraPool.ContainsKey(config.Id))
{
var device = CreateDeviceInstance(config);
// 默认设为运行状态,让协调器稍后去连接
//device.IsRunning = true;
_cameraPool.TryAdd(config.Id, device);
loadedCount++;
}
}
if (loadedCount > 0)
Console.WriteLine($"[Manager] 已从文件恢复 {loadedCount} 台设备配置");
}
catch (Exception ex)
{
Console.WriteLine($"[Manager] 加载配置文件警告: {ex.Message}");
}
// =========================================================
// 2. 全局驱动环境预初始化
// ========================================================= // =========================================================
HikSdkManager.Initialize(); HikSdkManager.Initialize();
@@ -162,7 +129,7 @@ public class CameraManager : IDisposable, IAsyncDisposable
_isEngineStarted = true; _isEngineStarted = true;
// ========================================================= // =========================================================
// 3. 启动协调器后台自愈循环 // 2. 启动协调器后台自愈循环
// ========================================================= // =========================================================
_ = Task.Factory.StartNew( _ = Task.Factory.StartNew(
() => _coordinator.RunCoordinationLoopAsync(_globalCts.Token), () => _coordinator.RunCoordinationLoopAsync(_globalCts.Token),
@@ -175,7 +142,7 @@ public class CameraManager : IDisposable, IAsyncDisposable
// *注意*:如果 Coordinator 需要显式注册,请在这里补上: // *注意*:如果 Coordinator 需要显式注册,请在这里补上:
foreach (var dev in _cameraPool.Values) _coordinator.Register(dev); foreach (var dev in _cameraPool.Values) _coordinator.Register(dev);
Console.WriteLine($"[CameraManager] 引擎启动成功当前管理 {_cameraPool.Count} 路相机设备"); _sysLog.Warning($"[Core] 设备管理引擎启动成功, 当前管理 {_cameraPool.Count} 路设备");
await Task.CompletedTask; await Task.CompletedTask;
} }
@@ -191,20 +158,6 @@ public class CameraManager : IDisposable, IAsyncDisposable
#region --- 4. (Telemetry Collection) --- #region --- 4. (Telemetry Collection) ---
/// <summary>
/// 获取所有相机的健康度报告
/// </summary>
public IEnumerable<CameraHealthReport> GetDetailedTelemetry()
{
return _cameraPool.Values.Select(cam => new CameraHealthReport
{
DeviceId = cam.Id,
Ip = cam.Config.IpAddress,
Status = cam.Status.ToString(),
LastError = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : "运行正常"
});
}
/// <summary> /// <summary>
/// 获取全量相机实时遥测数据快照 (MonitorController 使用) /// 获取全量相机实时遥测数据快照 (MonitorController 使用)
/// </summary> /// </summary>
@@ -250,10 +203,13 @@ public class CameraManager : IDisposable, IAsyncDisposable
public async Task UpdateDeviceConfigAsync(long deviceId, DeviceUpdateDto dto) public async Task UpdateDeviceConfigAsync(long deviceId, DeviceUpdateDto dto)
{ {
if (!_cameraPool.TryGetValue(deviceId, out var device)) if (!_cameraPool.TryGetValue(deviceId, out var device))
throw new KeyNotFoundException($"设备 {deviceId} 不存在"); {
_sysLog.Warning($"[Core] 设备更新制作, ID:{deviceId} 不存在.");
throw new KeyNotFoundException($"设备 ID:{deviceId} 不存在.");
}
// 1. 审计 // 1. 审计
device.AddAuditLog("收到配置更新请求"); _sysLog.Debug($"[Core] 响应设备配置更新请求, ID:{deviceId}.");
// 2. 创建副本进行对比 // 2. 创建副本进行对比
var oldConfig = device.Config; var oldConfig = device.Config;
@@ -287,7 +243,7 @@ public class CameraManager : IDisposable, IAsyncDisposable
if (needColdRestart) if (needColdRestart)
{ {
device.AddAuditLog($"检测到核心参数变更执行冷重启 (Reboot)"); _sysLog.Debug($"[Core] 检测到核心参数变更, 执行冷重启, ID:{deviceId}.");
bool wasRunning = device.IsRunning; bool wasRunning = device.IsRunning;
// A. 彻底停止 // A. 彻底停止
@@ -301,7 +257,7 @@ public class CameraManager : IDisposable, IAsyncDisposable
} }
else else
{ {
device.AddAuditLog($"检测到运行时参数变更执行热更新 (HotSwap)"); _sysLog.Debug($"[Core] 检测到运行时参数变更, 执行热更新, ID:{deviceId}.");
// A. 更新配置数据 // A. 更新配置数据
device.UpdateConfig(newConfig); device.UpdateConfig(newConfig);
@@ -317,47 +273,6 @@ public class CameraManager : IDisposable, IAsyncDisposable
device.ApplyOptions(options); device.ApplyOptions(options);
} }
} }
// [新增] 保存文件
SaveChanges();
}
/// <summary>
/// 全量替换更新 (兼容接口)
/// </summary>
public async Task UpdateDeviceAsync(int id, VideoSourceConfig newConfig)
{
if (!_cameraPool.TryGetValue(id, out var oldDevice))
throw new KeyNotFoundException($"设备 #{id} 不存在");
bool wasRunning = oldDevice.IsRunning ||
oldDevice.Status == VideoSourceStatus.Playing ||
oldDevice.Status == VideoSourceStatus.Connecting;
Console.WriteLine($"[Manager] 正在更新设备 #{id},配置变更中...");
try
{
await oldDevice.StopAsync();
await oldDevice.DisposeAsync();
}
catch (Exception ex)
{
Console.WriteLine($"[Manager] 销毁旧设备时警告: {ex.Message}");
}
var newDevice = CreateDeviceInstance(newConfig);
_cameraPool[id] = newDevice;
Console.WriteLine($"[Manager] 设备 #{id} 实例已重建。");
if (wasRunning)
{
await newDevice.StartAsync();
}
// [新增] 保存文件
SaveChanges();
} }
#endregion #endregion
@@ -366,6 +281,10 @@ public class CameraManager : IDisposable, IAsyncDisposable
public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult(); public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult();
/// <summary>
/// 释放资源
/// </summary>
/// <returns></returns>
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
if (_isDisposed) return; if (_isDisposed) return;
@@ -402,31 +321,38 @@ public class CameraManager : IDisposable, IAsyncDisposable
#region --- 7. (Helpers) --- #region --- 7. (Helpers) ---
/// <summary>
/// 创建设备实例
/// </summary>
/// <param name="config"></param>
/// <returns></returns>
private BaseVideoSource CreateDeviceInstance(VideoSourceConfig config) private BaseVideoSource CreateDeviceInstance(VideoSourceConfig config)
{ {
return config.Brand switch return config.Brand switch
{ {
DeviceBrand.HikVision => new HikVideoSource(config), DeviceBrand.HikVision => new HikVideoSource(config),
_ => throw new NotSupportedException($"不支持的设备品牌: {config.Brand}")
// 使用模式匹配获取不匹配的值,记录详细的 DTO 上下文
_ => HandleUnsupportedBrand(config)
}; };
} }
/// <summary> /// <summary>
/// [新增] 触发异步保存 (Fire-and-Forget) /// 处理不支持的设备品牌
/// 不阻塞当前 API 线程,让后台存储服务去排队写入
/// </summary> /// </summary>
private void SaveChanges() /// <param name="config"></param>
/// <returns></returns>
/// <exception cref="NotSupportedException"></exception>
private BaseVideoSource HandleUnsupportedBrand(VideoSourceConfig config)
{ {
try // 1. 构造错误消息
{ string errorMsg = $"❌ 不支持的设备品牌: {config.Brand} (ID: {config.Id}, Name: {config.Name})";
var allConfigs = _cameraPool.Values.Select(d => d.Config).ToList();
// 异步调用存储服务,不使用 await 以免阻塞 API 响应 // 2. 写入日志 - 建议带上 config 的解构信息,方便排查是否是前端传参错误
_ = _storage.SaveDevicesAsync(allConfigs); _sysLog.Error($"[Core] {errorMsg} | 配置详情: " + "{@Config}", config);
}
catch (Exception ex) // 3. 抛出异常,阻止程序进入不确定状态
{ throw new NotSupportedException(errorMsg);
Console.WriteLine($"[Manager] 触发保存失败: {ex.Message}");
}
} }
#endregion #endregion

View File

@@ -1,4 +1,7 @@
namespace SHH.CameraSdk; using Ayay.SerilogLogs;
using Serilog;
namespace SHH.CameraSdk;
/// <summary> /// <summary>
/// 全局流分发器(静态类 | 线程安全) /// 全局流分发器(静态类 | 线程安全)
@@ -14,6 +17,8 @@ public static class GlobalStreamDispatcher
{ {
#region --- 1. (Predefined Subscription Channels) --- #region --- 1. (Predefined Subscription Channels) ---
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
/// <summary> /// <summary>
/// UI 预览订阅通道:供 UI 模块订阅帧数据,用于实时画面显示 /// UI 预览订阅通道:供 UI 模块订阅帧数据,用于实时画面显示
/// 回调参数:(设备唯一标识, 处理后的智能帧数据) /// 回调参数:(设备唯一标识, 处理后的智能帧数据)
@@ -50,7 +55,7 @@ public static class GlobalStreamDispatcher
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[GlobalBus Error] 广播异常: {ex.Message}"); _sysLog.Error($"[GlobalBus] 广播分发异常: {ex.Message}");
} }
// B. 执行你原有的定向分发逻辑 (给处理链用) // B. 执行你原有的定向分发逻辑 (给处理链用)
@@ -151,7 +156,7 @@ public static class GlobalStreamDispatcher
if (deviceMap.TryRemove(specificDeviceId, out _)) if (deviceMap.TryRemove(specificDeviceId, out _))
{ {
// 可选:如果该 AppId 下没设备了,是否清理外层字典?(为了性能通常不清理,或者定期清理) // 可选:如果该 AppId 下没设备了,是否清理外层字典?(为了性能通常不清理,或者定期清理)
// Console.WriteLine($"[Dispatcher] {appId} 已停止订阅设备 {specificDeviceId}"); _sysLog.Information($"[Dispatcher] {appId} 已停止订阅设备 ID:{specificDeviceId }");
} }
} }
} }
@@ -223,7 +228,7 @@ public static class GlobalStreamDispatcher
{ {
// 单个订阅者异常隔离,不影响其他分发流程 // 单个订阅者异常隔离,不影响其他分发流程
task.Context.AddLog($"帧任务 [Seq:{sequence}] 投递到 AppId:{appId} 失败:{ex.Message}"); task.Context.AddLog($"帧任务 [Seq:{sequence}] 投递到 AppId:{appId} 失败:{ex.Message}");
Console.WriteLine($"[DispatchError] AppId={appId}, DeviceId={deviceId}, Error={ex.Message}"); _sysLog.Error($"[Dispatch] AppId={appId}, DeviceId={deviceId}, Error={ex.Message}");
} }
} }
@@ -242,7 +247,7 @@ public static class GlobalStreamDispatcher
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[DispatchError] DeviceSpecific AppId={appId}, Dev={deviceId}: {ex.Message}"); _sysLog.Error($"[Dispatch] DeviceSpecific AppId={appId}, Dev={deviceId}: {ex.Message}");
} }
} }
} }
@@ -285,7 +290,7 @@ public static class GlobalStreamDispatcher
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[SidecarDispatchError] App={sidecarAppId}, Dev={deviceId}: {ex.Message}"); _sysLog.Error($"[Dispatch] App={sidecarAppId}, Dev={deviceId}: {ex.Message}");
} }
} }
} }
@@ -317,7 +322,7 @@ public static class GlobalStreamDispatcher
// TryRemove 是原子的、线程安全的 // TryRemove 是原子的、线程安全的
if (_routingTable.TryRemove(appId, out _)) if (_routingTable.TryRemove(appId, out _))
{ {
Console.WriteLine($"[Dispatcher] 已强制移除 AppId [{appId}] 的所有订阅路由"); _sysLog.Error($"[Dispatcher] 已强制移除 AppId [{appId}] 的所有订阅路由.");
} }
} }

View File

@@ -1,4 +1,7 @@
namespace SHH.CameraSdk; using Ayay.SerilogLogs;
using Serilog;
namespace SHH.CameraSdk;
/// <summary> /// <summary>
/// 帧处理管道(后台处理核心) /// 帧处理管道(后台处理核心)
@@ -12,6 +15,8 @@ public class ProcessingPipeline
{ {
#region --- (Private Resources & States) --- #region --- (Private Resources & States) ---
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
/// <summary> 任务队列(有界通道):存储待处理的帧任务 </summary> /// <summary> 任务队列(有界通道):存储待处理的帧任务 </summary>
private readonly Channel<ProcessingTask> _queue; private readonly Channel<ProcessingTask> _queue;
@@ -135,7 +140,7 @@ public class ProcessingPipeline
catch (Exception ex) catch (Exception ex)
{ {
// 捕获处理过程中的异常,避免影响后续任务执行 // 捕获处理过程中的异常,避免影响后续任务执行
Console.WriteLine($"[PipelineError] 帧处理失败 (DeviceId: {task.DeviceId}, Seq: {task.Decision.Sequence}): {ex.Message}"); _sysLog.Error($"[Pipeline] 帧处理失败 (DeviceId: {task.DeviceId}, Seq: {task.Decision.Sequence}): {ex.Message}");
} }
finally finally
{ {

View File

@@ -1,4 +1,6 @@
using System.Net.NetworkInformation; using Ayay.SerilogLogs;
using Serilog;
using System.Net.NetworkInformation;
using System.Net.Sockets; using System.Net.Sockets;
namespace SHH.CameraSdk; namespace SHH.CameraSdk;
@@ -13,6 +15,8 @@ public class CameraCoordinator
{ {
#region --- (Private Resources & Configurations) --- #region --- (Private Resources & Configurations) ---
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
/// <summary> 已注册的相机设备集合(线程安全,支持并发添加与遍历) </summary> /// <summary> 已注册的相机设备集合(线程安全,支持并发添加与遍历) </summary>
private readonly ConcurrentBag<BaseVideoSource> _cameras = new(); private readonly ConcurrentBag<BaseVideoSource> _cameras = new();
@@ -90,7 +94,7 @@ public class CameraCoordinator
catch (Exception ex) catch (Exception ex)
{ {
// 捕获调度层全局异常,避免循环终止 // 捕获调度层全局异常,避免循环终止
Console.WriteLine($"[CoordinatorCritical] 调度循环异常: {ex.Message}"); _sysLog.Error($"[Coordinator] 调度循环异常: {ex.Message}");
} }
try try
@@ -186,7 +190,7 @@ public class CameraCoordinator
// 【关键修复】:增加了 && cam.IsRunning 判定,防止待机状态下被误复位 // 【关键修复】:增加了 && cam.IsRunning 判定,防止待机状态下被误复位
else if (isPhysicalOk && cam.IsOnline && !isFlowing && cam.IsRunning) // [cite: 504] else if (isPhysicalOk && cam.IsOnline && !isFlowing && cam.IsRunning) // [cite: 504]
{ {
Console.WriteLine($"[自愈] 设备 {cam.Id} 僵死({secondsSinceLastFrame:F1}秒无帧),复位中..."); _sysLog.Warning($"[Coordinator] [自愈] 设备 {cam.Id} 僵死({secondsSinceLastFrame:F1}秒无帧),复位中...");
await cam.StopAsync().ConfigureAwait(false); await cam.StopAsync().ConfigureAwait(false);
} }
} }

View File

@@ -1,4 +1,7 @@
namespace SHH.CameraSdk; using Ayay.SerilogLogs;
using Serilog;
namespace SHH.CameraSdk;
/// <summary> /// <summary>
/// 帧控制器(混合模式最终版) /// 帧控制器(混合模式最终版)
@@ -8,6 +11,8 @@
/// </summary> /// </summary>
public class FrameController public class FrameController
{ {
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
// 需求字典 // 需求字典
private readonly ConcurrentDictionary<string, FrameRequirement> _requirements = new(); private readonly ConcurrentDictionary<string, FrameRequirement> _requirements = new();
@@ -68,7 +73,7 @@ public class FrameController
// 2. 从积分累加器中移除(防止内存泄漏) // 2. 从积分累加器中移除(防止内存泄漏)
_accumulators.TryRemove(appId, out _); _accumulators.TryRemove(appId, out _);
Console.WriteLine($"[Scheduler] 已从调度中心彻底移除 AppId: {appId}"); _sysLog.Warning($"[Core] 帧控制器已从调度中心彻底移除 AppId: {appId}");
} }
// --------------------------------------------------------- // ---------------------------------------------------------

View File

@@ -22,46 +22,42 @@ namespace SHH.CameraSdk
services.AddSingleton<ProcessingConfigManager>(); services.AddSingleton<ProcessingConfigManager>();
// ============================================================= // =============================================================
// 2. 图像处理流水线编排 (Pipeline) // 2. 图像处理流水线编排 (Pipeline) - 修复版
// ============================================================= // =============================================================
// 这里我们利用 Factory 模式在注册时完成链条组装,保持了你原有的逻辑
// 1. 先注册下游节点 (Enhance)
// 这样整个系统(包括 Controller 和 Scale都共享这唯一的一个实例
services.AddSingleton<ImageEnhanceCluster>(sp =>
{
var configMgr = sp.GetRequiredService<ProcessingConfigManager>();
return new ImageEnhanceCluster(4, configMgr);
});
// 2. 再注册上游节点 (Scale) 并完成组装
services.AddSingleton<ImageScaleCluster>(sp => services.AddSingleton<ImageScaleCluster>(sp =>
{ {
var configMgr = sp.GetRequiredService<ProcessingConfigManager>(); var configMgr = sp.GetRequiredService<ProcessingConfigManager>();
// 手动创建实例 // 创建 Scale 实例
var scale = new ImageScaleCluster(4, configMgr); var scale = new ImageScaleCluster(4, configMgr);
var enhance = new ImageEnhanceCluster(4, configMgr);
// ★ 编排流水线:缩放 -> 增亮 // ★ 关键修复:从容器中获取已经在上面注册好的 Enhance 实例
// 而不是 new 一个新的
var enhance = sp.GetRequiredService<ImageEnhanceCluster>();
// ★ 编排流水线:缩放 -> 增亮 (现在引用的是同一个对象了)
scale.SetNext(enhance); scale.SetNext(enhance);
// ★ 全局路由挂载 (兼容旧驱动层) // ★ 全局路由挂载
GlobalPipelineRouter.SetProcessor(scale); GlobalPipelineRouter.SetProcessor(scale);
return scale; return scale;
}); });
// 注册 EnhanceCluster以防 Controller 单独请求它
// 注意:这里我们通过从 Scale 中获取 Next 来保证是同一个实例链条
services.AddSingleton<ImageEnhanceCluster>(sp =>
{
var scale = sp.GetRequiredService<ImageScaleCluster>();
// 这里假设链条没变,或者你可以重新 new 一个,但为了保持引用一致性,
// 建议尽量通过主入口访问,或者在这里重新创建独立的(取决于业务需求)。
// 按照你之前的逻辑,这里为了简单,我们重新注册一个新的或沿用上一个逻辑。
// *最佳实践*:如果 enhancing 是依附于 scaling 的,通常只注册 Head。
// 但为了兼容你原代码的 DI 注册:
return new ImageEnhanceCluster(4, sp.GetRequiredService<ProcessingConfigManager>());
});
// ============================================================= // =============================================================
// 3. 核心业务服务 // 3. 核心业务服务
// ============================================================= // =============================================================
// 文件存储服务 (依赖 processId)
services.AddSingleton<IStorageService>(sp => new FileStorageService(processId));
// 核心设备管理器 (自动注入 IStorageService) // 核心设备管理器 (自动注入 IStorageService)
services.AddSingleton<CameraManager>(); services.AddSingleton<CameraManager>();

View File

@@ -1,4 +1,7 @@
namespace SHH.CameraSdk using Ayay.SerilogLogs;
using Serilog;
namespace SHH.CameraSdk
{ {
#region --- (Frame Processor Cluster) --- #region --- (Frame Processor Cluster) ---
@@ -15,6 +18,9 @@
where TWorker : BaseWorker where TWorker : BaseWorker
{ {
#region --- --- #region --- ---
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
/// <summary> Worker 线程池,负责具体的帧处理任务 </summary> /// <summary> Worker 线程池,负责具体的帧处理任务 </summary>
protected readonly List<TWorker> _workers = new List<TWorker>(); protected readonly List<TWorker> _workers = new List<TWorker>();
@@ -39,7 +45,10 @@
{ {
// 校验并行度参数,避免无效配置 // 校验并行度参数,避免无效配置
if (workerCount < 1) if (workerCount < 1)
throw new ArgumentOutOfRangeException(nameof(workerCount), "Worker数量必须大于0"); {
_sysLog.Error("[Core] 帧处理集群初始化失败, 线程数必须 > 0.");
throw new ArgumentOutOfRangeException(nameof(workerCount), "帧处理集群初始化失败, 线程数必须 > 0.");
}
_configManager = configManager; // 先赋值配置管理器 _configManager = configManager; // 先赋值配置管理器
_workerCount = workerCount; _workerCount = workerCount;
@@ -50,7 +59,7 @@
_workers.Add(CreateWorker(i)); _workers.Add(CreateWorker(i));
} }
Console.WriteLine($"[{serviceName}] 服务已初始化 (并行度: {workerCount})"); _sysLog.Information($"[Core] 帧处理集群初始化成功, {serviceName} 并行数 {workerCount}. 注: 不能大于CPU核心数.");
} }
#endregion #endregion
@@ -156,6 +165,8 @@
{ {
#region --- --- #region --- ---
private static ILogger _gRpcLog = Log.ForContext("SourceContext", LogModules.gRpc);
/// <summary> 线程内任务队列容量限制100防止内存溢出 </summary> /// <summary> 线程内任务队列容量限制100防止内存溢出 </summary>
private readonly BlockingCollection<(long DeviceId, SmartFrame Frame, FrameDecision Decision)> _taskQueue = new BlockingCollection<(long, SmartFrame, FrameDecision)>(100); private readonly BlockingCollection<(long DeviceId, SmartFrame Frame, FrameDecision Decision)> _taskQueue = new BlockingCollection<(long, SmartFrame, FrameDecision)>(100);
@@ -195,7 +206,7 @@
{ {
// 背压处理:丢弃当前帧,释放引用计数 // 背压处理:丢弃当前帧,释放引用计数
frame.Dispose(); frame.Dispose();
Console.WriteLine($"[Worker] 任务队列已满,丢弃设备 {deviceId} 的帧 (引用计数已释放)"); _gRpcLog.Debug($"[gRpc] 任务队列已满,BaseWorker 丢弃设备 {deviceId} 的帧.");
} }
} }
@@ -228,7 +239,7 @@
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[Worker] 帧处理异常: {ex.Message}"); _gRpcLog.Information($"[gRpc] 帧处理异常BaseWorker 异常消息: {ex.Message}.");
// 异常保底策略:即使处理失败,也透传帧到下一个环节,保证流水线不中断 // 异常保底策略:即使处理失败,也透传帧到下一个环节,保证流水线不中断
NotifyFinished(taskItem.DeviceId, frame, taskItem.Decision); NotifyFinished(taskItem.DeviceId, frame, taskItem.Decision);
@@ -239,11 +250,11 @@
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// 正常取消:线程退出,无需报错 // 正常取消:线程退出,无需报错
Console.WriteLine("[Worker] 处理循环已正常终止"); _gRpcLog.Information($"[gRpc] BaseWorker 处理循环已正常终止.");
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[Worker] 处理循环异常终止: {ex.Message}"); _gRpcLog.Error($"[gRpc] BaseWorker 处理循环异常终止.");
} }
} }

View File

@@ -1,4 +1,6 @@
using OpenCvSharp; using Ayay.SerilogLogs;
using OpenCvSharp;
using Serilog;
namespace SHH.CameraSdk namespace SHH.CameraSdk
{ {
@@ -17,6 +19,9 @@ namespace SHH.CameraSdk
public class DisplayWindowManager : IDisposable public class DisplayWindowManager : IDisposable
{ {
#region --- --- #region --- ---
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
/// <summary> /// <summary>
/// 单个窗口的上下文信息载体 /// 单个窗口的上下文信息载体
/// 存储设备关联、运行状态、回调函数等核心数据 /// 存储设备关联、运行状态、回调函数等核心数据
@@ -64,7 +69,7 @@ namespace SHH.CameraSdk
_cameraManager = cameraManager; _cameraManager = cameraManager;
// 启动长驻UI线程设置 LongRunning 提升调度优先级 // 启动长驻UI线程设置 LongRunning 提升调度优先级
_uiThread = Task.Factory.StartNew(UILoop, TaskCreationOptions.LongRunning); _uiThread = Task.Factory.StartNew(UILoop, TaskCreationOptions.LongRunning);
Console.WriteLine("[DisplayManager] 渲染引擎就绪 (防僵尸窗口终极版)"); _sysLog.Information("[DisplayManager] 渲染引擎就绪 (防僵尸窗口终极版)");
} }
/// <summary> /// <summary>
@@ -77,7 +82,7 @@ namespace SHH.CameraSdk
// 2. 等待 UI 线程退出最多等待1秒防止卡死 // 2. 等待 UI 线程退出最多等待1秒防止卡死
try { _uiThread.Wait(1000); } try { _uiThread.Wait(1000); }
catch (Exception ex) { Console.WriteLine($"[DisplayManager] 线程退出异常: {ex.Message}"); } catch (Exception ex) { _sysLog.Error($"[DisplayManager] 线程退出异常: {ex.Message}"); }
// 3. 强制清理所有活跃窗口 // 3. 强制清理所有活跃窗口
foreach (var appId in _activeWindows.Keys) foreach (var appId in _activeWindows.Keys)
@@ -95,7 +100,7 @@ namespace SHH.CameraSdk
_uiActionQueue.Dispose(); _uiActionQueue.Dispose();
_cts.Dispose(); _cts.Dispose();
Console.WriteLine("[DisplayManager] 渲染引擎已安全销毁"); _sysLog.Information("[DisplayManager] 渲染引擎已安全销毁");
} }
#endregion #endregion
@@ -110,7 +115,7 @@ namespace SHH.CameraSdk
// 防重入:已存在该窗口则直接返回 // 防重入:已存在该窗口则直接返回
if (_activeWindows.ContainsKey(appId)) return; if (_activeWindows.ContainsKey(appId)) return;
Console.WriteLine($"[DisplayManager] 正在启动窗口: {appId} -> Device {deviceId}"); _sysLog.Information($"[DisplayManager] 正在启动窗口: {appId} -> Device {deviceId}");
// 初始化窗口上下文 // 初始化窗口上下文
var context = new WindowContext var context = new WindowContext
@@ -126,7 +131,7 @@ namespace SHH.CameraSdk
if (mouseEvent == MouseEventTypes.LButtonDown) if (mouseEvent == MouseEventTypes.LButtonDown)
{ {
context.IsPaused = !context.IsPaused; context.IsPaused = !context.IsPaused;
Console.WriteLine($"[DisplayManager] 窗口 {appId} 状态切换: {(context.IsPaused ? "" : "")}"); _sysLog.Information($"[DisplayManager] 窗口 {appId} 状态切换: {(context.IsPaused ? "" : "")}");
} }
}; };
@@ -148,7 +153,7 @@ namespace SHH.CameraSdk
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[DisplayManager] 窗口 {appId} 初始化失败: {ex.Message}"); _sysLog.Error($"[DisplayManager] 窗口 {appId} 初始化失败: {ex.Message}");
} }
}); });
@@ -179,7 +184,7 @@ namespace SHH.CameraSdk
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[DisplayManager] 帧克隆失败: {ex.Message}"); _sysLog.Error($"[DisplayManager] 帧克隆失败: {ex.Message}");
return; return;
} }
@@ -223,7 +228,7 @@ namespace SHH.CameraSdk
// 从注册表中移除窗口上下文 // 从注册表中移除窗口上下文
if (_activeWindows.TryRemove(appId, out var context)) if (_activeWindows.TryRemove(appId, out var context))
{ {
Console.WriteLine($"[DisplayManager] 正在清理窗口资源: {appId}"); _sysLog.Information($"[DisplayManager] 正在清理窗口资源: {appId}");
// 步骤1立即取消帧数据流订阅 // 步骤1立即取消帧数据流订阅
GlobalStreamDispatcher.Unsubscribe(appId); GlobalStreamDispatcher.Unsubscribe(appId);
@@ -237,7 +242,7 @@ namespace SHH.CameraSdk
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[DisplayManager] 窗口 {appId} 销毁失败: {ex.Message}"); _sysLog.Error($"[DisplayManager] 窗口 {appId} 销毁失败: {ex.Message}");
} }
}); });
@@ -257,7 +262,7 @@ namespace SHH.CameraSdk
if (_activeWindows.TryGetValue(appId, out var ctx)) if (_activeWindows.TryGetValue(appId, out var ctx))
{ {
ctx.IsPaused = true; ctx.IsPaused = true;
Console.WriteLine($"[DisplayManager] 窗口 {appId} 已暂停"); _sysLog.Information($"[DisplayManager] 窗口 {appId} 已暂停");
} }
} }
@@ -270,7 +275,7 @@ namespace SHH.CameraSdk
if (_activeWindows.TryGetValue(appId, out var ctx)) if (_activeWindows.TryGetValue(appId, out var ctx))
{ {
ctx.IsPaused = false; ctx.IsPaused = false;
Console.WriteLine($"[DisplayManager] 窗口 {appId} 已恢复"); _sysLog.Information($"[DisplayManager] 窗口 {appId} 已恢复");
} }
} }
#endregion #endregion
@@ -290,7 +295,7 @@ namespace SHH.CameraSdk
var device = _cameraManager.GetDevice(deviceId); var device = _cameraManager.GetDevice(deviceId);
if (device == null) if (device == null)
{ {
Console.WriteLine($"[策略联动] 设备 {deviceId} 不存在"); _sysLog.Information($"[策略联动] 设备 {deviceId} 不存在");
return; return;
} }
@@ -298,7 +303,7 @@ namespace SHH.CameraSdk
var frameController = device.Controller; var frameController = device.Controller;
if (frameController == null) if (frameController == null)
{ {
Console.WriteLine($"[策略联动] 设备 {deviceId} 未配置帧控制器"); _sysLog.Information($"[策略联动] 设备 {deviceId} 未配置帧控制器");
return; return;
} }
@@ -306,12 +311,12 @@ namespace SHH.CameraSdk
if (fps > 0) if (fps > 0)
{ {
frameController.Register(appId, fps); frameController.Register(appId, fps);
Console.WriteLine($"[策略联动] ✅ 已注册流控: {appId} -> {fps} FPS"); _sysLog.Information($"[策略联动] ✅ 已注册流控: {appId} -> {fps} FPS");
} }
else else
{ {
frameController.Unregister(appId); frameController.Unregister(appId);
Console.WriteLine($"[策略联动] 🗑️ 已注销流控: {appId}"); _sysLog.Information($"[策略联动] 🗑️ 已注销流控: {appId}");
} }
// 记录审计日志,用于前端排查问题 // 记录审计日志,用于前端排查问题
@@ -319,7 +324,7 @@ namespace SHH.CameraSdk
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[策略联动] ❌ 联动失败: {ex.Message}"); _sysLog.Error($"[策略联动] ❌ 联动失败: {ex.Message}");
} }
} }
#endregion #endregion
@@ -360,7 +365,7 @@ namespace SHH.CameraSdk
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[UI] 渲染循环异常: {ex.Message}"); _sysLog.Error($"[UI] 渲染循环异常: {ex.Message}");
} }
} }
@@ -385,7 +390,7 @@ namespace SHH.CameraSdk
// Visible < 1.0 表示窗口已被用户手动关闭 // Visible < 1.0 表示窗口已被用户手动关闭
if (Cv2.GetWindowProperty(appId, WindowPropertyFlags.Visible) < 1.0) if (Cv2.GetWindowProperty(appId, WindowPropertyFlags.Visible) < 1.0)
{ {
Console.WriteLine($"[UI] 检测到窗口 {appId} 已被手动关闭,触发清理..."); _sysLog.Information($"[UI] 检测到窗口 {appId} 已被手动关闭,触发清理...");
// 异步清理:避免 StopDisplay 内部的队列操作阻塞 UI 线程 // 异步清理:避免 StopDisplay 内部的队列操作阻塞 UI 线程
Task.Run(() => StopDisplay(appId)); Task.Run(() => StopDisplay(appId));
} }

View File

@@ -1,172 +0,0 @@
using System.Text.Json;
namespace SHH.CameraSdk;
public class FileStorageService : IStorageService
{
public int ProcessId { get; }
private readonly string _baseDir;
private readonly string _devicesPath;
private readonly string _systemLogPath; // 系统日志路径
private readonly string _logsDir; // 设备日志文件夹
// 【关键优化】双锁分离:配置读写和日志读写互不干扰
private readonly SemaphoreSlim _configLock = new SemaphoreSlim(1, 1);
private readonly SemaphoreSlim _logLock = new SemaphoreSlim(1, 1);
// JSON 配置 (保持不变)
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
IncludeFields = true,
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString
};
public FileStorageService(int processId)
{
ProcessId = processId;
// 目录结构:
// App_Data/Process_1/
// ├── devices.json
// ├── system.log
// └── logs/
// ├── device_101.log
// └── device_102.log
_baseDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", $"Process_{processId}");
_devicesPath = Path.Combine(_baseDir, "devices.json");
_systemLogPath = Path.Combine(_baseDir, "system.log");
_logsDir = Path.Combine(_baseDir, "logs");
if (!Directory.Exists(_baseDir)) Directory.CreateDirectory(_baseDir);
if (!Directory.Exists(_logsDir)) Directory.CreateDirectory(_logsDir);
Console.WriteLine($"[Storage] 服务就绪 | 日志路径: {_systemLogPath}");
}
// ==================================================================
// 1. 设备配置管理 (使用 _configLock)
// ==================================================================
public async Task SaveDevicesAsync(IEnumerable<VideoSourceConfig> configs)
{
await _configLock.WaitAsync();
try
{
if (SdkGlobal.SaveCameraConfigEnable)
{
var json = JsonSerializer.Serialize(configs, _jsonOptions);
await File.WriteAllTextAsync(_devicesPath, json);
}
}
catch (Exception ex)
{
Console.WriteLine($"[Storage] ❌ 保存配置失败: {ex.Message}");
}
finally { _configLock.Release(); }
}
public async Task<List<VideoSourceConfig>> LoadDevicesAsync()
{
if (!File.Exists(_devicesPath)) return new List<VideoSourceConfig>();
await _configLock.WaitAsync();
try
{
if (!SdkGlobal.SaveCameraConfigEnable)
return new List<VideoSourceConfig>();
var json = await File.ReadAllTextAsync(_devicesPath);
if (string.IsNullOrWhiteSpace(json)) return new List<VideoSourceConfig>();
var list = JsonSerializer.Deserialize<List<VideoSourceConfig>>(json, _jsonOptions);
return list ?? new List<VideoSourceConfig>();
//return new List<VideoSourceConfig>();
}
catch (Exception ex)
{
Console.WriteLine($"[Storage] ❌ 读取配置失败: {ex.Message}");
return new List<VideoSourceConfig>();
}
finally { _configLock.Release(); }
}
// ==================================================================
// 2. 系统操作日志 (使用 _logLock)
// ==================================================================
public async Task AppendSystemLogAsync(string action, string ip, string path)
{
// 格式: [时间] | IP | 动作 路径
var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var line = $"[{time}] | {ip} | {action} {path}";
await _logLock.WaitAsync(); // 等待日志锁
try
{
// 追加写入 (Async)
await File.AppendAllTextAsync(_systemLogPath, line + Environment.NewLine);
}
catch { /* 忽略日志写入错误,别崩了主程序 */ }
finally { _logLock.Release(); }
}
public async Task<List<string>> GetSystemLogsAsync(int count)
{
if (!File.Exists(_systemLogPath)) return new List<string> { "暂无日志" };
await _logLock.WaitAsync();
try
{
// 读取所有行 (如果日志文件非常大这里建议优化为倒序读取但几MB以内没问题)
var lines = await File.ReadAllLinesAsync(_systemLogPath);
// 取最后 N 行,并反转(让最新的显示在最上面)
return lines.TakeLast(count).Reverse().ToList();
}
catch (Exception ex)
{
return new List<string> { $"读取失败: {ex.Message}" };
}
finally { _logLock.Release(); }
}
// ==================================================================
// 3. 设备审计日志 (使用 _logLock)
// ==================================================================
public async Task AppendDeviceLogAsync(int deviceId, string message)
{
var path = Path.Combine(_logsDir, $"device_{deviceId}.log");
var time = DateTime.Now.ToString("MM-dd HH:mm:ss");
var line = $"{time} > {message}";
await _logLock.WaitAsync(); // 复用日志锁防止多文件同时IO导致磁盘抖动
try
{
await File.AppendAllTextAsync(path, line + Environment.NewLine);
}
catch { }
finally { _logLock.Release(); }
}
public async Task<List<string>> GetDeviceLogsAsync(int deviceId, int count)
{
var path = Path.Combine(_logsDir, $"device_{deviceId}.log");
if (!File.Exists(path)) return new List<string>();
await _logLock.WaitAsync();
try
{
var lines = await File.ReadAllLinesAsync(path);
return lines.TakeLast(count).Reverse().ToList();
}
catch
{
return new List<string>();
}
finally { _logLock.Release(); }
}
}

View File

@@ -1,4 +1,7 @@
namespace SHH.CameraSdk; using Ayay.SerilogLogs;
using Serilog;
namespace SHH.CameraSdk;
/// <summary> /// <summary>
/// [配置中心] 预处理参数管理器 /// [配置中心] 预处理参数管理器
@@ -6,6 +9,8 @@
/// </summary> /// </summary>
public class ProcessingConfigManager public class ProcessingConfigManager
{ {
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
// 内存字典Key=设备ID, Value=配置对象 // 内存字典Key=设备ID, Value=配置对象
private readonly ConcurrentDictionary<long, ProcessingOptions> _configs = new(); private readonly ConcurrentDictionary<long, ProcessingOptions> _configs = new();
@@ -30,7 +35,7 @@ public class ProcessingConfigManager
// 直接覆盖旧配置,由于是引用替换,原子性较高 // 直接覆盖旧配置,由于是引用替换,原子性较高
_configs.AddOrUpdate(deviceId, newOptions, (key, old) => newOptions); _configs.AddOrUpdate(deviceId, newOptions, (key, old) => newOptions);
Console.WriteLine($"[ConfigManager] 设备 {deviceId} 预处理参数已更新: " + _sysLog.Information($"[ConfigManager] 设备 {deviceId} 预处理参数已更新: " +
$"Expand={newOptions.EnableExpand} Shrink:{newOptions.EnableShrink} 分辨率:({newOptions.TargetWidth}x{newOptions.TargetHeight}), " + $"Expand={newOptions.EnableExpand} Shrink:{newOptions.EnableShrink} 分辨率:({newOptions.TargetWidth}x{newOptions.TargetHeight}), " +
$"EnableBrightness}}={newOptions.EnableBrightness}"); $"EnableBrightness}}={newOptions.EnableBrightness}");
} }

View File

@@ -9,13 +9,10 @@ namespace SHH.CameraSdk;
/// </summary> /// </summary>
public class UserActionFilter : IActionFilter public class UserActionFilter : IActionFilter
{ {
private readonly IStorageService _storage;
// 【关键点】构造函数注入 // 【关键点】构造函数注入
// ASP.NET Core 会自动把我们在 Program.cs 中注册的 IStorageService 实例传进来 // ASP.NET Core 会自动把我们在 Program.cs 中注册的 IStorageService 实例传进来
public UserActionFilter(IStorageService storage) public UserActionFilter()
{ {
_storage = storage;
} }
/// <summary> /// <summary>
@@ -32,11 +29,6 @@ public class UserActionFilter : IActionFilter
if (method != "GET") if (method != "GET")
{ {
var ip = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; var ip = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown";
// 3. 调用存储服务写入日志
// 注意:这里我们不等待任务完成 (Fire-and-Forget),以免日志写入拖慢 API 响应速度
// 因为 _storage.AppendSystemLogAsync 内部目前是空实现(Task.CompletedTask),所以这里绝对不会卡顿
_ = _storage.AppendSystemLogAsync(method, ip, path);
} }
} }

View File

@@ -38,8 +38,8 @@ public static class Bootstrapper
"--uris", "localhost,9001,command,调试PC;", "--uris", "localhost,9001,command,调试PC;",
// 日志中心配置 (格式: IP,Port,Desc) // 日志中心配置 (格式: IP,Port,Desc)
"--sequris", "58.216.225.5,20026,日志处置中心;", "--sequris", "172.16.41.241,20026,日志处置中心;",
"--seqkey", "Shine101173874928;", "--seqkey", "Shine899195994250;",
// 端口策略 // 端口策略
"--mode", "1", "--mode", "1",
@@ -100,11 +100,11 @@ public static class Bootstrapper
// ========================================================= // =========================================================
if (exitCode != 0) if (exitCode != 0)
{ {
sysLog.Fatal($"💀 [程序终止] {reason} (Code: {exitCode})"); sysLog.Fatal($"[Core] 💀 [程序终止] {reason} (Code: {exitCode})");
} }
else else
{ {
sysLog.Information($"👋 [程序退出] {reason}"); sysLog.Information($"[Core] 👋 [程序退出] {reason}");
} }
// ========================================================= // =========================================================
@@ -113,7 +113,7 @@ public static class Bootstrapper
// 防止 SDK 句柄残留导致下次启动无法连接相机 // 防止 SDK 句柄残留导致下次启动无法连接相机
try try
{ {
sysLog.Information("正在清理 Hikvision SDK 资源..."); sysLog.Information("[Core] 正在清理 Hikvision SDK 资源...");
// 如果你的项目中引用了 SDK请务必解开这行注释 // 如果你的项目中引用了 SDK请务必解开这行注释
HikNativeMethods.NET_DVR_Cleanup(); HikNativeMethods.NET_DVR_Cleanup();
@@ -162,7 +162,7 @@ public static class Bootstrapper
/// </summary> /// </summary>
public static int ScanForAvailablePort(ServiceConfig config, ILogger logger) 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++) for (int i = 0; i <= config.MaxPortRange; i++)
{ {
@@ -171,15 +171,15 @@ public static class Bootstrapper
{ {
if (currentPort != config.BasePort) if (currentPort != config.BasePort)
{ {
logger.Warning($"⚙️ 端口自动漂移: {config.BasePort} -> {currentPort}"); logger.Warning($"[Core] ⚙️ 端口自动漂移: {config.BasePort} -> {currentPort}");
} }
else else
{ {
logger.Information($"✅ 端口检测通过: {currentPort}"); logger.Information($"[Core] ✅ 端口检测通过: {currentPort}");
} }
return currentPort; return currentPort;
} }
logger.Debug($"⚠️ 端口 {currentPort} 被占用,尝试下一个..."); logger.Debug($"[Core] ⚠️ 端口 {currentPort} 被占用,尝试下一个...");
} }
return -1; return -1;
} }
@@ -217,16 +217,16 @@ public static class Bootstrapper
/// </summary> /// </summary>
public static void WarmUpHardware(ILogger logger) public static void WarmUpHardware(ILogger logger)
{ {
logger.Information("Hik Sdk 开始预热."); logger.Information("[Core] Hik Sdk 开始预热.");
try try
{ {
HikNativeMethods.NET_DVR_Init(); HikNativeMethods.NET_DVR_Init();
HikSdkManager.ForceWarmUp(); HikSdkManager.ForceWarmUp();
logger.Information("💡Hik Sdk 预热成功."); logger.Information("[Core] 💡Hik Sdk 预热成功.");
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.Error(ex, "⚠️ Hik Sdk 预热失败."); logger.Error(ex, "[Core] ⚠️ Hik Sdk 预热失败.");
} }
} }
@@ -244,13 +244,13 @@ public static class Bootstrapper
try try
{ {
// 将 tcp:// 转换为 http:// 以适配 gRPC // 将 tcp:// 转换为 http:// 以适配 gRpc
string targetUrl = config.CommandEndpoints.First().Uri.Replace("tcp://", "http://"); string targetUrl = config.CommandEndpoints.First().Uri.Replace("tcp://", "http://");
using var channel = GrpcChannel.ForAddress(targetUrl); using var channel = GrpcChannel.ForAddress(targetUrl);
var client = new GatewayProvider.GatewayProviderClient(channel); var client = new GatewayProvider.GatewayProviderClient(channel);
gRpcLog.Information($"[gRPC] 正在执行预注册: {targetUrl}"); gRpcLog.Information($"[gRpc] 正在执行预注册: {targetUrl}");
var resp = await client.RegisterInstanceAsync(new RegisterRequest var resp = await client.RegisterInstanceAsync(new RegisterRequest
{ {
InstanceId = config.AppId, InstanceId = config.AppId,
@@ -261,11 +261,11 @@ public static class Bootstrapper
ProcessId = Environment.ProcessId, ProcessId = Environment.ProcessId,
Description = "" Description = ""
}); });
gRpcLog.Information($"💡[gRPC] 预注册成功: {resp.Message}"); gRpcLog.Information($"[gRpc] 💡预注册成功: {resp.Message}");
} }
catch (Exception ex) catch (Exception ex)
{ {
gRpcLog.Error($"⚠️ [gRPC] 预注册尝试失败: {ex.Message}"); gRpcLog.Error($"[gRpc] ⚠️ 预注册尝试失败: {ex.Message}");
} }
} }

View File

@@ -6,7 +6,7 @@ using SHH.Contracts.Grpc;
namespace SHH.CameraService; namespace SHH.CameraService;
/// <summary> /// <summary>
/// gRPC 指令分发器 /// gRpc 指令分发器
/// 职责:接收从 GrpcCommandReceiverWorker 传入的 Proto 消息,解析参数并路由至具体的 Handler。 /// 职责:接收从 GrpcCommandReceiverWorker 传入的 Proto 消息,解析参数并路由至具体的 Handler。
/// </summary> /// </summary>
public class CommandDispatcher public class CommandDispatcher
@@ -29,14 +29,14 @@ public class CommandDispatcher
/// <summary> /// <summary>
/// 执行指令分发 /// 执行指令分发
/// </summary> /// </summary>
/// <param name="protoMsg">从 gRPC Server Streaming 接收到的原始 Proto 指令对象</param> /// <param name="protoMsg">从 gRpc Server Streaming 接收到的原始 Proto 指令对象</param>
public async Task DispatchAsync(CommandPayloadProto protoMsg) public async Task DispatchAsync(CommandPayloadProto protoMsg)
{ {
if (protoMsg == null) return; if (protoMsg == null) return;
string cmdCode = protoMsg.CmdCode; // 例如 "Sync_Camera" string cmdCode = protoMsg.CmdCode; // 例如 "Sync_Camera"
_gRpcLog.Information($"[gRPC] 响应请求, 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 业务分发."); _gRpcLog.Information($"[gRpc] 响应请求, 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 业务分发.");
_gRpcLog.Debug($"[gRPC] 响应请求, {protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 业务分发 => {protoMsg}"); _gRpcLog.Debug($"[gRpc] 响应请求, {protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 业务分发 => {protoMsg}");
try try
{ {
@@ -51,20 +51,20 @@ public class CommandDispatcher
// 3. 调用具体业务执行 // 3. 调用具体业务执行
await handler.ExecuteAsync(token); await handler.ExecuteAsync(token);
_gRpcLog.Information($"[gRPC] 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 执行成功."); _gRpcLog.Information($"[gRpc] 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 执行成功.");
} }
else else
{ {
_gRpcLog.Warning($"[gRPC] 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 未找到指令处理器."); _gRpcLog.Warning($"[gRpc] 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 未找到指令处理器.");
} }
} }
catch (Exception ex) 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) // 注意:关于 ACK (require_ack)
// 在 NetMQ 时代需要手动回发结果,在 gRPC Server Streaming 模式下, // 在 NetMQ 时代需要手动回发结果,在 gRpc Server Streaming 模式下,
// 建议通过 Unary RPC (例如另设一个 ReportCommandResult 方法) 异步上报执行结果。 // 建议通过 Unary RPC (例如另设一个 ReportCommandResult 方法) 异步上报执行结果。
} }
} }

View File

@@ -1,44 +1,52 @@
using System.Diagnostics; using Ayay.SerilogLogs;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Serilog;
using SHH.CameraSdk; using SHH.CameraSdk;
using System.Diagnostics;
namespace SHH.CameraService; namespace SHH.CameraService;
/// <summary>
/// 父进程守护服务 (BackgroundService)
/// <para>核心逻辑:定期检查启动本服务的父进程是否存活,若父进程退出(如 UI 崩溃),则触发本服务自动退出,避免孤儿进程占用相机硬件资源。</para>
/// </summary>
public class ParentProcessSentinel : BackgroundService public class ParentProcessSentinel : BackgroundService
{ {
private readonly ServiceConfig _config; private readonly ServiceConfig _config;
private readonly IHostApplicationLifetime _lifetime; private readonly IHostApplicationLifetime _lifetime;
private readonly ILogger<ParentProcessSentinel> _logger; private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
/// <summary>
/// 使用统一的结构化日志记录器SourceContext 设置为 Core 模块
/// </summary>
public ParentProcessSentinel( public ParentProcessSentinel(
ServiceConfig config, ServiceConfig config,
IHostApplicationLifetime lifetime, IHostApplicationLifetime lifetime)
ILogger<ParentProcessSentinel> logger)
{ {
_config = config; _config = config;
_lifetime = lifetime; _lifetime = lifetime;
_logger = logger;
} }
/// <summary>
/// 执行后台守护逻辑
/// </summary>
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
int pid = _config.ParentPid; int pid = _config.ParentPid;
// 1. 验证 PID 合法性。如果 PID 为 0 或负数,可能是手动启动调试模式,不执行守护逻辑
// 如果 PID 为 0 或负数,说明不需要守护(可能是手动启动调试)
if (pid <= 0) if (pid <= 0)
{ {
_logger.LogInformation("未指定有效的父进程 PID守护模式已禁用。"); _sysLog.Warning("[Sentinel] 未指定有效的父进程 PID ({ParentPid}),守护模式已禁用,服务将持续运行.", pid);
return; return;
} }
_logger.LogInformation($"父进程守护已启动,正在监控 PID: {pid}"); _sysLog.Information("[Sentinel] 父进程守护已启动,正在监控目标 PID: {ParentPid}", pid);
while (!stoppingToken.IsCancellationRequested) while (!stoppingToken.IsCancellationRequested)
{ {
if (!IsParentRunning(pid)) if (!IsParentRunning(pid))
{ {
_logger.LogWarning($"[ALERT] 检测到父进程 (PID:{pid}) 已退出!正在终止当前服务..."); _sysLog.Warning("[Sentinel] ### ALERT ### 检测到父进程 (PID:{ParentPid}) 已退出!正在下发系统终止信号...", pid);
// 触发程序优雅退出 // 触发程序优雅退出
_lifetime.StopApplication(); _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) private bool IsParentRunning(int pid)
{ {
try try
@@ -72,7 +85,7 @@ public class ParentProcessSentinel : BackgroundService
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "检查父进程状态时发生未知错误,默认为存活"); _sysLog.Debug("[Sentinel] 无法定位 PID 为 {ParentPid} 的进程,判定为已退出.", pid);
return true; // 发生未知错误时,保守起见认为它还活着 return true; // 发生未知错误时,保守起见认为它还活着
} }
} }

View File

@@ -12,6 +12,7 @@ namespace SHH.CameraService;
public class DeviceConfigHandler : ICommandHandler public class DeviceConfigHandler : ICommandHandler
{ {
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
private readonly CameraManager _cameraManager; private readonly CameraManager _cameraManager;
/// <summary> /// <summary>
@@ -42,12 +43,15 @@ public class DeviceConfigHandler : ICommandHandler
// 2. 尝试获取现有设备 // 2. 尝试获取现有设备
var device = _cameraManager.GetDevice(dto.Id); 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) if (device != null)
{ {
// ========================================================= // =========================================================
// 场景 A: 设备已存在 -> 执行智能更新 (Smart Update) // 场景 A: 设备已存在 -> 执行智能更新 (Smart Update)
// ========================================================= // =========================================================
Console.WriteLine($"[Sync] 更新设备配置: {dto.Id} ({dto.Name})");
// 将全量配置映射为部分更新 DTO // 将全量配置映射为部分更新 DTO
var updateDto = new DeviceUpdateDto var updateDto = new DeviceUpdateDto
@@ -86,7 +90,6 @@ public class DeviceConfigHandler : ICommandHandler
// ========================================================= // =========================================================
// 场景 B: 设备不存在 -> 执行新增 (Add New) // 场景 B: 设备不存在 -> 执行新增 (Add New)
// ========================================================= // =========================================================
Console.WriteLine($"[Sync] 新增设备: {dto.Id} ({dto.Name})");
// 构造全新的设备配置 // 构造全新的设备配置
var newConfig = new VideoSourceConfig var newConfig = new VideoSourceConfig
@@ -126,7 +129,7 @@ public class DeviceConfigHandler : ICommandHandler
// 情况 1: 收到“启动”指令 // 情况 1: 收到“启动”指令
if (!device.IsOnline) // 只有没在线时才点火 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(); _ = device.StartAsync();
} }
} }
@@ -135,7 +138,7 @@ public class DeviceConfigHandler : ICommandHandler
// 情况 2: 收到“停止”指令 (即 ImmediateExecution = false) // 情况 2: 收到“停止”指令 (即 ImmediateExecution = false)
if (device.IsOnline) // 只有在线时才熄火 if (device.IsOnline) // 只有在线时才熄火
{ {
Console.WriteLine($"[Sync] 指令:立即停止设备 {dto.Id}"); _sysLog.Warning($"[Sync] 设备立即停止 {dto.Id}");
_ = device.StopAsync(); _ = device.StopAsync();
} }
} }

View File

@@ -1,7 +1,8 @@
using Grpc.Core; using Ayay.SerilogLogs;
using Grpc.Core;
using Grpc.Net.Client; using Grpc.Net.Client;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Serilog;
using SHH.CameraSdk; using SHH.CameraSdk;
using SHH.Contracts; using SHH.Contracts;
using SHH.Contracts.Grpc; using SHH.Contracts.Grpc;
@@ -10,14 +11,15 @@ using System.Collections.Concurrent;
namespace SHH.CameraService; namespace SHH.CameraService;
/// <summary> /// <summary>
/// 设备状态监控工作者 (gRPC 版) /// 设备状态监控工作者 (gRpc 版)
/// 职责:监控相机状态并在状态变更或心跳周期内,通过 gRPC 批量上报至所有配置的端点 /// 职责:监控相机状态并在状态变更或心跳周期内,通过 gRpc 批量上报至所有配置的端点
/// </summary> /// </summary>
public class DeviceStatusHandler : BackgroundService public class DeviceStatusHandler : BackgroundService
{ {
private static ILogger _gRpcLog = Log.ForContext("SourceContext", LogModules.gRpc);
private readonly CameraManager _manager; private readonly CameraManager _manager;
private readonly ServiceConfig _config; private readonly ServiceConfig _config;
private readonly ILogger<DeviceStatusHandler> _logger;
// 状态存储CameraId -> 状态载荷 // 状态存储CameraId -> 状态载荷
private readonly ConcurrentDictionary<string, StatusEventPayload> _stateStore = new(); private readonly ConcurrentDictionary<string, StatusEventPayload> _stateStore = new();
@@ -27,12 +29,10 @@ public class DeviceStatusHandler : BackgroundService
public DeviceStatusHandler( public DeviceStatusHandler(
CameraManager manager, CameraManager manager,
ServiceConfig config, ServiceConfig config)
ILogger<DeviceStatusHandler> logger)
{ {
_manager = manager; _manager = manager;
_config = config; _config = config;
_logger = logger;
} }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@@ -46,7 +46,7 @@ public class DeviceStatusHandler : BackgroundService
// 2. 订阅 SDK 状态变更事件 // 2. 订阅 SDK 状态变更事件
_manager.OnDeviceStatusChanged += OnSdkStatusChanged; _manager.OnDeviceStatusChanged += OnSdkStatusChanged;
_logger.LogInformation("[StatusWorker] gRPC 状态上报已启动,配置节点数: {Count}", _config.CommandEndpoints.Count); _gRpcLog.Information($"[gRpc] 状态上报已启动,配置节点数: {_config.CommandEndpoints.Count}");
// 3. 定时循环 (1秒1次检查) // 3. 定时循环 (1秒1次检查)
var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
@@ -60,7 +60,7 @@ public class DeviceStatusHandler : BackgroundService
catch (OperationCanceledException) { /* 正常退出 */ } catch (OperationCanceledException) { /* 正常退出 */ }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "[StatusWorker] 运行异常"); _gRpcLog.Error($"[gRpc] 状态上报运行异常");
} }
finally finally
{ {
@@ -96,12 +96,12 @@ public class DeviceStatusHandler : BackgroundService
{ {
long now = Environment.TickCount64; long now = Environment.TickCount64;
// 策略: 有变更(Dirty) 或 超过5秒(强制心跳) // 策略: 有变更(Dirty) 或 超过 2 秒(强制心跳)
bool shouldSend = _isDirty || (now - _lastSendTick > 5000); bool shouldSend = _isDirty || (now - _lastSendTick > 2000);
if (shouldSend && _config.CommandEndpoints.Any()) if (shouldSend && _config.CommandEndpoints.Any())
{ {
// 1. 构建 gRPC 请求包 // 1. 构建 gRpc 请求包
var request = new StatusBatchRequest var request = new StatusBatchRequest
{ {
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
@@ -129,13 +129,12 @@ public class DeviceStatusHandler : BackgroundService
using var channel = GrpcChannel.ForAddress(grpcUrl); using var channel = GrpcChannel.ForAddress(grpcUrl);
var client = new GatewayProvider.GatewayProviderClient(channel); var client = new GatewayProvider.GatewayProviderClient(channel);
// 获取 gRPC 内部生成的服务全称 // 获取 gRpc 内部生成的服务全称
// 这就是客户端尝试调用的真实路径:/包名.服务名/方法名 // 这就是客户端尝试调用的真实路径:/包名.服务名/方法名
var methodName = "ReportStatusBatch";
var serviceName = client.GetType().DeclaringType?.Name ?? "Unknown"; var serviceName = client.GetType().DeclaringType?.Name ?? "Unknown";
_logger.LogInformation("[gRPC Debug] 准备调用端点: {Url}", grpcUrl); _gRpcLog.Debug("[gRpc] 准备调用端点: {Url}", grpcUrl);
_logger.LogInformation("[gRPC Debug] 客户端契约服务名: {Service}", serviceName); _gRpcLog.Debug("[gRpc] 客户端契约服务名: {Service}", serviceName);
// 执行调用 // 执行调用
var response = await client.ReportStatusBatchAsync(request, var response = await client.ReportStatusBatchAsync(request,
@@ -143,7 +142,8 @@ public class DeviceStatusHandler : BackgroundService
if (response.Success) 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; _isDirty = false;
_lastSendTick = Environment.TickCount64; _lastSendTick = Environment.TickCount64;
} }
@@ -151,17 +151,17 @@ public class DeviceStatusHandler : BackgroundService
catch (RpcException ex) catch (RpcException ex)
{ {
// 这里是关键:打印 RpcException 的详细状态 // 这里是关键:打印 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通常意味着路径不对 // 如果是 Unimplemented通常意味着路径不对
if (ex.StatusCode == StatusCode.Unimplemented) if (ex.StatusCode == StatusCode.Unimplemented)
{ {
_logger.LogError("[gRPC Fix] 请检查服务端是否注册了名为 'GatewayProvider' 的服务,且其 package 声明与客户端一致。"); _gRpcLog.Error("[gRpc] 请检查服务端是否注册了名为 'GatewayProvider' 的服务,且其 package 声明与客户端一致。");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("[gRPC Fatal] 非 RPC 异常: {Msg}", ex.Message); _gRpcLog.Error("[gRpc] 非 RPC 异常: {Msg}", ex.Message);
} }
} }
} }

View File

@@ -9,9 +9,9 @@ using SHH.Contracts.Grpc; // 引用 Proto 生成的命名空间
namespace SHH.CameraService namespace SHH.CameraService
{ {
/// <summary> /// <summary>
/// gRPC 指令接收后台服务 /// gRpc 指令接收后台服务
/// 职责: /// 职责:
/// 1. 维护与 AiVideo 的 gRPC 长连接。 /// 1. 维护与 AiVideo 的 gRpc 长连接。
/// 2. 完成节点逻辑注册。 /// 2. 完成节点逻辑注册。
/// 3. 监听 Server Streaming 指令流并移交给 Dispatcher。 /// 3. 监听 Server Streaming 指令流并移交给 Dispatcher。
/// </summary> /// </summary>
@@ -33,7 +33,7 @@ namespace SHH.CameraService
var gRpcLog = Log.ForContext("SourceContext", LogModules.gRpc); var gRpcLog = Log.ForContext("SourceContext", LogModules.gRpc);
// 预留系统启动缓冲时间,确保数据库和 SDK 已就绪 // 预留系统启动缓冲时间,确保数据库和 SDK 已就绪
gRpcLog.Information("[gRPC] 指令接收服务启动,等待环境预热..."); gRpcLog.Information("[gRpc] 指令接收服务启动,等待环境预热...");
await Task.Delay(3000, stoppingToken); await Task.Delay(3000, stoppingToken);
while (!stoppingToken.IsCancellationRequested) while (!stoppingToken.IsCancellationRequested)
@@ -48,7 +48,7 @@ namespace SHH.CameraService
var client = new GatewayProvider.GatewayProviderClient(channel); var client = new GatewayProvider.GatewayProviderClient(channel);
// --- 第一步:发起节点逻辑注册 (Unary) --- // --- 第一步:发起节点逻辑注册 (Unary) ---
gRpcLog.Information("[gRPC] 正在发起逻辑注册: {Url}", targetUrl); gRpcLog.Information("[gRpc] 正在发起逻辑注册: {Url}", targetUrl);
var regResp = await client.RegisterInstanceAsync(new RegisterRequest var regResp = await client.RegisterInstanceAsync(new RegisterRequest
{ {
InstanceId = _config.AppId, InstanceId = _config.AppId,
@@ -59,7 +59,7 @@ namespace SHH.CameraService
if (regResp.Success) if (regResp.Success)
{ {
gRpcLog.Information("[gRPC] 注册成功, 正在建立双向指令通道..."); gRpcLog.Information("[gRpc] 注册成功, 正在建立双向指令通道...");
// --- 第二步:开启 Server Streaming 指令流 --- // --- 第二步:开启 Server Streaming 指令流 ---
using var call = client.OpenCommandChannel(new CommandStreamRequest using var call = client.OpenCommandChannel(new CommandStreamRequest
@@ -86,14 +86,14 @@ namespace SHH.CameraService
} }
catch (RpcException ex) 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); await Task.Delay(5000, stoppingToken);
} }
catch (Exception ex) catch (Exception ex)
{ {
gRpcLog.Debug("[gRPC] 非预期链路异常: {Msg}5秒后尝试重连", ex.Message); gRpcLog.Debug("[gRpc] 非预期链路异常: {Msg}5秒后尝试重连", ex.Message);
await Task.Delay(5000, stoppingToken); await Task.Delay(5000, stoppingToken);
} }
} }

View File

@@ -1,4 +1,6 @@
using Newtonsoft.Json.Linq; using Ayay.SerilogLogs;
using Newtonsoft.Json.Linq;
using Serilog;
using SHH.CameraSdk; using SHH.CameraSdk;
using SHH.Contracts; using SHH.Contracts;
@@ -9,6 +11,8 @@ namespace SHH.CameraService
/// </summary> /// </summary>
public class RemoveCameraHandler : ICommandHandler public class RemoveCameraHandler : ICommandHandler
{ {
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
private readonly CameraManager _cameraManager; private readonly CameraManager _cameraManager;
/// <summary> /// <summary>
@@ -50,7 +54,7 @@ namespace SHH.CameraService
if (deviceId <= 0) if (deviceId <= 0)
{ {
Console.WriteLine($"[{ActionName}] 收到无效指令: ID解析失败 ({payload})"); _sysLog.Warning($"[Sync] 收到无效指令, ID解析失败 ({payload})");
return; return;
} }
@@ -58,26 +62,25 @@ namespace SHH.CameraService
var device = _cameraManager.GetDevice(deviceId); var device = _cameraManager.GetDevice(deviceId);
if (device == null) if (device == null)
{ {
Console.WriteLine($"[{ActionName}] 设备 {deviceId} 已经不在管理池中,无需操作"); _sysLog.Warning($"[Sync] 设备 {deviceId} 已经不在管理池中,无需操作.");
return; return;
} }
// 3. 安全移除 // 3. 安全移除
// 这里建议增加审计日志,记录谁触发了删除(如果协议里有用户信息的话) // 这里建议增加审计日志,记录谁触发了删除(如果协议里有用户信息的话)
device.AddAuditLog("收到远程指令:彻底移除设备"); _sysLog.Debug($"[Sync] 收到远程指令, 正在安全移除设备, ID:{deviceId} Name:{device.Config.Name} .");
Console.WriteLine($"[{ActionName}] 正在安全移除设备: {deviceId} ({device.Config.Name})");
// CameraManager 内部会StopAsync -> DisposeAsync -> TryRemove -> SaveChanges // CameraManager 内部会StopAsync -> DisposeAsync -> TryRemove -> SaveChanges
await _cameraManager.RemoveDeviceAsync(deviceId); await _cameraManager.RemoveDeviceAsync(deviceId);
Console.WriteLine($"[{ActionName}] 设备 {deviceId} 已彻底清理并从持久化库中移除"); _sysLog.Information($"[Sync] 收到远程指令, 设备, ID:{deviceId} Name:{device.Config.Name}已彻底清理并从持久化库中移除 .");
// 4. (可选) 此处可以调用 CommandDispatcher 发送 Success ACK // 4. (可选) 此处可以调用 CommandDispatcher 发送 Success ACK
} }
catch (Exception ex) catch (Exception ex)
{ {
// 捕获异常,防止影响全局 Socket 轮询 // 捕获异常,防止影响全局 Socket 轮询
Console.WriteLine($"[{ActionName}] 移除设备 {deviceId} 过程中发生致命错误: {ex.Message}"); _sysLog.Error($"[Sync] 移除设备, ID:{deviceId} 过程中发生致命错误, {ex.Message}.");
} }
} }
} }

View File

@@ -1,13 +1,22 @@
using Microsoft.Extensions.Hosting; using Ayay.SerilogLogs;
using Microsoft.Extensions.Hosting;
using OpenCvSharp; using OpenCvSharp;
using Serilog;
using SHH.CameraSdk; // 引用 SDK 核心 using SHH.CameraSdk; // 引用 SDK 核心
using SHH.Contracts; using SHH.Contracts;
using System.Diagnostics; using System.Diagnostics;
namespace SHH.CameraService; namespace SHH.CameraService;
/// <summary>
/// 图像监控采集控制器 (流媒体分发引擎)
/// <para>功能:监听全局图像采集总线,对图像进行实时 JPG 编码,并动态分发至云端、大屏等订阅目标。</para>
/// <para>设计模式:发布-订阅模式 + 扇出 (Fan-out) 分发。</para>
/// </summary>
public class ImageMonitorController : BackgroundService public class ImageMonitorController : BackgroundService
{ {
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
// 注入所有注册的目标(云端、大屏等),实现动态分发 // 注入所有注册的目标(云端、大屏等),实现动态分发
private readonly IEnumerable<StreamTarget> _targets; private readonly IEnumerable<StreamTarget> _targets;
@@ -16,29 +25,35 @@ public class ImageMonitorController : BackgroundService
// 如果您确实需要 100请注意带宽压力。此处我保留您要求的 100但建议未来调优。 // 如果您确实需要 100请注意带宽压力。此处我保留您要求的 100但建议未来调优。
private readonly int[] _encodeParams = { (int)ImwriteFlags.JpegQuality, 100 }; private readonly int[] _encodeParams = { (int)ImwriteFlags.JpegQuality, 100 };
/// <summary>
/// 构造函数
/// </summary>
/// <param name="targets"></param>
public ImageMonitorController(IEnumerable<StreamTarget> targets) public ImageMonitorController(IEnumerable<StreamTarget> targets)
{ {
_targets = targets; _targets = targets;
} }
/// <summary>
/// 启动后台服务:挂载事件总线
/// </summary>
protected override Task ExecuteAsync(CancellationToken stoppingToken) protected override Task ExecuteAsync(CancellationToken stoppingToken)
{ {
Console.WriteLine("[StreamWorker] 启动流媒体采集引擎..."); _sysLog.Information("[Core] 启动流媒体采集引擎...");
// ========================================================= // =========================================================
// 订阅逻辑:接入 "上帝模式" (God Mode) // 订阅逻辑:接入 "上帝模式" (God Mode)
// ========================================================= // =========================================================
// 理由:NetMQ 网关需要无差别地获取所有设备的图像。 // 理由:gRpc 需要无差别地获取所有设备的图像。
GlobalStreamDispatcher.OnGlobalFrame += ProcessFrame; GlobalStreamDispatcher.OnGlobalFrame += ProcessFrame;
_sysLog.Information($"[StreamWorker] 已挂载至全局广播总线,正在监听帧信息.");
//Console.WriteLine($"[StreamWorker] 已挂载至全局广播总线,正在监听 {GlobalStreamDispatcher.OnGlobalFrame?.GetInvocationList().Length ?? 0} 个订阅者...");
var tcs = new TaskCompletionSource(); var tcs = new TaskCompletionSource();
stoppingToken.Register(() => stoppingToken.Register(() =>
{ {
// 停止时反注册,防止静态事件内存泄漏 // 停止时反注册,防止静态事件内存泄漏
GlobalStreamDispatcher.OnGlobalFrame -= ProcessFrame; GlobalStreamDispatcher.OnGlobalFrame -= ProcessFrame;
Console.WriteLine("[StreamWorker] 已断开全局广播连接"); _sysLog.Information("[Core] 流媒体采集引擎已断开全局广播连接.");
tcs.SetResult(); tcs.SetResult();
}); });
@@ -46,9 +61,11 @@ public class ImageMonitorController : BackgroundService
} }
/// <summary> /// <summary>
/// [回调函数] 处理每一帧图像 /// [回调函数] 处理实时帧
/// 注意:此方法运行在 SDK 采集线程池,必须极速处理,严禁阻塞! /// <para>注意:此方法 SDK 采集线程池触发,必须保持极速处理,严禁在内部执行 IO 等耗时阻塞操作。</para>
/// </summary> /// </summary>
/// <param name="deviceId">设备唯一标识 ID</param>
/// <param name="frame">包含原始图像(InternalMat)和处理后图像(TargetMat)的帧数据</param>
private void ProcessFrame(long deviceId, SmartFrame frame) private void ProcessFrame(long deviceId, SmartFrame frame)
{ {
try try
@@ -110,20 +127,22 @@ public class ImageMonitorController : BackgroundService
if (!ok) if (!ok)
{ {
// 如果这里打印,说明管道由于某种原因被关闭了(通常是程序正在退出) // 如果这里打印,说明管道由于某种原因被关闭了(通常是程序正在退出)
Console.WriteLine($"[DEBUG] 管道写入失败,目标: {target.Config.Name}"); _sysLog.Warning($"[ImageMonitor] 管道写入失败,目标: {target.Config.Name}");
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
// 极少发生的内存错误,打印日志但不抛出,避免崩溃 SDK 线程 // 极少发生的内存错误,打印日志但不抛出,避免崩溃 SDK 线程
Console.WriteLine($"[StreamWorker] 采集处理异常: {ex.Message}"); _sysLog.Error($"[ImageMonitor] 采集处理异常: {ex.Message}");
} }
} }
/// <summary> /// <summary>
/// 辅助:OpenCV 内存编码 /// 调用 OpenCV 进行内存级图片编码
/// </summary> /// </summary>
/// <param name="mat">待编码的 OpenCV Mat 矩阵</param>
/// <returns>JPG 字节数组</returns>
private byte[] EncodeImage(Mat mat) private byte[] EncodeImage(Mat mat)
{ {
// ImEncode 将 Mat 编码为一维字节数组 (托管内存) // ImEncode 将 Mat 编码为一维字节数组 (托管内存)

View File

@@ -1,4 +1,6 @@
using Microsoft.Extensions.Hosting; using Ayay.SerilogLogs;
using Microsoft.Extensions.Hosting;
using Serilog;
using SHH.CameraSdk; using SHH.CameraSdk;
namespace SHH.CameraService; namespace SHH.CameraService;
@@ -65,8 +67,9 @@ public class PipelineConfigurator : IHostedService
GlobalPipelineRouter.SetProcessor(_scale); GlobalPipelineRouter.SetProcessor(_scale);
// 启动日志:打印管道组装结果,便于运维排查 // 启动日志:打印管道组装结果,便于运维排查
Console.WriteLine("[Pipeline] 图像处理链组装完成: ImageScaleCluster -> ImageEnhanceCluster"); Log.ForContext("SourceContext", LogModules.Core)
Console.WriteLine("[Pipeline] 提示:帧数据将按 '缩放 → 增强' 顺序处理,可通过 GlobalPipelineRouter 调整流程"); .Information(@"[Pipeline] 图像处理链组装完成: ImageScaleCluster -> ImageEnhanceCluster
提示:帧数据将按 '缩放 → 增强' 顺序处理,可通过 GlobalPipelineRouter 调整流程");
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -1,25 +1,26 @@
using Google.Protobuf; using Ayay.SerilogLogs;
using Google.Protobuf;
using Grpc.Net.Client; using Grpc.Net.Client;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Serilog;
using SHH.Contracts.Grpc; using SHH.Contracts.Grpc;
namespace SHH.CameraService; namespace SHH.CameraService;
/// <summary> /// <summary>
/// gRPC 视频流发送工作者 /// gRpc 视频流发送工作者
/// 职责:监听特定的 StreamTarget 队列,建立 gRPC 客户端流并持续推送图片 /// 职责:监听特定的 StreamTarget 队列,建立 gRpc 客户端流并持续推送图片
/// </summary> /// </summary>
public class GrpcSenderWorker : BackgroundService public class GrpcSenderWorker : BackgroundService
{ {
private static ILogger _gRpcLog = Log.ForContext("SourceContext", LogModules.gRpc);
private readonly StreamTarget _target; private readonly StreamTarget _target;
private readonly ILogger<GrpcSenderWorker> _logger;
private readonly string _grpcUrl; private readonly string _grpcUrl;
public GrpcSenderWorker(StreamTarget target, ILogger<GrpcSenderWorker> logger) public GrpcSenderWorker(StreamTarget target)
{ {
_target = target; _target = target;
_logger = logger;
// 自动适配地址:将配置的 tcp://localhost:9001 转换为 http://localhost:9001 // 自动适配地址:将配置的 tcp://localhost:9001 转换为 http://localhost:9001
// 并且严格使用你验证成功的 localhost // 并且严格使用你验证成功的 localhost
@@ -28,7 +29,7 @@ public class GrpcSenderWorker : BackgroundService
protected override async Task ExecuteAsync(CancellationToken stoppingToken) 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) while (!stoppingToken.IsCancellationRequested)
{ {
@@ -41,7 +42,7 @@ public class GrpcSenderWorker : BackgroundService
// 2. 开启客户端流 (UploadVideoStream 是在 proto 中定义的) // 2. 开启客户端流 (UploadVideoStream 是在 proto 中定义的)
using var call = client.UploadVideoStream(cancellationToken: stoppingToken); using var call = client.UploadVideoStream(cancellationToken: stoppingToken);
_logger.LogInformation($"[gRPC Worker] 已开启视频推送流: {_target.Config.Name}"); _gRpcLog.Information($"[gRpc] 已开启视频推送流, 目标: {_target.Config.Name}, 地址: {_grpcUrl}");
// 3. 核心搬运循环:从内存队列 (Channel) 读取数据 // 3. 核心搬运循环:从内存队列 (Channel) 读取数据
await foreach (var payload in _target.Channel.Reader.ReadAllAsync(stoppingToken)) await foreach (var payload in _target.Channel.Reader.ReadAllAsync(stoppingToken))
@@ -53,7 +54,7 @@ public class GrpcSenderWorker : BackgroundService
continue; continue;
} }
// 将业务 DTO 转换为 gRPC 原生 Request // 将业务 DTO 转换为 gRpc 原生 Request
var request = new VideoFrameRequest var request = new VideoFrameRequest
{ {
CameraId = payload.CameraId ?? "0", CameraId = payload.CameraId ?? "0",
@@ -63,7 +64,7 @@ public class GrpcSenderWorker : BackgroundService
HasOriginalImage = payload.HasOriginalImage, HasOriginalImage = payload.HasOriginalImage,
HasTargetImage = payload.HasTargetImage, HasTargetImage = payload.HasTargetImage,
// ★ 核心:将 byte[] 转换为 gRPC 的 ByteString (高性能) // ★ 核心:将 byte[] 转换为 gRpc 的 ByteString (高性能)
OriginalImageBytes = payload.OriginalImageBytes != null OriginalImageBytes = payload.OriginalImageBytes != null
? ByteString.CopyFrom(payload.OriginalImageBytes) ? ByteString.CopyFrom(payload.OriginalImageBytes)
: ByteString.Empty, : ByteString.Empty,
@@ -94,7 +95,7 @@ public class GrpcSenderWorker : BackgroundService
catch (OperationCanceledException) { break; } catch (OperationCanceledException) { break; }
catch (Exception ex) 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); await Task.Delay(5000, stoppingToken);
} }
} }

View File

@@ -25,13 +25,13 @@ public class Program
// ============================================================= // =============================================================
// 1. 启动日志 // 1. 启动日志
// ============================================================= // =============================================================
sysLog.Warning($"🚀 视频取流进程启动, 日志组件初始化完毕 => 进程: {opts.AppId}"); sysLog.Warning($"[Core] 🚀 视频取流进程启动, 日志组件初始化完毕 => 进程: {opts.AppId}");
string argString = string.Join(" ", args); string argString = string.Join(" ", args);
sysLog.Debug($"🚀 启动参数({(isDebugArgs ? "" : "")}: {argString}"); sysLog.Debug($"[Core] 🚀 启动参数({(isDebugArgs ? "" : "")}: {argString}");
// ============================================================= // =============================================================
// 2. 硬件预热、端口扫描、gRPC链接 // 2. 硬件预热、端口扫描、gRpc链接
// ============================================================= // =============================================================
Bootstrapper.WarmUpHardware(sysLog); Bootstrapper.WarmUpHardware(sysLog);
@@ -39,13 +39,13 @@ public class Program
int activePort = Bootstrapper.ScanForAvailablePort(config, sysLog); int activePort = Bootstrapper.ScanForAvailablePort(config, sysLog);
if (activePort == -1) if (activePort == -1)
{ {
sysLog.Fatal("💀 无法启动:配置范围内无可用端口"); sysLog.Fatal("[Core] 💀 无法启动:配置范围内无可用端口");
Bootstrapper.Shutdown("无法启动:配置范围内无可用端口", exitCode: 1); Bootstrapper.Shutdown("无法启动:配置范围内无可用端口", exitCode: 1);
return; return;
} }
config.UpdateActualPort(activePort); // 回填端口 config.UpdateActualPort(activePort); // 回填端口
// 具体的 gRPC 链接逻辑封装在 Bootstrapper 中,保持 Main 清爽但逻辑可见 // 具体的 gRpc 链接逻辑封装在 Bootstrapper 中,保持 Main 清爽但逻辑可见
await Bootstrapper.RegisterToGatewayAsync(config); await Bootstrapper.RegisterToGatewayAsync(config);
// ============================================================= // =============================================================
@@ -53,7 +53,7 @@ public class Program
// ============================================================= // =============================================================
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// ★ 核心改动:一行代码注册所有业务 (SDK, Workers, gRPC, 视频流) // ★ 核心改动:一行代码注册所有业务 (SDK, Workers, gRpc, 视频流)
builder.Services.AddCameraBusinessServices(config, sysLog); builder.Services.AddCameraBusinessServices(config, sysLog);
// ★ 核心改动:注册 Web 基础 (Controller, Swagger, Cors) // ★ 核心改动:注册 Web 基础 (Controller, Swagger, Cors)
@@ -72,7 +72,7 @@ public class Program
// 启动监听 // 启动监听
string url = $"http://0.0.0.0:{config.BasePort}"; string url = $"http://0.0.0.0:{config.BasePort}";
sysLog.Information($"🚀 [WebApi] 服务启动,监听: {url}"); sysLog.Information($"[WebApi] 🚀 服务启动,监听: {url}");
await app.RunAsync(url); await app.RunAsync(url);
} }
@@ -90,7 +90,9 @@ public class Program
_ = app.Services.GetRequiredService<ConnectivitySentinel>(); _ = app.Services.GetRequiredService<ConnectivitySentinel>();
await manager.StartAsync(); await manager.StartAsync();
Console.WriteLine("✅[System] 核心业务逻辑已激活。");
var sysLog = Log.ForContext("SourceContext", LogModules.Core);
sysLog.Information($"[Core] 🚀 核心业务逻辑已激活, 设备管理器已就绪.");
} }
} }

View File

@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; // 用于泛型 ILogger<>
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using SHH.CameraSdk; 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) 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.AddSingleton<IEnumerable<StreamTarget>>(netTargets);
services.AddHostedService<ImageMonitorController>(); services.AddHostedService<ImageMonitorController>();
@@ -96,7 +98,7 @@ public static class ServiceCollectionExtensions
{ {
// 注意:这里需要使用 Microsoft.Extensions.Logging.ILogger 来适配构造函数 // 注意:这里需要使用 Microsoft.Extensions.Logging.ILogger 来适配构造函数
services.AddHostedService(sp => services.AddHostedService(sp =>
new GrpcSenderWorker(target, sp.GetRequiredService<ILogger<GrpcSenderWorker>>())); new GrpcSenderWorker(target));
} }
} }