新增 Mjpegplayer 用来播放 Web 流
This commit is contained in:
120
SHH.MjpegPlayer/Core/Extensions/NetHttpExtension.cs
Normal file
120
SHH.MjpegPlayer/Core/Extensions/NetHttpExtension.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
using Ayay.SerilogLogs;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
using System.Text;
|
||||
|
||||
namespace SHH.MjpegPlayer;
|
||||
|
||||
/// <summary>
|
||||
/// 扩展 HttpClient 的 PostJson 方法,用于发送 JSON 格式的数据
|
||||
/// </summary>
|
||||
public static class NetHttpExtension
|
||||
{
|
||||
// Optimized: 统一日志对象
|
||||
private static readonly ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
||||
|
||||
// Optimized: 使用静态单例 HttpClient 防止套接字耗尽。注意:生产环境建议配合 SocketsHttpHandler
|
||||
private static readonly HttpClient _httpClient = new HttpClient();
|
||||
|
||||
#region 同步方法 (Sync-over-Async, 谨慎使用)
|
||||
|
||||
/// <summary>
|
||||
/// 发送 JSON 格式的 POST 请求 (同步)
|
||||
/// </summary>
|
||||
public static string PostJson(this object jsonData, string url, int timeout = 2000)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Optimized: 显式调用异步版本并等待,注意在某些上下文可能死锁
|
||||
return PostJsonAsync(jsonData, url, timeout).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sysLog.Error(ex, "Post 同步请求异常: {Url}", url);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送 JSON 格式的 POST 请求并反序列化 (同步)
|
||||
/// </summary>
|
||||
public static T? PostJson<T>(this object jsonData, string url, int timeout = 2000)
|
||||
{
|
||||
try
|
||||
{
|
||||
var msg = PostJson(jsonData, url, timeout);
|
||||
return string.IsNullOrWhiteSpace(msg) ? default : JsonConvert.DeserializeObject<T>(msg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sysLog.Error(ex, "Post 同步请求并解析 JSON 异常: {Url}", url);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 异步方法 (推荐使用)
|
||||
|
||||
/// <summary>
|
||||
/// 发送 JSON 格式的 POST 请求 (异步)
|
||||
/// </summary>
|
||||
/// <param name="jsonData">要发送的对象</param>
|
||||
/// <param name="url">目标地址</param>
|
||||
/// <param name="timeout">超时(ms)</param>
|
||||
public static async Task<string> PostJsonAsync(this object jsonData, string url, int timeout = 2000)
|
||||
{
|
||||
string jsonString = string.Empty;
|
||||
try
|
||||
{
|
||||
// Optimized: 序列化处理
|
||||
jsonString = jsonData is string s ? s : JsonConvert.SerializeObject(jsonData);
|
||||
using var content = new StringContent(jsonString, Encoding.UTF8, "application/json");
|
||||
|
||||
// Optimized: 设置请求级别的超时处理(HttpClient.Timeout 是全局的,此处利用 CancellationTokenSource)
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeout));
|
||||
|
||||
var response = await _httpClient.PostAsync(url, content, cts.Token);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
_sysLog.Warning("Post 请求状态异常: {Url}, StatusCode: {Code}", url, response.StatusCode);
|
||||
return string.Empty;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_sysLog.Warning("Post 请求超时: {Url}, Timeout: {Timeout}ms", url, timeout);
|
||||
return string.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Modified: 使用结构化日志记录错误
|
||||
_sysLog.Error(ex, "Post 异步请求发生故障: {Url}", url);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送 JSON 格式的 POST 请求并反序列化 (异步)
|
||||
/// </summary>
|
||||
public static async Task<T?> PostJsonAsync<T>(this object jsonData, string url, int timeout = 2000)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await PostJsonAsync(jsonData, url, timeout);
|
||||
if (string.IsNullOrWhiteSpace(result)) return default;
|
||||
|
||||
return JsonConvert.DeserializeObject<T>(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sysLog.Error(ex, "Post 异步请求解析 JSON 失败: {Url}", url);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
190
SHH.MjpegPlayer/Core/Extensions/NetPortExtension.cs
Normal file
190
SHH.MjpegPlayer/Core/Extensions/NetPortExtension.cs
Normal file
@@ -0,0 +1,190 @@
|
||||
using Ayay.SerilogLogs;
|
||||
using Serilog;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SHH.MjpegPlayer
|
||||
{
|
||||
/// <summary>
|
||||
/// 网口占用检测
|
||||
/// </summary>
|
||||
public static class NetPortExtension
|
||||
{
|
||||
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
||||
|
||||
#region IsServerPort
|
||||
|
||||
/// <summary>
|
||||
/// 是否端口
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsServerPort(this int value)
|
||||
{
|
||||
if (value > 0 && value < 65535)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsPortOccupied
|
||||
|
||||
/// <summary>
|
||||
/// 端口占用检测
|
||||
/// </summary>
|
||||
/// <param name="port"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsPortOccupied(this int port)
|
||||
{
|
||||
var ipProperties = IPGlobalProperties.GetIPGlobalProperties();
|
||||
IPEndPoint[] activeListeners = ipProperties.GetActiveTcpListeners();
|
||||
foreach (var endPoint in activeListeners)
|
||||
{
|
||||
if (endPoint.Port == port)
|
||||
return true; // 端口被占用
|
||||
}
|
||||
|
||||
return false; // 端口可用
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetProcessIdByPort
|
||||
|
||||
/// <summary>
|
||||
/// 查询端口占用进程 Pid
|
||||
/// </summary>
|
||||
/// <param name="port"></param>
|
||||
/// <returns></returns>
|
||||
public static int GetProcessIdByPort(this int port)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (Process proc = new Process())
|
||||
{
|
||||
proc.StartInfo.FileName = "cmd.exe";
|
||||
proc.StartInfo.Arguments = $"/c netstat -ano | findstr :{port}";
|
||||
proc.StartInfo.UseShellExecute = false;
|
||||
proc.StartInfo.RedirectStandardOutput = true;
|
||||
proc.StartInfo.CreateNoWindow = true;
|
||||
|
||||
proc.Start();
|
||||
string output = proc.StandardOutput.ReadToEnd();
|
||||
proc.WaitForExit();
|
||||
|
||||
// 解析输出(示例:TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 1234)
|
||||
Match match = Regex.Match(output, @":\d+\s+.*?LISTENING\s+(\d+)");
|
||||
if (match.Success && int.TryParse(match.Groups[1].Value, out int pid))
|
||||
return pid;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sysLog.Warning("查询端口占用进程出错", ex.Message, ex.StackTrace);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetProcessIdByPort
|
||||
|
||||
/// <summary>
|
||||
/// 查询端口占用进程 Pid
|
||||
/// </summary>
|
||||
/// <param name="port"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetProcessNameIdByPort(this int port)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (Process proc = new Process())
|
||||
{
|
||||
proc.StartInfo.FileName = "cmd.exe";
|
||||
proc.StartInfo.Arguments = $"/c netstat -ano | findstr :{port}";
|
||||
proc.StartInfo.UseShellExecute = false;
|
||||
proc.StartInfo.RedirectStandardOutput = true;
|
||||
proc.StartInfo.CreateNoWindow = true;
|
||||
|
||||
proc.Start();
|
||||
string output = proc.StandardOutput.ReadToEnd();
|
||||
proc.WaitForExit();
|
||||
|
||||
// 解析输出(示例:TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 1234)
|
||||
Match match = Regex.Match(output, @":\d+\s+.*?LISTENING\s+(\d+)");
|
||||
if (match.Success && int.TryParse(match.Groups[1].Value, out int pid))
|
||||
{
|
||||
using (Process process = Process.GetProcessById(pid))
|
||||
return process.ProcessName;
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sysLog.Warning($"查询端口占用进程出错, 错误信息:{ex.Message} {ex.StackTrace}");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PortOccupiedProc
|
||||
|
||||
/// <summary>
|
||||
/// 端口占用检测并杀掉进程
|
||||
/// </summary>
|
||||
/// <param name="port"></param>
|
||||
/// <returns>返回占用端口清理结果</returns>
|
||||
public static bool PortOccupiedProc(this int port)
|
||||
{
|
||||
if (port.IsPortOccupied())
|
||||
{
|
||||
_sysLog.Warning("服务器端口被占用, Port: {port}");
|
||||
|
||||
// 等待 5 秒
|
||||
for (var i = 0; i < 10; i++)
|
||||
Thread.Sleep(500);
|
||||
|
||||
// 查找占用端口的进程
|
||||
var pid = port.GetProcessIdByPort();
|
||||
if (pid != 0)
|
||||
{
|
||||
// 获取进程名
|
||||
string procName = pid.GetProcessName();
|
||||
|
||||
// 找到占用端口的进程
|
||||
_sysLog.Warning($"找到占用端口进程 Pid: {pid} 进程名:{procName}, 5 秒后即将尝试杀掉占用端口的进程.");
|
||||
|
||||
// 等待 5 秒
|
||||
for (var i = 0; i < 10; i++)
|
||||
Thread.Sleep(500);
|
||||
|
||||
// 杀掉指定进程
|
||||
if (!pid.KillProcessByPid(procName))
|
||||
{
|
||||
// 退出应用
|
||||
return false;
|
||||
}
|
||||
|
||||
// 等待 2 秒
|
||||
Thread.Sleep(2000);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
197
SHH.MjpegPlayer/Core/Extensions/ProcessExtension.cs
Normal file
197
SHH.MjpegPlayer/Core/Extensions/ProcessExtension.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using Ayay.SerilogLogs;
|
||||
using Serilog;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SHH.MjpegPlayer
|
||||
{
|
||||
/// <summary>
|
||||
/// 进程扩展
|
||||
/// </summary>
|
||||
public static class ProcessExtension
|
||||
{
|
||||
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
||||
|
||||
#region GetProcessName
|
||||
|
||||
/// <summary>
|
||||
/// 获取进程名称
|
||||
/// </summary>
|
||||
/// <param name="pid"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetProcessName(this int pid)
|
||||
{
|
||||
try
|
||||
{
|
||||
var process = Process.GetProcessById(pid);
|
||||
return process.ProcessName;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sysLog.Error(ex, "查询进程名出错, Pid: {Pid}", pid);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region KillProcessByPid
|
||||
|
||||
/// <summary>
|
||||
/// 杀掉进程
|
||||
/// </summary>
|
||||
/// <param name="pid"></param>
|
||||
/// <param name="procName"></param>
|
||||
/// <returns></returns>
|
||||
public static bool KillProcessByPid(this int pid, string procName = "")
|
||||
{
|
||||
try
|
||||
{
|
||||
var process = Process.GetProcessById(pid);
|
||||
|
||||
if (process != null)
|
||||
{
|
||||
procName = process.ProcessName;
|
||||
process.Kill();
|
||||
|
||||
_sysLog.Warning("拒绝停止高权限系统进程: {Pid} - {Name}", pid, process.ProcessName);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 找不到 ID 对应的进程,应该是进异常不会进这里
|
||||
_sysLog.Information("成功杀掉进程 - Pid: {Pid}", pid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
_sysLog.Warning("杀掉进程失败,Pid: {Pid} 不存在", pid);
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sysLog.Error(ex, "杀掉进程异常, Pid: {Pid}", pid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region KillProcessByName
|
||||
|
||||
/// <summary>
|
||||
/// 杀掉进程
|
||||
/// </summary>
|
||||
/// <param name="pid"></param>
|
||||
/// <param name="procName"></param>
|
||||
/// <returns></returns>
|
||||
public static int KillProcessByName(this string procName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(procName)) return 0;
|
||||
|
||||
int killCount = 0;
|
||||
try
|
||||
{
|
||||
var processes = Process.GetProcessesByName(procName);
|
||||
foreach (var proc in processes)
|
||||
{
|
||||
using (proc) // Optimized: 确保 Process 资源被释放
|
||||
{
|
||||
try
|
||||
{
|
||||
if (proc.IsHighPrivilegeProcess()) continue;
|
||||
|
||||
int currentId = proc.Id;
|
||||
proc.Kill();
|
||||
killCount++;
|
||||
_sysLog.Information("成功通过名称杀掉进程 - Pid: {Pid}, Name: {Name}", currentId, procName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sysLog.Error(ex, "通过名称杀掉单个进程失败: {Name}", procName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return killCount;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sysLog.Error(ex, "通过名称杀掉进程列表异常: {Name}", procName);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region StartProcess
|
||||
|
||||
/// <summary>
|
||||
/// 开启进程
|
||||
/// </summary>
|
||||
/// <param name="procPath"></param>
|
||||
public static bool StartProcess(this string procPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(procPath))
|
||||
{
|
||||
_sysLog.Error("启动进程失败,路径不存在: {Path}", procPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Optimized: 显式记录启动行为
|
||||
var process = Process.Start(procPath);
|
||||
if (process != null)
|
||||
{
|
||||
_sysLog.Information("进程启动成功: {Path}, Pid: {Pid}", procPath, process.Id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sysLog.Error(ex, "启动进程异常: {Path}", procPath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsHighPrivilegeProcess
|
||||
|
||||
/// <summary>
|
||||
/// 检测是否高权限等级
|
||||
/// </summary>
|
||||
/// <param name="proc"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsHighPrivilegeProcess(this Process proc)
|
||||
{
|
||||
// 典型的高权限进程列表(可根据实际需求扩展)
|
||||
string[] highPrivilegeProcesses = new[] {
|
||||
"System", "smss.exe", "csrss.exe", "wininit.exe", "services.exe",
|
||||
"lsass.exe", "winlogon.exe", "spoolsv.exe", "svchost.exe",
|
||||
"csrss", "msedge"
|
||||
};
|
||||
|
||||
// 检查进程名称是否在高权限列表中
|
||||
foreach (string name in highPrivilegeProcesses)
|
||||
{
|
||||
if (proc.ProcessName.Equals(name, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查进程是否属于系统会话(Session 0)
|
||||
try
|
||||
{
|
||||
return proc.SessionId == 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 如果无法获取 SessionId,保守返回 true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
61
SHH.MjpegPlayer/Core/ImageChannel.cs
Normal file
61
SHH.MjpegPlayer/Core/ImageChannel.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
namespace SHH.MjpegPlayer
|
||||
{
|
||||
/// <summary>图片通道</summary>
|
||||
public class ImageChannel
|
||||
{
|
||||
/// <summary>进程 ID</summary>
|
||||
public Int32 ProcId { get; set; }
|
||||
|
||||
/// <summary>设备 ID</summary>
|
||||
public Int64 DeviceId { get; set; }
|
||||
|
||||
/// <summary>设备 IP</summary>
|
||||
public string IpAddress { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>名称</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>类型</summary>
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>图像宽度</summary>
|
||||
public int ImageWidth { get; set; }
|
||||
|
||||
/// <summary>图像高度</summary>
|
||||
public int ImageHeight { get; set; }
|
||||
|
||||
/// <summary>更新时间</summary>
|
||||
public DateTime UpdateTime { get; set; }
|
||||
|
||||
/// <summary>是否正在播放</summary>
|
||||
public bool IsPlaying { get; set; }
|
||||
|
||||
/// <summary>是否需要推流到 Rtmp 服务器</summary>
|
||||
public bool UseRtmp { get; set; } = true;
|
||||
|
||||
#region RtmpUri
|
||||
|
||||
private string _rtmpUri = string.Empty;
|
||||
|
||||
/// <summary>Rtmp 推流地址</summary>
|
||||
public string RtmpUri
|
||||
{
|
||||
get => _rtmpUri;
|
||||
set
|
||||
{
|
||||
if (_rtmpUri == value)
|
||||
return;
|
||||
_rtmpUri = value;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TestUri
|
||||
|
||||
/// <summary>测试地址</summary>
|
||||
public string TestUri => $"?id={DeviceId}&typeCode={Type}";
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
72
SHH.MjpegPlayer/Core/ImageChannels.cs
Normal file
72
SHH.MjpegPlayer/Core/ImageChannels.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using Core.WcfProtocol;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace SHH.MjpegPlayer
|
||||
{
|
||||
/// <summary>图片通道集合</summary>
|
||||
public class ImageChannels
|
||||
{
|
||||
#region Channels
|
||||
|
||||
/// <summary>
|
||||
/// 通道信息 (线程安全版本)
|
||||
/// </summary>
|
||||
// [修复] 使用 ConcurrentDictionary 替代 Dictionary,防止多线程读写(如推流和接收图片同时进行)时崩溃
|
||||
public ConcurrentDictionary<string, ImageChannel> Channels { get; set; }
|
||||
= new ConcurrentDictionary<string, ImageChannel>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Do
|
||||
|
||||
/// <summary>
|
||||
/// 处置图片
|
||||
/// </summary>
|
||||
/// <param name="req"></param>
|
||||
/// <param name="key"></param>
|
||||
public ImageChannel? Do(UploadImageRequest req, string key)
|
||||
{
|
||||
// [修复] 使用 GetOrAdd 原子操作,无需 lock,彻底解决并发冲突
|
||||
// 如果 key 不存在,则创建新通道;如果存在,则返回现有通道
|
||||
var chn = Channels.GetOrAdd(key, k => new ImageChannel
|
||||
{
|
||||
DeviceId = req.Id,
|
||||
Name = req.Name,
|
||||
Type = req.Type,
|
||||
});
|
||||
|
||||
// 更新指定信息 (直接属性赋值是原子性的,无需锁)
|
||||
chn.IpAddress = req.IpAddress;
|
||||
chn.ProcId = req.ProcId;
|
||||
chn.ImageWidth = req.ImageWidth;
|
||||
chn.ImageHeight = req.ImageHeight;
|
||||
chn.UpdateTime = req.Time;
|
||||
|
||||
return chn;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Get
|
||||
|
||||
/// <summary>
|
||||
/// 获取通道信息
|
||||
/// </summary>
|
||||
/// <param name="deviceId"></param>
|
||||
/// <param name="aiTypeCode"></param>
|
||||
/// <returns></returns>
|
||||
public ImageChannel? Get(string deviceId, string aiTypeCode)
|
||||
{
|
||||
string key = $"{deviceId}#{aiTypeCode}";
|
||||
|
||||
// [修复] ConcurrentDictionary 读取原本就是线程安全的
|
||||
if (Channels.TryGetValue(key, out var val))
|
||||
{
|
||||
return val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
94
SHH.MjpegPlayer/Core/JsonConfig.cs
Normal file
94
SHH.MjpegPlayer/Core/JsonConfig.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace SHH.MjpegPlayer;
|
||||
|
||||
/// <summary>
|
||||
/// Json 配置文件
|
||||
/// </summary>
|
||||
public class JsonConfig
|
||||
{
|
||||
#region Load
|
||||
|
||||
/// <summary>
|
||||
/// 加载配置
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static T? Load<T>(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newPath = $"{Environment.CurrentDirectory}\\{path}";
|
||||
path = newPath.Replace("Res\\Plugins\\", "");
|
||||
var sr = new StreamReader(path);
|
||||
var data = sr.ReadToEnd();
|
||||
sr.Close();
|
||||
sr = null;
|
||||
|
||||
data = data.Replace(@"""$schema"": ""https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json"",", "");
|
||||
|
||||
var obj = JsonConvert.DeserializeObject<T>(data);
|
||||
//Logs.LogInformation<JsonConfig>(EIdFiles.LoadSucceed,
|
||||
// $"配置{EIdFiles.LoadSucceed.GetDescription()}, Path:{path} 类型:{typeof(T).FullName}.");
|
||||
return obj;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Logs.LogWarning<JsonConfig>(EIdFiles.LoadFailed,
|
||||
// $"配置{EIdFiles.LoadSucceed.GetDescription()}, Path:{path} 类型:{typeof(T).FullName}.", ex.Message, ex.StackTrace);
|
||||
return default(T);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Save
|
||||
|
||||
/// <summary>
|
||||
/// 保存配置
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="caption"></param>
|
||||
/// <returns></returns>
|
||||
public static bool Save(object obj, string path, string caption)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newPath = Path.GetFullPath(path);
|
||||
if (File.Exists(newPath))
|
||||
File.Delete(newPath);
|
||||
|
||||
var loc = newPath.LastIndexOf("\\");
|
||||
if (loc > 0)
|
||||
{
|
||||
var newDir = newPath.Substring(0, loc);
|
||||
Directory.CreateDirectory(newDir);
|
||||
}
|
||||
|
||||
var msg = JsonConvert.SerializeObject(obj, Formatting.Indented);
|
||||
msg = msg.Insert(1, "\"$schema\": \"https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json\",\r\n");
|
||||
|
||||
var sw = new StreamWriter(newPath);
|
||||
sw.Write(msg);
|
||||
sw.Flush();
|
||||
sw.Close();
|
||||
sw = null;
|
||||
|
||||
//Logs.LogInformation<JsonConfig>(EIdFiles.SaveSucceed,
|
||||
// $"配置{EIdFiles.SaveSucceed.GetDescription()}, Path:{path}\r\n\t\t\tCaption:{caption} 类型:{obj.GetType().FullName}.");
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Logs.LogInformation<JsonConfig>(EIdFiles.SaveFailed,
|
||||
// $"配置{EIdFiles.SaveFailed.GetDescription()}, Path:{path}\r\n\t\t\tCaption:{caption} 类型:{obj.GetType().FullName}.", ex.Message, ex.StackTrace);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
17
SHH.MjpegPlayer/Core/Models/CfgRtmpReply.cs
Normal file
17
SHH.MjpegPlayer/Core/Models/CfgRtmpReply.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace SHH.MjpegPlayer;
|
||||
|
||||
/// <summary>RTMP 配置响应类</summary>
|
||||
public class CfgRtmpReply
|
||||
{
|
||||
/// <summary>响应消息</summary>
|
||||
public string msg { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>响应状态码</summary>
|
||||
public int code { get; set; }
|
||||
|
||||
/// <summary>RTMP 推流地址列表</summary>
|
||||
public RtmpVo[]? rtmpVoList { get; set; }
|
||||
|
||||
/// <summary>是否成功(状态码为 200 时返回 true)</summary>
|
||||
public bool IsSuccess => code == 200;
|
||||
}
|
||||
35
SHH.MjpegPlayer/Core/Models/EIdSys.cs
Normal file
35
SHH.MjpegPlayer/Core/Models/EIdSys.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace SHH.MjpegPlayer
|
||||
{
|
||||
public enum EIdSys
|
||||
{
|
||||
/// <summary>根据PID杀掉进程成功</summary>
|
||||
[Description("根据PID杀掉进程成功")]
|
||||
KillProcByIdSucceed = 1000101,
|
||||
|
||||
/// <summary>按秒统计汇总</summary>
|
||||
[Description("按秒统计汇总")]
|
||||
TotalBySecond = 100701,
|
||||
|
||||
/// <summary>按分钟统计汇总</summary>
|
||||
[Description("按分钟统计汇总")]
|
||||
TotalByMinute = 100702,
|
||||
|
||||
/// <summary>按小时统计汇总</summary>
|
||||
[Description("按小时统计汇总")]
|
||||
TotalByHour = 100703,
|
||||
|
||||
/// <summary>查询进程名出错</summary>
|
||||
[Description("查询进程名出错")]
|
||||
SearchProcNameError = 1000901,
|
||||
|
||||
/// <summary>根据PID杀掉进程出错</summary>
|
||||
[Description("根据PID杀掉进程出错")]
|
||||
KillProcByIdError = 1000902,
|
||||
|
||||
/// <summary>启动进程出错</summary>
|
||||
[Description("启动进程出错")]
|
||||
StartProcessError = 1000903,
|
||||
}
|
||||
}
|
||||
18
SHH.MjpegPlayer/Core/Models/JsonConfigUris.cs
Normal file
18
SHH.MjpegPlayer/Core/Models/JsonConfigUris.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace SHH.MjpegPlayer
|
||||
{
|
||||
public class JsonConfigUris
|
||||
{
|
||||
public static string DispatcherConfig;
|
||||
public static string RtspRtcConfig;
|
||||
public static string RtspRtcPortsConfig;
|
||||
public static string MjpegConfig;
|
||||
public static string CloudServerConfig;
|
||||
public static string CloudServerSessionsConfig;
|
||||
public static string CloudAgentConfig;
|
||||
public static string CloudAITerminalConfig;
|
||||
public static string VirtualCameraConfig;
|
||||
public static string AIMainConfig;
|
||||
public static string AIDbConfig;
|
||||
public static string ToolLogConfig;
|
||||
}
|
||||
}
|
||||
43
SHH.MjpegPlayer/Core/Models/MjpegConfig.cs
Normal file
43
SHH.MjpegPlayer/Core/Models/MjpegConfig.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
namespace SHH.MjpegPlayer;
|
||||
|
||||
/// <summary>
|
||||
/// Mjpeg 配置
|
||||
/// </summary>
|
||||
public class MjpegConfig
|
||||
{
|
||||
/// <summary>Mjpeg 服务 IP 地址</summary>
|
||||
public string SvrMjpegIp
|
||||
= "0.0.0.0";
|
||||
|
||||
/// <summary>Mjpeg 服务端口开始</summary>
|
||||
public int SvrMjpegPortBegin
|
||||
= 25031;
|
||||
|
||||
/// <summary>Mjpeg 服务端口结束</summary>
|
||||
public int SvrMjpegPortEnd
|
||||
= 25300;
|
||||
|
||||
/// <summary>帧间隔, 单位毫秒 (值为 125, 每秒 8 帧)</summary>
|
||||
public int FrameInterval { get; set; }
|
||||
= 125;
|
||||
|
||||
/// <summary>Mjpeg Wcf 接收图片接口</summary>
|
||||
public int WcfPushImagePort
|
||||
= 25030;
|
||||
|
||||
/// <summary>接收图片的服务器名称</summary>
|
||||
public string SvrNamePushImage { get; set; }
|
||||
= "ImageService.svc";
|
||||
|
||||
/// <summary>最大接收数据大小</summary>
|
||||
public int SvrPushImageMaxRecMsgSize { get; set; }
|
||||
= 2000 * 1024 * 1024;
|
||||
|
||||
/// <summary>Rtmp 服务地址</summary>
|
||||
public string RtmpServerDjhUri { get; set; }
|
||||
= "http://172.16.41.108:8889/intellect/nvr/getRtmp";
|
||||
|
||||
/// <summary>是否使用 Rtmp 服务</summary>
|
||||
public bool UseRtmpServer { get; set; }
|
||||
= false;
|
||||
}
|
||||
17
SHH.MjpegPlayer/Core/Models/RtmpVo.cs
Normal file
17
SHH.MjpegPlayer/Core/Models/RtmpVo.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace SHH.MjpegPlayer;
|
||||
|
||||
/// <summary>RTMP 推流对象类</summary>
|
||||
public class RtmpVo
|
||||
{
|
||||
/// <summary>算法代码</summary>
|
||||
public string algCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>设备ID</summary>
|
||||
public string deviceId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>设备IP地址</summary>
|
||||
public string deviceIp { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>RTMP 推流地址</summary>
|
||||
public string rtmp { get; set; } = string.Empty;
|
||||
}
|
||||
100
SHH.MjpegPlayer/Core/Models/SessionInfo.cs
Normal file
100
SHH.MjpegPlayer/Core/Models/SessionInfo.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
namespace SHH.MjpegPlayer
|
||||
{
|
||||
/// <summary>
|
||||
/// 会话信息
|
||||
/// </summary>
|
||||
public class SessionInfo
|
||||
{
|
||||
#region Key
|
||||
|
||||
/// <summary>流标识</summary>
|
||||
public string? Key => $"{DeviceId}#{TypeCode}";
|
||||
|
||||
#endregion
|
||||
|
||||
#region DeviceId
|
||||
|
||||
/// <summary>设备类型</summary>
|
||||
public string? DeviceId { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region TypeCode
|
||||
|
||||
/// <summary>类型编码</summary>
|
||||
public string? TypeCode { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region ClientIp
|
||||
|
||||
/// <summary>客户端 IP</summary>
|
||||
public string? ClientIp { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region ClientPort
|
||||
|
||||
/// <summary>客户端端口</summary>
|
||||
public int ClientPort { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Message
|
||||
|
||||
/// <summary>消息</summary>
|
||||
public string? Message { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region AcceptTime
|
||||
|
||||
/// <summary>接入时间</summary>
|
||||
public DateTime AcceptTime { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Counter
|
||||
|
||||
/// <summary>计数器</summary>
|
||||
public SumByTime? Counter { get; init; }
|
||||
|
||||
#endregion
|
||||
|
||||
// =======================================================
|
||||
// [新增] 专门给诊断大屏用的属性,前端可直接读取数值
|
||||
// =======================================================
|
||||
|
||||
/// <summary>接收帧率 (源头健康度)</summary>
|
||||
public int RecvFps
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Counter == null || Counter.TotalSecond == null) return 0;
|
||||
|
||||
// 从字典中安全获取 "接收帧数"
|
||||
if (Counter.TotalSecond.TryGetValue("接收帧数", out uint val))
|
||||
{
|
||||
return (int)val;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>播放/发送帧率 (客户端健康度)</summary>
|
||||
public int PlayFps
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Counter == null || Counter.TotalSecond == null) return 0;
|
||||
|
||||
// 从字典中安全获取 "播放帧数"
|
||||
if (Counter.TotalSecond.TryGetValue("播放帧数", out uint val))
|
||||
{
|
||||
return (int)val;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
190
SHH.MjpegPlayer/Core/Models/SumByTime.cs
Normal file
190
SHH.MjpegPlayer/Core/Models/SumByTime.cs
Normal file
@@ -0,0 +1,190 @@
|
||||
using System.Text;
|
||||
|
||||
namespace SHH.MjpegPlayer
|
||||
{
|
||||
/// <summary>
|
||||
/// 按时间统计
|
||||
/// </summary>
|
||||
public class SumByTime
|
||||
{
|
||||
#region Defines
|
||||
|
||||
/// <summary>最近刷新在哪一秒</summary>
|
||||
private int LastRefreshSecond = DateTime.Now.Second;
|
||||
|
||||
/// <summary>最近刷新在哪一分钟</summary>
|
||||
private int LastRefreshMinute = DateTime.Now.Minute;
|
||||
|
||||
/// <summary>最近刷新在哪一小时</summary>
|
||||
private int LastRefreshHour = DateTime.Now.Minute;
|
||||
|
||||
/// <summary>秒统计</summary>
|
||||
private Dictionary<string, uint> _second
|
||||
= new Dictionary<string, uint>();
|
||||
|
||||
/// <summary>分钟统计</summary>
|
||||
private Dictionary<string, uint> _minute
|
||||
= new Dictionary<string, uint>();
|
||||
|
||||
/// <summary>小时统计</summary>
|
||||
private Dictionary<string, uint> _hour
|
||||
= new Dictionary<string, uint>();
|
||||
|
||||
/// <summary>累计统计</summary>
|
||||
public Dictionary<string, ulong> All { get; init; }
|
||||
= new Dictionary<string, ulong>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region TotalSecond
|
||||
|
||||
/// <summary>秒统计</summary>
|
||||
public Dictionary<string, uint> TotalSecond { get; init; }
|
||||
= new Dictionary<string, uint>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region TotalMinute
|
||||
|
||||
/// <summary>分统计</summary>
|
||||
public Dictionary<string, uint> TotalMinute { get; init; }
|
||||
= new Dictionary<string, uint>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region TotalHour
|
||||
|
||||
/// <summary>小时统计</summary>
|
||||
public Dictionary<string, uint> TotalHour { get; init; }
|
||||
= new Dictionary<string, uint>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Refresh
|
||||
|
||||
/// <summary>
|
||||
/// 刷新方法调用次数
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="methodName"></param>
|
||||
/// <param name="count"></param>
|
||||
public void Refresh(string methodName, uint count = 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
#region 加入集合
|
||||
|
||||
// 加入集合
|
||||
lock (_second)
|
||||
{
|
||||
if (!_second.ContainsKey(methodName))
|
||||
_second.Add(methodName, 0);
|
||||
}
|
||||
|
||||
// 加入集合
|
||||
lock (_minute)
|
||||
{
|
||||
if (!_minute.ContainsKey(methodName))
|
||||
_minute.Add(methodName, 0);
|
||||
}
|
||||
|
||||
lock (_hour)
|
||||
{
|
||||
if (!_hour.ContainsKey(methodName))
|
||||
_hour.Add(methodName, 0);
|
||||
}
|
||||
|
||||
// 加入集合
|
||||
lock (All)
|
||||
{
|
||||
if (!All.ContainsKey(methodName))
|
||||
All.Add(methodName, 0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 时间变更统计
|
||||
|
||||
// 秒刷新
|
||||
if (!LastRefreshSecond.Equals(DateTime.Now.Second))
|
||||
{
|
||||
LastRefreshSecond = DateTime.Now.Second;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach (var de in _second)
|
||||
{
|
||||
// 更新输出用统计信息
|
||||
if (!TotalSecond.ContainsKey(de.Key))
|
||||
TotalSecond.Add(de.Key, de.Value);
|
||||
else
|
||||
TotalSecond[de.Key] = de.Value;
|
||||
|
||||
sb.Append($"\r\n\t{de.Key} => 执行 {de.Value} 次");
|
||||
_second[de.Key] = 0;
|
||||
}
|
||||
var logMsg = $"统计 => SumBySecond 统计时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm")}{sb.ToString()}";
|
||||
//Logs.LogInformation<SumByTime>(EIdSys.TotalBySecond, logMsg);
|
||||
}
|
||||
|
||||
// 分钟刷新
|
||||
if (!LastRefreshMinute.Equals(DateTime.Now.Minute))
|
||||
{
|
||||
LastRefreshMinute = DateTime.Now.Minute;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach (var de in _minute)
|
||||
{
|
||||
// 更新输出用统计信息
|
||||
if (!TotalMinute.ContainsKey(de.Key))
|
||||
TotalMinute.Add(de.Key, de.Value);
|
||||
else
|
||||
TotalMinute[de.Key] = de.Value;
|
||||
|
||||
sb.Append($"\r\n\t{de.Key} => 执行 {de.Value} 次, 平均每秒 {Math.Round((double)de.Value / 60, 2)} 次");
|
||||
_minute[de.Key] = 0;
|
||||
}
|
||||
var logMsg = $"统计 => SumByMinute 统计时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm")}{sb.ToString()}";
|
||||
//Logs.LogInformation<SumByTime>(EIdSys.TotalByMinute, logMsg);
|
||||
}
|
||||
|
||||
// 小时刷新
|
||||
if (!LastRefreshHour.Equals(DateTime.Now.Hour))
|
||||
{
|
||||
LastRefreshHour = DateTime.Now.Hour;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach (var de in _hour)
|
||||
{
|
||||
// 更新输出用统计信息
|
||||
if (!TotalHour.ContainsKey(de.Key))
|
||||
TotalHour.Add(de.Key, de.Value);
|
||||
else
|
||||
TotalHour[de.Key] = de.Value;
|
||||
|
||||
sb.Append($"\r\n\t{de.Key} => 执行 {de.Value} 次, 平均每秒 {Math.Round((double)de.Value / 60, 2)} 次");
|
||||
_hour[de.Key] = 0;
|
||||
}
|
||||
var logMsg = $"统计 => SumByHour 统计时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm")}{sb.ToString()}";
|
||||
//Logs.LogInformation<SumByTime>(EIdSys.TotalByHour, logMsg);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 数值更新
|
||||
|
||||
_second[methodName] += count;
|
||||
_minute[methodName] += count;
|
||||
_hour[methodName] += count;
|
||||
All[methodName] += count;
|
||||
|
||||
#endregion
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Logs.LogWarning<SumByTime>(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
68
SHH.MjpegPlayer/Core/PrismMsg.cs
Normal file
68
SHH.MjpegPlayer/Core/PrismMsg.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using Prism.Events;
|
||||
|
||||
namespace SHH.MjpegPlayer;
|
||||
|
||||
/// <summary>Prism 消息框架</summary>
|
||||
public class PrismMsg<T>
|
||||
{
|
||||
#region Defines
|
||||
|
||||
public IEventAggregator _ea;
|
||||
|
||||
private static PrismMsg<T>? _instance = null;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
/// <summary>构造函数</summary>
|
||||
private PrismMsg()
|
||||
{
|
||||
_ea = new EventAggregator();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Instance
|
||||
|
||||
/// <summary>获取实例信息</summary>
|
||||
public static PrismMsg<T> Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
_instance = new PrismMsg<T>();
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Publish
|
||||
|
||||
/// <summary>发送消息</summary>
|
||||
public static void Publish(T msg)
|
||||
{
|
||||
if (Instance == null)
|
||||
return;
|
||||
|
||||
dynamic? data = msg;
|
||||
Instance._ea.GetEvent<PubSubEvent<T>>().Publish(data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Subscribe
|
||||
|
||||
/// <summary>订阅消息</summary>
|
||||
public static void Subscribe(Action<T> method)
|
||||
{
|
||||
if (Instance == null || Instance._ea == null)
|
||||
return;
|
||||
|
||||
Instance._ea.GetEvent<PubSubEvent<T>>().Subscribe(method);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
101
SHH.MjpegPlayer/Core/Watchs/MemoryWatchdog.cs
Normal file
101
SHH.MjpegPlayer/Core/Watchs/MemoryWatchdog.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using Ayay.SerilogLogs;
|
||||
using Serilog;
|
||||
using System.Diagnostics;
|
||||
using System.Timers;
|
||||
|
||||
namespace SHH.MjpegPlayer;
|
||||
|
||||
/// <summary>
|
||||
/// 内存监控
|
||||
/// </summary>
|
||||
public static class MemoryWatchdog
|
||||
{
|
||||
private static readonly ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
||||
|
||||
private static System.Timers.Timer? _timer;
|
||||
private static long _thresholdBytes;
|
||||
|
||||
private static ILogger _logger => Log.Logger;
|
||||
|
||||
/// <summary>
|
||||
/// 启动内存监控
|
||||
/// </summary>
|
||||
/// <param name="intervalSeconds">检查间隔(秒),默认60秒</param>
|
||||
/// <param name="limitMB">内存阈值(MB),超过此值自动退出,默认800MB</param>
|
||||
public static void Start(int intervalSeconds = 60, int limitMB = 800)
|
||||
{
|
||||
// 1. 参数安全检查
|
||||
if (intervalSeconds < 1) intervalSeconds = 1; // 至少 1 秒
|
||||
if (limitMB < 100) limitMB = 100; // 至少100MB,防止误杀
|
||||
|
||||
// 2. 转换单位
|
||||
// MB -> Bytes
|
||||
_thresholdBytes = (long)limitMB * 1024 * 1024;
|
||||
|
||||
// 秒 -> 毫秒
|
||||
double intervalMs = intervalSeconds * 1000.0;
|
||||
|
||||
// 3. 初始化定时器
|
||||
Stop(); // 防止重复启动
|
||||
_timer = new System.Timers.Timer(intervalMs);
|
||||
_timer.Elapsed += CheckMemoryUsage;
|
||||
_timer.AutoReset = true; // 循环执行
|
||||
_timer.Start();
|
||||
|
||||
// 可选:记录启动日志
|
||||
if (_logger != null)
|
||||
{
|
||||
_sysLog.Warning($"[系统] 内存看门狗已启动。每 {intervalSeconds} 秒检查一次,阈值: {limitMB} MB.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckMemoryUsage(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
Process currentProc = Process.GetCurrentProcess();
|
||||
|
||||
// 【重要】刷新快照
|
||||
currentProc.Refresh();
|
||||
|
||||
long currentUsage = currentProc.WorkingSet64;
|
||||
|
||||
if (currentUsage > _thresholdBytes)
|
||||
{
|
||||
double currentMB = currentUsage / 1024.0 / 1024.0;
|
||||
double limitMB = _thresholdBytes / 1024.0 / 1024.0;
|
||||
|
||||
if (_logger != null)
|
||||
{
|
||||
_sysLog.Warning($"[严重] 内存占用 ({currentMB:F2} MB) 超过阈值 ({limitMB} MB),程序即将自杀重启或退出.");
|
||||
}
|
||||
|
||||
// 等待日志输出
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
|
||||
// 强制退出
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger != null)
|
||||
{
|
||||
_sysLog.Warning($"[严重] 内存检查出错.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Stop()
|
||||
{
|
||||
if (_timer != null)
|
||||
{
|
||||
_timer.Stop();
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user