ProcessUtil.cs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Diagnostics;
  6. using System.Globalization;
  7. using System.IO;
  8. using System.Runtime.InteropServices;
  9. using System.Text;
  10. using System.Threading;
  11. using System.Threading.Tasks;
  12. #nullable enable
  13. namespace HelixTestRunner;
  14. public static partial class ProcessUtil
  15. {
  16. [LibraryImport("libc", SetLastError = true, EntryPoint = "kill")]
  17. private static partial int sys_kill(int pid, int sig);
  18. public static Task CaptureDumpAsync()
  19. {
  20. var dumpDirectoryPath = Environment.GetEnvironmentVariable("HELIX_DUMP_FOLDER");
  21. if (dumpDirectoryPath == null)
  22. {
  23. return Task.CompletedTask;
  24. }
  25. var process = Process.GetCurrentProcess();
  26. var dumpFilePath = Path.Combine(dumpDirectoryPath, $"{process.ProcessName}-{process.Id}.dmp");
  27. return CaptureDumpAsync(process.Id, dumpFilePath);
  28. }
  29. public static Task CaptureDumpAsync(int pid)
  30. {
  31. var dumpDirectoryPath = Environment.GetEnvironmentVariable("HELIX_DUMP_FOLDER");
  32. if (dumpDirectoryPath == null)
  33. {
  34. return Task.CompletedTask;
  35. }
  36. var process = Process.GetProcessById(pid);
  37. var dumpFilePath = Path.Combine(dumpDirectoryPath, $"{process.ProcessName}.{process.Id}.dmp");
  38. return CaptureDumpAsync(process.Id, dumpFilePath);
  39. }
  40. public static Task CaptureDumpAsync(int pid, string dumpFilePath)
  41. {
  42. // Skip this on OSX, we know it's unsupported right now
  43. if (OperatingSystem.IsMacOS())
  44. {
  45. // Can we capture stacks or do a gcdump instead?
  46. return Task.CompletedTask;
  47. }
  48. if (!File.Exists($"{Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT")}/dotnet-dump") &&
  49. !File.Exists($"{Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT")}/dotnet-dump.exe"))
  50. {
  51. return Task.CompletedTask;
  52. }
  53. return RunAsync($"{Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT")}/dotnet-dump", $"collect -p {pid} -o \"{dumpFilePath}\"");
  54. }
  55. public static async Task<ProcessResult> RunAsync(
  56. string filename,
  57. string arguments,
  58. string? workingDirectory = null,
  59. string? dumpDirectoryPath = null,
  60. bool throwOnError = true,
  61. IDictionary<string, string?>? environmentVariables = null,
  62. Action<string>? outputDataReceived = null,
  63. Action<string>? errorDataReceived = null,
  64. Action<int>? onStart = null,
  65. CancellationToken cancellationToken = default)
  66. {
  67. PrintMessage($"Running '{filename} {arguments}'");
  68. using var process = new Process()
  69. {
  70. StartInfo =
  71. {
  72. FileName = filename,
  73. Arguments = arguments,
  74. RedirectStandardOutput = true,
  75. RedirectStandardError = true,
  76. UseShellExecute = false,
  77. CreateNoWindow = true,
  78. },
  79. EnableRaisingEvents = true
  80. };
  81. if (workingDirectory != null)
  82. {
  83. process.StartInfo.WorkingDirectory = workingDirectory;
  84. }
  85. dumpDirectoryPath ??= Environment.GetEnvironmentVariable("HELIX_DUMP_FOLDER");
  86. if (dumpDirectoryPath != null)
  87. {
  88. process.StartInfo.EnvironmentVariables["COMPlus_DbgEnableMiniDump"] = "1";
  89. process.StartInfo.EnvironmentVariables["COMPlus_DbgMiniDumpName"] = Path.Combine(dumpDirectoryPath, $"{Path.GetFileName(filename)}.%d.dmp");
  90. }
  91. if (environmentVariables != null)
  92. {
  93. foreach (var kvp in environmentVariables)
  94. {
  95. process.StartInfo.Environment.Add(kvp);
  96. }
  97. }
  98. var outputBuilder = new StringBuilder();
  99. process.OutputDataReceived += (_, e) =>
  100. {
  101. if (e.Data != null)
  102. {
  103. if (outputDataReceived != null)
  104. {
  105. outputDataReceived.Invoke(e.Data);
  106. }
  107. else
  108. {
  109. outputBuilder.AppendLine(e.Data);
  110. }
  111. }
  112. };
  113. var errorBuilder = new StringBuilder();
  114. process.ErrorDataReceived += (_, e) =>
  115. {
  116. if (e.Data != null)
  117. {
  118. if (errorDataReceived != null)
  119. {
  120. errorDataReceived.Invoke(e.Data);
  121. }
  122. else
  123. {
  124. errorBuilder.AppendLine(e.Data);
  125. }
  126. }
  127. };
  128. var processLifetimeTask = new TaskCompletionSource<ProcessResult>();
  129. process.Exited += (_, e) =>
  130. {
  131. PrintMessage($"'{process.StartInfo.FileName} {process.StartInfo.Arguments}' completed with exit code '{process.ExitCode}'");
  132. if (throwOnError && process.ExitCode != 0)
  133. {
  134. processLifetimeTask.TrySetException(new InvalidOperationException($"Command {filename} {arguments} returned exit code {process.ExitCode} output: {outputBuilder.ToString()}"));
  135. }
  136. else
  137. {
  138. processLifetimeTask.TrySetResult(new ProcessResult(outputBuilder.ToString(), errorBuilder.ToString(), process.ExitCode));
  139. }
  140. };
  141. process.Start();
  142. onStart?.Invoke(process.Id);
  143. process.BeginOutputReadLine();
  144. process.BeginErrorReadLine();
  145. var canceledTcs = new TaskCompletionSource<object?>();
  146. await using var _ = cancellationToken.Register(() => canceledTcs.TrySetResult(null));
  147. var result = await Task.WhenAny(processLifetimeTask.Task, canceledTcs.Task);
  148. if (result == canceledTcs.Task)
  149. {
  150. if (dumpDirectoryPath != null)
  151. {
  152. var dumpFilePath = Path.Combine(dumpDirectoryPath, $"{Path.GetFileName(filename)}.{process.Id}.dmp");
  153. // Capture a process dump if the dumpDirectory is set
  154. await CaptureDumpAsync(process.Id, dumpFilePath);
  155. }
  156. if (!OperatingSystem.IsWindows())
  157. {
  158. sys_kill(process.Id, sig: 2); // SIGINT
  159. var cancel = new CancellationTokenSource();
  160. await Task.WhenAny(processLifetimeTask.Task, Task.Delay(TimeSpan.FromSeconds(5), cancel.Token));
  161. cancel.Cancel();
  162. }
  163. if (!process.HasExited)
  164. {
  165. process.CloseMainWindow();
  166. if (!process.HasExited)
  167. {
  168. process.Kill();
  169. }
  170. }
  171. }
  172. return await processLifetimeTask.Task;
  173. }
  174. public static void PrintMessage(string message) => Console.WriteLine($"{DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)} {message}");
  175. public static void PrintErrorMessage(string message) => Console.Error.WriteLine($"{DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)} {message}");
  176. }