新增 Mjpegplayer 用来播放 Web 流

This commit is contained in:
2026-01-21 19:03:59 +08:00
parent f79cb6e74d
commit c438edfa0d
71 changed files with 4538 additions and 452 deletions

View File

@@ -0,0 +1,75 @@
using Ayay.SerilogLogs;
using Core.WcfProtocol;
using Serilog;
namespace SHH.MjpegPlayer
{
/// <summary>
/// CoreImagesService 服务
/// </summary>
public class CoreImagesService : ICoreImagesService
{
private static readonly ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
#region Defines
/// <summary>
/// 按秒统计
/// </summary>
public static SumByTime _sumBySecond = new SumByTime();
#endregion
#region UploadImage
/// <summary>
/// 上传图片
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
public UploadImageReply UploadImage(UploadImageRequest req)
{
var reply = new UploadImageReply();
try
{
// 日志准备
_sumBySecond.Refresh("UploadImage");
PrismMsg<UploadImageRequest>.Publish(req);
}
catch (Exception ex)
{
_sysLog.Warning($"上传图片失败, {ex.Message} {ex.StackTrace}");
reply.ReplyFalt(ex.Message, ex.Source);
}
reply.ReplySuccess();
return reply;
}
#endregion
#region UploadImageOneWay
/// <summary>
/// 上传图片
/// </summary>
/// <param name="req"></param>
public void UploadImageOneWay(UploadImageRequest req)
{
try
{
_sumBySecond.Refresh("UploadImage");
PrismMsg<UploadImageRequest>.Publish(req);
}
catch (Exception ex)
{
_sysLog.Warning($"上传图片失败, {ex.Message} {ex.StackTrace}");
}
}
#endregion
}
}

View File

@@ -0,0 +1,163 @@
using Newtonsoft.Json;
using System.Net.Sockets;
using System.Text;
namespace SHH.MjpegPlayer
{
/// <summary>
/// MJPEG HTTP命令类
/// </summary>
public class MjpegHttpCmd
{
#region DoHttpCmd
public static bool DoHttpCmd(NetworkStream stream,
SessionInfo Info, string Cmd)
{
try
{
switch (Cmd)
{
case "view":
Info.Message = "执行 view 命令.";
DoHttpCmdView(stream, Info);
return true;
case "task":
Info.Message = "执行 task 命令.";
DoHttpCmdTask(stream, Info);
return true;
}
return false;
}
catch (Exception ex)
{
SendJson(stream, $"Command Failed: {ex.Message}", 400);
return true;
}
}
#endregion
#region DoHttpCmdView
private static void DoHttpCmdView(NetworkStream stream, SessionInfo Info)
{
var sessions = new List<SessionInfo>();
int iSessionCount = 0;
var allSessions = MjpegStatics.Sessions.GetAllSessionInfos();
foreach (var sessionInfo in allSessions)
{
if (sessionInfo == null) continue;
if (!string.IsNullOrEmpty(Info.DeviceId))
{
if (!string.IsNullOrEmpty(sessionInfo.DeviceId)
&& !sessionInfo.DeviceId.Equals(Info.DeviceId))
continue;
}
if (!string.IsNullOrEmpty(Info.TypeCode))
{
if (!string.IsNullOrEmpty(sessionInfo.TypeCode)
&& !sessionInfo.TypeCode.Equals(Info.TypeCode))
continue;
}
iSessionCount++;
sessions.Add(sessionInfo);
}
var chns = new List<ImageChannel>();
var imgChns = MjpegStatics.ImageChannels;
int iImgChanCount = 0;
foreach (var kvp in imgChns.Channels)
{
var imgChannel = kvp.Value;
if (imgChannel == null) continue;
if (!string.IsNullOrEmpty(Info.DeviceId))
{
if (!imgChannel.DeviceId.ToString().Equals(Info.DeviceId))
continue;
}
if (!string.IsNullOrEmpty(Info.TypeCode))
{
if (!imgChannel.Type.Equals(Info.TypeCode))
continue;
}
iImgChanCount++;
chns.Add(imgChannel);
}
var result = new
{
webAccessCount = iSessionCount,
deviceChannelCount = iImgChanCount,
webAccessItems = sessions,
deviceChannels = chns
};
SendJson(stream, JsonConvert.SerializeObject(result, Formatting.Indented));
}
#endregion
#region DoHttpCmdTask
private static void DoHttpCmdTask(NetworkStream stream, SessionInfo Info)
{
// [Optimized]: 直接从 TaskManager 获取实时任务快照,避免遍历旧的静态字典
var activeTasks = TaskManager.RunningTasks.Values.ToList();
int iTaskCount = activeTasks.Count;
int iMjpegServerListenCount = activeTasks.Count(t => t.Name.Contains("MjpegServer-"));
var result = new
{
taskCount = iTaskCount,
portListenCount = iMjpegServerListenCount,
// 映射为前端需要的格式
taskItems = activeTasks.Select(t => new { t.Name, t.Type })
};
// 使用 Newtonsoft.Json 或 System.Text.Json 输出
SendJson(stream, JsonConvert.SerializeObject(result, Formatting.Indented));
}
#endregion
#region Helper
private static void SendJson(NetworkStream stream, string json, int code = 200)
{
try
{
string statusLine = code == 200 ? "200 OK" : "400 Bad Request";
// [修复] 添加 CORS 头,允许诊断页面跨域访问
byte[] response = Encoding.UTF8.GetBytes(
$"HTTP/1.1 {statusLine}\r\n" +
"Access-Control-Allow-Origin: *\r\n" +
"Access-Control-Allow-Methods: GET, POST\r\n" +
"Content-Type: application/json; charset=utf-8\r\n" +
$"Content-Length: {Encoding.UTF8.GetByteCount(json)}\r\n\r\n" +
json
);
stream.Write(response, 0, response.Length);
stream.Flush();
stream.Close();
}
catch { }
}
#endregion
}
}

