TaskUtils.cs 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. using System.Diagnostics;
  2. namespace MeloongCore;
  3. public static class TaskUtils {
  4. public static void Run(this Task task) => task.GetAwaiter().GetResult();
  5. public static T Run<T>(this Task<T> task) => task.GetAwaiter().GetResult();
  6. /// <summary>
  7. /// 静默运行程序并等待其结束,返回其输出和退出码。
  8. /// 支持 notepad、git 等命令。
  9. /// 超时或启动失败会抛出异常。
  10. /// </summary>
  11. public static async Task<(string Output, int ExitCode)> RunProgramAsync(string file, string arguments = "", int? timeoutMs = null, Encoding? encoding = null, CancellationToken cancellationToken = default) {
  12. cancellationToken.ThrowIfCancellationRequested();
  13. var info = new ProcessStartInfo {
  14. Arguments = arguments,
  15. FileName = PathUtils.ToShortPath(file),
  16. UseShellExecute = false,
  17. CreateNoWindow = true,
  18. RedirectStandardOutput = true, StandardOutputEncoding = encoding,
  19. RedirectStandardError = true, StandardErrorEncoding = encoding
  20. };
  21. using var program = new Process { StartInfo = info, EnableRaisingEvents = true };
  22. if (!program.Start()) throw new InvalidOperationException($"运行程序时出现意外错误:{file} {arguments}");
  23. bool hasTimeout = timeoutMs is > 0;
  24. Logger.Info($"运行程序,并返回其输出:{file} {arguments}{(hasTimeout ? $",最长可等待 {timeoutMs}ms" : "")}");
  25. // 输出和错误流
  26. var outputTask = program.StandardOutput.ReadToEndAsync();
  27. var errorTask = program.StandardError.ReadToEndAsync();
  28. var completionSource = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
  29. program.Exited += (_, _) => completionSource.TrySetResult(null);
  30. if (program.HasExited) completionSource.TrySetResult(null);
  31. var task = completionSource.Task;
  32. var timeoutTask = Task.Delay(hasTimeout ? timeoutMs!.Value : Timeout.Infinite);
  33. var cancelTask = Task.Delay(Timeout.Infinite, cancellationToken);
  34. Logger.Trace($"等待程序 {program.Id} 完成");
  35. var completedTask = await Task.WhenAny(task, timeoutTask, cancelTask);
  36. if (completedTask != task) {
  37. try {
  38. if (!program.HasExited) program.Kill();
  39. } catch (InvalidOperationException) { // 进程已退出,无需处理
  40. }
  41. if (completedTask == cancelTask) throw new OperationCanceledException(cancellationToken);
  42. throw new TimeoutException($"运行程序超时:{file} {arguments}");
  43. }
  44. return (await outputTask + await errorTask, program.ExitCode);
  45. }
  46. public static void ForEach<T>(IEnumerable<T> source, Action<T> body)
  47. => Parallel.ForEach(source, body);
  48. public static async Task ForEachAsync<T>(IEnumerable<T> source, int maxDegreeOfParallelism, Func<T, CancellationToken, Task> body, CancellationToken cancellationToken = default) {
  49. using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
  50. var token = linkedCancellationTokenSource.Token;
  51. var enumeratorLock = new object();
  52. using var enumerator = source.GetEnumerator();
  53. async Task WorkerAsync() {
  54. try {
  55. while (true) {
  56. T item;
  57. lock (enumeratorLock) {
  58. token.ThrowIfCancellationRequested();
  59. if (!enumerator.MoveNext()) return;
  60. item = enumerator.Current;
  61. }
  62. await body(item, token).ConfigureAwait(false);
  63. }
  64. } catch {
  65. try {
  66. linkedCancellationTokenSource.Cancel(); // 让尚未开始的 worker 尽快停止
  67. } catch {
  68. }
  69. throw;
  70. }
  71. }
  72. await Task.WhenAll([.. Enumerable.Range(0, maxDegreeOfParallelism).Select(_ => Task.Run(WorkerAsync))]).ConfigureAwait(false);
  73. }
  74. }