289 lines
8.6 KiB
C#
289 lines
8.6 KiB
C#
using Microsoft.AspNetCore.SignalR;
|
|
using System.Collections.Concurrent;
|
|
using DotNext;
|
|
using Common;
|
|
using server.Hubs;
|
|
|
|
namespace server.Services;
|
|
|
|
public class ProgressReporter : ProgressInfo, IProgress<int>
|
|
{
|
|
private int _progress = 0;
|
|
private int _stepProgress = 1;
|
|
private int _expectedSteps = 100;
|
|
private int _parentProportion = 100;
|
|
|
|
public int Progress => _progress;
|
|
public int MaxProgress { get; set; } = 100;
|
|
public int StepProgress
|
|
{
|
|
get => _stepProgress;
|
|
set
|
|
{
|
|
_stepProgress = value;
|
|
_expectedSteps = MaxProgress / value;
|
|
}
|
|
}
|
|
public int ExpectedSteps
|
|
{
|
|
get => _expectedSteps;
|
|
set
|
|
{
|
|
_expectedSteps = value;
|
|
MaxProgress = Number.IntPow(10, Number.GetLength(value));
|
|
_stepProgress = MaxProgress / value;
|
|
}
|
|
}
|
|
public Func<int, Task>? ReporterFunc { get; set; } = null;
|
|
public ProgressReporter? Parent { get; set; }
|
|
public ProgressReporter? Child { get; set; }
|
|
|
|
private ProgressStatus _status = ProgressStatus.Pending;
|
|
private string _errorMessage;
|
|
|
|
public string TaskId { get; set; } = Guid.NewGuid().ToString();
|
|
public int ProgressPercent => _progress * 100 / MaxProgress;
|
|
public ProgressStatus Status => _status;
|
|
public string ErrorMessage => _errorMessage;
|
|
|
|
public ProgressReporter(Func<int, Task>? reporter = null, int initProgress = 0, int maxProgress = 100, int step = 1)
|
|
{
|
|
_progress = initProgress;
|
|
MaxProgress = maxProgress;
|
|
StepProgress = step;
|
|
ReporterFunc = reporter;
|
|
}
|
|
|
|
public ProgressReporter(int parentProportion, int expectedSteps = 100, Func<int, Task>? reporter = null)
|
|
{
|
|
this._parentProportion = parentProportion;
|
|
MaxProgress = Number.IntPow(10, Number.GetLength(expectedSteps));
|
|
StepProgress = MaxProgress / expectedSteps;
|
|
ReporterFunc = reporter;
|
|
}
|
|
|
|
private async void ForceReport(int value)
|
|
{
|
|
try
|
|
{
|
|
if (ReporterFunc != null)
|
|
await ReporterFunc(value);
|
|
|
|
if (Parent != null)
|
|
Parent.Increase((value - _progress) / StepProgress * _parentProportion / (MaxProgress / StepProgress));
|
|
|
|
_progress = value;
|
|
}
|
|
catch (OperationCanceledException ex)
|
|
{
|
|
_errorMessage = ex.Message;
|
|
this._status = ProgressStatus.Canceled;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_errorMessage = ex.Message;
|
|
this._status = ProgressStatus.Failed;
|
|
}
|
|
}
|
|
|
|
public async void Report(int value)
|
|
{
|
|
if (this._status == ProgressStatus.Pending)
|
|
this._status = ProgressStatus.InProgress;
|
|
else if (this.Status != ProgressStatus.InProgress)
|
|
return;
|
|
|
|
if (value > MaxProgress) return;
|
|
ForceReport(value);
|
|
}
|
|
|
|
public void Increase(int? value = null)
|
|
{
|
|
if (this._status == ProgressStatus.Pending)
|
|
this._status = ProgressStatus.InProgress;
|
|
else if (this.Status != ProgressStatus.InProgress)
|
|
return;
|
|
|
|
if (value.HasValue)
|
|
{
|
|
if (_progress + value.Value >= MaxProgress) return;
|
|
this.Report(_progress + value.Value);
|
|
}
|
|
else
|
|
{
|
|
if (_progress + StepProgress >= MaxProgress) return;
|
|
this.Report(_progress + StepProgress);
|
|
}
|
|
}
|
|
|
|
public void Finish()
|
|
{
|
|
this._status = ProgressStatus.Completed;
|
|
this.ForceReport(MaxProgress);
|
|
}
|
|
|
|
public void Cancel()
|
|
{
|
|
this._status = ProgressStatus.Canceled;
|
|
this._errorMessage = "User Cancelled";
|
|
this.ForceReport(_progress);
|
|
}
|
|
|
|
public void Error(string message)
|
|
{
|
|
this._status = ProgressStatus.Failed;
|
|
this._errorMessage = message;
|
|
this.ForceReport(_progress);
|
|
}
|
|
|
|
public ProgressReporter CreateChild(int proportion, int expectedSteps = 100)
|
|
{
|
|
var child = new ProgressReporter(proportion, expectedSteps);
|
|
child.Parent = this;
|
|
this.Child = child;
|
|
return child;
|
|
}
|
|
}
|
|
|
|
public class ProgressTrackerService : BackgroundService
|
|
{
|
|
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
|
private readonly ConcurrentDictionary<string, TaskProgressInfo> _taskMap = new();
|
|
private readonly IHubContext<ProgressHub, IProgressReceiver> _hubContext;
|
|
|
|
private class TaskProgressInfo
|
|
{
|
|
public ProgressReporter Reporter { get; set; }
|
|
public string? ConnectionId { get; set; }
|
|
public required CancellationToken CancellationToken { get; set; }
|
|
public required CancellationTokenSource CancellationTokenSource { get; set; }
|
|
public required DateTime UpdatedAt { get; set; }
|
|
}
|
|
|
|
public ProgressTrackerService(IHubContext<ProgressHub, IProgressReceiver> hubContext)
|
|
{
|
|
_hubContext = hubContext;
|
|
}
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
while (!stoppingToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
foreach (var kvp in _taskMap)
|
|
{
|
|
var info = kvp.Value;
|
|
// 超过 1 分钟且任务已完成/失败/取消
|
|
if ((now - info.UpdatedAt).TotalMinutes > 1 &&
|
|
(info.Reporter.Status == ProgressStatus.Completed ||
|
|
info.Reporter.Status == ProgressStatus.Failed ||
|
|
info.Reporter.Status == ProgressStatus.Canceled))
|
|
{
|
|
_taskMap.TryRemove(kvp.Key, out _);
|
|
logger.Info($"Cleaned up task {kvp.Key}");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.Error(ex, "Error during ProgressTracker cleanup");
|
|
}
|
|
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
|
|
}
|
|
}
|
|
|
|
public (string, ProgressReporter) CreateTask(CancellationToken? cancellationToken = null)
|
|
{
|
|
CancellationTokenSource? cancellationTokenSource;
|
|
if (cancellationToken.HasValue)
|
|
{
|
|
cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value);
|
|
}
|
|
else
|
|
{
|
|
cancellationTokenSource = new CancellationTokenSource();
|
|
}
|
|
|
|
var progressInfo = new TaskProgressInfo
|
|
{
|
|
ConnectionId = null,
|
|
UpdatedAt = DateTime.UtcNow,
|
|
CancellationToken = cancellationTokenSource.Token,
|
|
CancellationTokenSource = cancellationTokenSource,
|
|
};
|
|
|
|
var progress = new ProgressReporter(async value =>
|
|
{
|
|
cancellationTokenSource.Token.ThrowIfCancellationRequested();
|
|
|
|
// 通过 SignalR 推送进度
|
|
if (progressInfo.ConnectionId != null)
|
|
await _hubContext.Clients.Client(progressInfo.ConnectionId).OnReceiveProgress(progressInfo.Reporter);
|
|
});
|
|
|
|
progressInfo.Reporter = progress;
|
|
|
|
_taskMap.TryAdd(progressInfo.Reporter.TaskId, progressInfo);
|
|
|
|
return (progressInfo.Reporter.TaskId, progress);
|
|
}
|
|
|
|
public Optional<ProgressReporter> GetReporter(string taskId)
|
|
{
|
|
if (_taskMap.TryGetValue(taskId, out var info))
|
|
{
|
|
return info.Reporter;
|
|
}
|
|
return Optional<ProgressReporter>.None;
|
|
}
|
|
|
|
public Optional<ProgressStatus> GetProgressStatus(string taskId)
|
|
{
|
|
if (_taskMap.TryGetValue(taskId, out var info))
|
|
{
|
|
return info.Reporter.Status;
|
|
}
|
|
return Optional<ProgressStatus>.None;
|
|
}
|
|
|
|
public bool BindTask(string taskId, string connectionId)
|
|
{
|
|
if (_taskMap.TryGetValue(taskId, out var info) && info != null)
|
|
{
|
|
lock (info)
|
|
{
|
|
info.ConnectionId = connectionId;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public bool CancelTask(string taskId)
|
|
{
|
|
try
|
|
{
|
|
if (_taskMap.TryGetValue(taskId, out var info) && info != null)
|
|
{
|
|
lock (info)
|
|
{
|
|
info.CancellationTokenSource.Cancel();
|
|
info.Reporter.Cancel();
|
|
info.UpdatedAt = DateTime.UtcNow;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.Error(ex, $"Failed to cancel task {taskId}");
|
|
return false;
|
|
}
|
|
}
|
|
}
|