View File

@@ -0,0 +1,28 @@
using Player.MJPEG;
namespace SHH.MjpegPlayer
{
/// <summary>
/// MjpegImagesService 服务
/// </summary>
public class MjpegImagesService : CoreImagesService, IMjpegImagesService
{
#region GetRtspRtcPlayInfo
/// <summary>
/// 获取 RtspRtc 播放信息
/// </summary>
/// <returns></returns>
public MjpegPlayInfoReply GetRtspRtcPlayInfo()
{
var reply = new MjpegPlayInfoReply();
// 发送消息
PrismMsg<MjpegPlayInfoReply>.Publish(reply);
return reply;
}
#endregion
}
}

View File

@@ -0,0 +1,98 @@
using System.Net;
using System.Net.Sockets;
namespace SHH.MjpegPlayer
{
/// <summary>
/// Mjpeg 服务
/// </summary>
public class MjpegServer
{
// [修复] 静态列表管理监听器,支持优雅停止
private static readonly List<TcpListener> _listeners = new List<TcpListener>();
private static readonly object _lock = new object();
/// <summary>
/// 启动服务
/// </summary>
/// <param name="port"></param>
public static void Start(int port)
{
try
{
// 示例:在 MjpegServer 初始化循环中调用
TaskManager.Run($"MjpegServer-{port}", "Network", async (token) =>
{
// [Modified]: 使用 TaskManager 托管,支持外部取消令牌 token
try
{
var cfg = MjpegStatics.Cfg;
IPAddress ipAddress = IPAddress.Any;
if (!string.IsNullOrEmpty(cfg.SvrMjpegIp) && IPAddress.TryParse(cfg.SvrMjpegIp, out var parsedIp))
{
ipAddress = parsedIp;
}
var server = new TcpListener(ipAddress, port);
lock (_lock) _listeners.Add(server);
server.Start();
// Logs.LogInformation...
try
{
// [Modified]: 检查取消令牌和全局运行状态
while (!token.IsCancellationRequested)
{
try
{
// 使用 AcceptTcpClientAsync 的重载或在外部检查
var client = await server.AcceptTcpClientAsync();
if (client == null) continue;
var session = new MjpegSession();
session.Create(client);
}
catch (Exception ex)
{
// [修复] 异常防暴
await Task.Delay(1000, token);
}
}
}
finally
{
// [修复] 任务退出清理
try { server.Stop(); } catch { }
lock (_lock) _listeners.Remove(server);
}
}
catch (Exception ex)
{
// Logs.LogError... 捕获初始化异常
}
});
}
catch (Exception ex)
{
//Logs.LogError<MjpegServer>(ex.Message, ex.StackTrace);
}
}
/// <summary>
/// 停止所有服务 (新增)
/// </summary>
public static void StopAll()
{
lock (_lock)
{
foreach (var server in _listeners)
{
try { server.Stop(); } catch { }
}
_listeners.Clear();
}
}
}
}

View File

@@ -0,0 +1,341 @@
using Ayay.SerilogLogs;
using Core.WcfProtocol;
using Serilog;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace SHH.MjpegPlayer
{
/// <summary>
/// Mjpeg 会话工作单元
/// </summary>
public class MjpegSession : IDisposable
{
private static readonly ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
#region Counter
private SumByTime _sumBySecond = new SumByTime();
/// <summary>
/// 计数器
/// </summary>
public SumByTime Counter => _sumBySecond;
#endregion
#region Info
/// <summary>
/// 基础信息
/// </summary>
public SessionInfo Info { get; private set; }
#endregion
#region Cmd
/// <summary>
/// 命令
/// </summary>
public string? Cmd { get; set; }
#endregion
// [修复] 引入 Disposed 标志位
private volatile bool _isDisposed = false;
#region Constructor
/// <summary>
/// 构造函数
/// </summary>
public MjpegSession()
{
Info = new SessionInfo
{
Counter = _sumBySecond
};
}
#endregion
#region DoHttpHeader
private bool DoHttpHeader(NetworkStream stream)
{
try
{
byte[] buffer = new byte[4096];
int totalBytesRead = 0;
string httpRequest = string.Empty;
while (totalBytesRead < buffer.Length)
{
int bytesRead = stream.Read(buffer, totalBytesRead, buffer.Length - totalBytesRead);
if (bytesRead == 0) return false;
totalBytesRead += bytesRead;
httpRequest = Encoding.ASCII.GetString(buffer, 0, totalBytesRead);
if (httpRequest.Contains("\r\n\r\n")) break;
}
if (!httpRequest.Contains("\r\n\r\n")) return false;
if (!string.IsNullOrEmpty(httpRequest))
{
int queryStartIndex = httpRequest.IndexOf('?');
if (queryStartIndex > -1)
{
int spaceIndex = httpRequest.IndexOf(' ', queryStartIndex);
if (spaceIndex == -1) spaceIndex = httpRequest.Length;
string queryString = httpRequest.Substring(queryStartIndex + 1, spaceIndex - queryStartIndex - 1);
var queryParams = System.Web.HttpUtility.ParseQueryString(queryString);
if (queryParams != null)
{
Info.DeviceId = queryParams["id"];
Info.TypeCode = queryParams["typeCode"];
Cmd = queryParams["cmd"];
}
}
}
if (string.IsNullOrEmpty(Cmd))
{
if (string.IsNullOrEmpty(Info.DeviceId) || string.IsNullOrEmpty(Info.TypeCode))
{
SendErrorResponse(stream, "错误缺少必要参数id 或 typeCode");
return false;
}
}
else
{
if (MjpegHttpCmd.DoHttpCmd(stream, Info, Cmd)) return false;
}
return true;
}
catch (Exception ex)
{
SendErrorResponse(stream, $"解析异常: {ex.Message}");
return false;
}
}
private void SendErrorResponse(NetworkStream stream, string msg)
{
try
{
byte[] response = Encoding.UTF8.GetBytes(
$"HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n{msg}");
stream.Write(response, 0, response.Length);
stream.Flush();
}
catch { }
}
#endregion
#region Create
/// <summary>
/// 创建会话
/// </summary>
public void Create(TcpClient client)
{
try
{
if (Info == null) return;
Info.AcceptTime = DateTime.Now;
// 初始化最近接收时间,避免刚连接就被判定为超时
LastRecImgTime = DateTime.Now;
Task.Run(() => { DoWorkTask(client); });
}
catch (Exception ex)
{
_sysLog.Error($"MjpegSession Create Exception, 异常信息:{ex.Message}, {ex.StackTrace}");
}
}
#endregion
#region DoWorkTask
private void DoWorkTask(TcpClient client)
{
try
{
using (var stream = client.GetStream())
{
// 设置写入超时 3秒
stream.WriteTimeout = 3000;
#region ,
int iLoc = 0;
while (!client.Connected)
{
Thread.Sleep(50);
if (++iLoc > 60) return;
}
try
{
if (client.Client?.RemoteEndPoint is IPEndPoint endpoint)
{
Info.ClientIp = endpoint.Address.ToString();
Info.ClientPort = endpoint.Port;
}
}
catch { }
if (!DoHttpHeader(stream)) return;
#endregion
MjpegStatics.Sessions.AddSession(this);
byte[] header = Encoding.ASCII.GetBytes(
"HTTP/1.1 200 OK\r\n" +
"Content-Type: multipart/x-mixed-replace; boundary=frame\r\n\r\n");
stream.Write(header, 0, header.Length);
var frameInterval = MjpegStatics.Cfg.FrameInterval;
if (frameInterval < 1 || frameInterval > 500) frameInterval = 125;
Stopwatch stopwatch = Stopwatch.StartNew();
UploadImageRequest? lastProcItem = null;
byte[] boundaryBytes = Encoding.ASCII.GetBytes("\r\n--frame\r\nContent-Type: image/jpeg\r\nContent-Length: ");
byte[] doubleNewLine = Encoding.ASCII.GetBytes("\r\n\r\n");
while (client.Connected && !_isDisposed)
{
try
{
// [新增] 僵尸连接熔断机制:如果源头超过 60 秒没有新图片,主动断开连接
if ((DateTime.Now - LastRecImgTime).TotalSeconds > 60)
{
_sysLog.Warning($"会话超时断开 (源头无数据 > 60s): {Info.Key} - Client: {Info.ClientIp}");
break;
}
stopwatch.Restart();
if (_lastRecObj == null || _lastRecObj.ImageBytes == null)
{
Info.Message = "等待图片数据抵达";
Thread.Sleep(40);
continue;
}
Info.Message = "视频流播放中";
if (lastProcItem != null && lastProcItem != _lastRecObj)
{
_sumBySecond.Refresh("有效帧数");
}
lastProcItem = _lastRecObj;
// 如果图片太旧超过3秒认为是滞后数据暂时不发
if ((DateTime.Now - _lastRecObj.Time).TotalSeconds > 3)
{
Thread.Sleep(40);
continue;
}
byte[] imageData = _lastRecObj.ImageBytes;
stream.Write(boundaryBytes, 0, boundaryBytes.Length);
byte[] lengthBytes = Encoding.ASCII.GetBytes(imageData.Length.ToString());
stream.Write(lengthBytes, 0, lengthBytes.Length);
stream.Write(doubleNewLine, 0, doubleNewLine.Length);
stream.Write(imageData, 0, imageData.Length);
_sumBySecond.Refresh("播放帧数");
stopwatch.Stop();
var needSleep = frameInterval - (int)stopwatch.ElapsedMilliseconds;
if (needSleep > 0) Thread.Sleep(needSleep);
}
catch (Exception ex)
{
_sysLog.Warning($"播放连接断开, : {ex.Message}");
break;
}
}
}
}
catch (Exception ex)
{
_sysLog.Error($"异常信息:{ex.Message}, {ex.StackTrace}");
}
finally
{
Dispose();
if (client != null)
{
try { client.Close(); client.Dispose(); } catch { }
}
}
}
#endregion
#region DoImageProc
private UInt64 _lastPlayImageOrder = 0;
private UploadImageRequest? _lastRecObj = null;
/// <summary>
/// 处置图片
/// </summary>
public void DoImageProc(UploadImageRequest req)
{
try
{
LastRecImgTime = DateTime.Now;
_sumBySecond.Refresh("接收帧数");
if (req == null || req.ImageBytes == null || req.ImageBytes.Length < 100) return;
// 防止死锁:序号重置检测
if (req.Order < _lastPlayImageOrder)
{
if (_lastPlayImageOrder > 100 && req.Order < 100)
{
_lastPlayImageOrder = req.Order;
}
else
{
return;
}
}
_lastPlayImageOrder = req.Order;
_lastRecObj = req;
}
catch (Exception ex)
{
_sysLog.Error($"异常信息:{ex.Message}, {ex.StackTrace}");
}
}
/// <summary>
/// 最近收到图片时间
/// </summary>
public DateTime LastRecImgTime { get; set; } = DateTime.MinValue;
#endregion
#region Dispose
public void Dispose()
{
if (_isDisposed) return;
_isDisposed = true;
MjpegStatics.Sessions.RemoveSession(this);
}
#endregion
}
}

View File

@@ -0,0 +1,116 @@
using Core.WcfProtocol;
using System.Collections.Concurrent;
namespace SHH.MjpegPlayer;
/// <summary>
/// 服务器会话集合 (线程安全重构版)
/// </summary>
public class MjpegSessions
{
// 核心改变使用字典建立索引Key = DeviceId#TypeCode
// 这样可以将查找特定摄像头的复杂度从 O(N) 降低到 O(1)
private readonly ConcurrentDictionary<string, List<MjpegSession>> _sessionMap
= new ConcurrentDictionary<string, List<MjpegSession>>();
/// <summary>
/// 构造函数
/// </summary>
public MjpegSessions()
{
PrismMsg<UploadImageRequest>.Subscribe(ProcUploadImageRequest);
}
/// <summary>
/// 优化后的图片分发逻辑 (解决 O(N) 和 线程安全问题)
/// </summary>
/// <param name="req"></param>
public void ProcUploadImageRequest(UploadImageRequest req)
{
try
{
string key = $"{req.Id}#{req.Type}";
// 1. 更新通道信息 (ImageChannels 现已是线程安全的)
var chn = MjpegStatics.ImageChannels.Do(req, key);
// 2. O(1) 快速查找关注该流的会话列表
if (_sessionMap.TryGetValue(key, out var targetSessions))
{
// 必须加锁,防止遍历时 List 被其他线程(如 AddSession/RemoveSession) 修改
lock (targetSessions)
{
// 倒序遍历,方便在需要时移除失效会话
for (var i = targetSessions.Count - 1; i >= 0; i--)
{
var session = targetSessions[i];
session.DoImageProc(req);
}
}
if (chn != null) chn.IsPlaying = targetSessions.Count > 0;
}
else
{
if (chn != null) chn.IsPlaying = false;
}
}
catch (Exception ex)
{
//Logs.LogWarning<MjpegSessions>(ex.Message, ex.StackTrace);
}
}
/// <summary>
/// 添加会话
/// </summary>
/// <param name="session"></param>
public void AddSession(MjpegSession session)
{
if (session?.Info?.Key == null) return;
// 使用 GetOrAdd 确保线程安全地获取或创建 List
var list = _sessionMap.GetOrAdd(session.Info.Key, _ => new List<MjpegSession>());
lock (list)
{
list.Add(session);
}
}
/// <summary>
/// 移除会话
/// </summary>
/// <param name="session"></param>
public void RemoveSession(MjpegSession session)
{
if (session?.Info?.Key == null) return;
if (_sessionMap.TryGetValue(session.Info.Key, out var list))
{
lock (list)
{
list.Remove(session);
}
}
}
/// <summary>
/// 获取当前所有会话信息的快照 (用于 HTTP API 统计与展示)
/// [新增] 此方法替代旧版直接访问 Sessions 列表,防止 HTTP 线程与 MJPEG 线程发生冲突
/// </summary>
public List<SessionInfo> GetAllSessionInfos()
{
var result = new List<SessionInfo>();
// 遍历字典,线程安全地收集所有 Info
foreach (var kvp in _sessionMap)
{
// 对内部 List 加锁,确保复制过程不被打断
lock (kvp.Value)
{
result.AddRange(kvp.Value.Select(s => s.Info));
}
}
return result;
}
}

View File

@@ -0,0 +1,163 @@
using Ayay.SerilogLogs;
using Serilog;
namespace SHH.MjpegPlayer
{
/// <summary>
/// RTMP 推流参数同步服务器
/// 职责:定期将本地图片通道信息同步至流媒体服务器,并获取最新的 RTMP 推流地址。
/// </summary>
public class RtmpPushServer
{
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
#region Instance
/// <summary>
/// 获取 RTMP 推流处理器的全局单例实例
/// </summary>
public static RtmpPushServer Instance { get; } = new RtmpPushServer();
// 私有构造函数防止外部 new
private RtmpPushServer() { }
#endregion
#region Start
/// <summary>
/// 启动 RTMP 推流任务 (对接新架构 TaskManager)
/// </summary>
public void Start()
{
// Optimized: 使用 TaskManager.Run 替代旧的线程启动方式,实现任务可视化管理
TaskManager.Run("RtmpPushSync", "Monitor", async (token) =>
{
try
{
_sysLog.Information("RTMP 推流同步任务正在启动...");
// 1. 初始化延迟:稍作延迟,等待系统其他组件初始化完成
await Task.Delay(2000, token);
// 2. 配置校验
if (!MjpegStatics.Cfg.UseRtmpServer)
{
_sysLog.Warning("配置未启用 RTMP 服务,推流任务已跳过");
return;
}
#region
bool isConnected = false;
while (!isConnected && !token.IsCancellationRequested)
{
try
{
var cfg = MjpegStatics.Cfg;
var testItems = new List<object> { new { deviceIp = "", deviceId = "", algCode = "" } };
var result = testItems.PostJson<CfgRtmpReply>(cfg.RtmpServerDjhUri);
if (result != null && result.IsSuccess)
{
_sysLog.Information("流媒体服务接口检测通过: {Uri}", cfg.RtmpServerDjhUri);
isConnected = true;
}
else
{
_sysLog.Warning("检测流媒体服务接口失败, 30秒后再试... Uri: {Uri}", cfg.RtmpServerDjhUri);
await Task.Delay(30000, token);
}
}
catch (Exception ex)
{
_sysLog.Error(ex, "流媒体服务接口检测异常");
await Task.Delay(5000, token);
}
}
#endregion
#region ( OnDoTaskAction)
while (!token.IsCancellationRequested)
{
// 执行业务同步逻辑
await SyncRtmpParametersAsync(token);
// 每 50ms 执行一次 (对应原 Start 中的 50ms 频率)
await Task.Delay(50, token);
}
#endregion
}
catch (OperationCanceledException)
{
_sysLog.Information("RTMP 推流同步任务已正常取消");
}
catch (Exception ex)
{
_sysLog.Fatal(ex, "RTMP 推流任务发生致命错误");
}
});
}
#endregion
#region
private async Task SyncRtmpParametersAsync(CancellationToken token)
{
try
{
var cfg = MjpegStatics.Cfg;
var imgChns = MjpegStatics.ImageChannels;
var pushItems = new List<object>();
// 构建上报数据
foreach (var kvp in imgChns.Channels)
{
if (token.IsCancellationRequested) return;
var imgChn = kvp.Value;
if (imgChn == null || !imgChn.UseRtmp) continue;
pushItems.Add(new
{
deviceIp = string.IsNullOrEmpty(imgChn.IpAddress) ? "127.0.0.1" : imgChn.IpAddress,
deviceId = imgChn.DeviceId.ToString(),
algCode = imgChn.Type
});
}
if (pushItems.Count > 0)
{
// 使用 await 配合异步扩展,释放线程池线程
// 如果你的 NetHttpExtension 已支持异步,则直接 await。
// 否则使用 await Task.Run(() => pushItems.PostJson(...)) 暂时过渡。
var result = await pushItems.PostJsonAsync<CfgRtmpReply>(cfg.RtmpServerDjhUri, 2000);
if (result?.rtmpVoList != null && result.IsSuccess)
{
foreach (var item in result.rtmpVoList)
{
var channel = imgChns.Get(item.deviceId, item.algCode);
if (channel != null && channel.RtmpUri != item.rtmp)
{
// 假设后续我们会统计实例中的成功次数
channel.RtmpUri = item.rtmp;
}
}
}
}
}
catch (Exception ex)
{
// 使用统一的 Serilog 对象输出结构化日志
_sysLog.Error(ex, "SyncRtmpParametersAsync 执行异常");
}
}
#endregion
}
}

View File

@@ -0,0 +1,48 @@
using System.Collections.Concurrent;
namespace SHH.MjpegPlayer;
/// <summary>
/// 任务状态信息载荷
/// </summary>
public record TaskMetadata(string Name, string Type, DateTime StartTime);
/// <summary>
/// 任务管理器:替代原 CoreTaskRun 功能
/// 职责:记录运行中的异步任务,支持状态检索和统一取消
/// </summary>
public static class TaskManager
{
// 存储运行中的任务及其元数据
public static readonly ConcurrentDictionary<string, TaskMetadata> RunningTasks = new();
// 存储取消令牌,用于停止特定任务
private static readonly ConcurrentDictionary<string, CancellationTokenSource> _tokens = new();
/// <summary>
/// 注册并运行一个受控任务
/// </summary>
public static void Run(string taskName, string taskType, Func<CancellationToken, Task> action)
{
var cts = new CancellationTokenSource();
_tokens[taskName] = cts;
var metadata = new TaskMetadata(taskName, taskType, DateTime.Now);
RunningTasks[taskName] = metadata;
// 启动异步任务
Task.Run(async () =>
{
try
{
await action(cts.Token);
}
finally
{
// Optimized: 任务结束(无论正常还是异常)必须清理资源
RunningTasks.TryRemove(taskName, out _);
_tokens.TryRemove(taskName, out _);
}
}, cts.Token);
}
}