aspnetcore/eng/helix/content/RunTests/ProcessUtil.cs

207 lines
7.1 KiB
C#

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
#nullable enable
namespace RunTests
{
public static class ProcessUtil
{
[DllImport("libc", SetLastError = true, EntryPoint = "kill")]
private static extern int sys_kill(int pid, int sig);
public static Task CaptureDumpAsync()
{
var dumpDirectoryPath = Environment.GetEnvironmentVariable("HELIX_DUMP_FOLDER");
if (dumpDirectoryPath == null)
{
return Task.CompletedTask;
}
var process = Process.GetCurrentProcess();
var dumpFilePath = Path.Combine(dumpDirectoryPath, $"{process.ProcessName}-{process.Id}.dmp");
return CaptureDumpAsync(process.Id, dumpFilePath);
}
public static Task CaptureDumpAsync(int pid)
{
var dumpDirectoryPath = Environment.GetEnvironmentVariable("HELIX_DUMP_FOLDER");
if (dumpDirectoryPath == null)
{
return Task.CompletedTask;
}
var process = Process.GetProcessById(pid);
var dumpFilePath = Path.Combine(dumpDirectoryPath, $"{process.ProcessName}.{process.Id}.dmp");
return CaptureDumpAsync(process.Id, dumpFilePath);
}
public static Task CaptureDumpAsync(int pid, string dumpFilePath)
{
// Skip this on OSX, we know it's unsupported right now
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// Can we capture stacks or do a gcdump instead?
return Task.CompletedTask;
}
return RunAsync($"{Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT")}/dotnet-dump", $"collect -p {pid} -o \"{dumpFilePath}\"");
}
public static async Task<ProcessResult> RunAsync(
string filename,
string arguments,
string? workingDirectory = null,
string dumpDirectoryPath = null,
bool throwOnError = true,
IDictionary<string, string?>? environmentVariables = null,
Action<string>? outputDataReceived = null,
Action<string>? errorDataReceived = null,
Action<int>? onStart = null,
CancellationToken cancellationToken = default)
{
Console.WriteLine($"Running '{filename} {arguments}'");
using var process = new Process()
{
StartInfo =
{
FileName = filename,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
},
EnableRaisingEvents = true
};
if (workingDirectory != null)
{
process.StartInfo.WorkingDirectory = workingDirectory;
}
dumpDirectoryPath ??= Environment.GetEnvironmentVariable("HELIX_DUMP_FOLDER");
if (dumpDirectoryPath != null)
{
process.StartInfo.EnvironmentVariables["COMPlus_DbgEnableMiniDump"] = "1";
process.StartInfo.EnvironmentVariables["COMPlus_DbgMiniDumpName"] = Path.Combine(dumpDirectoryPath, $"{Path.GetFileName(filename)}.%d.dmp");
}
if (environmentVariables != null)
{
foreach (var kvp in environmentVariables)
{
process.StartInfo.Environment.Add(kvp);
}
}
var outputBuilder = new StringBuilder();
process.OutputDataReceived += (_, e) =>
{
if (e.Data != null)
{
if (outputDataReceived != null)
{
outputDataReceived.Invoke(e.Data);
}
else
{
outputBuilder.AppendLine(e.Data);
}
}
};
var errorBuilder = new StringBuilder();
process.ErrorDataReceived += (_, e) =>
{
if (e.Data != null)
{
if (errorDataReceived != null)
{
errorDataReceived.Invoke(e.Data);
}
else
{
errorBuilder.AppendLine(e.Data);
}
}
};
var processLifetimeTask = new TaskCompletionSource<ProcessResult>();
process.Exited += (_, e) =>
{
Console.WriteLine($"'{process.StartInfo.FileName} {process.StartInfo.Arguments}' completed with exit code '{process.ExitCode}'");
if (throwOnError && process.ExitCode != 0)
{
processLifetimeTask.TrySetException(new InvalidOperationException($"Command {filename} {arguments} returned exit code {process.ExitCode} output: {outputBuilder.ToString()}"));
}
else
{
processLifetimeTask.TrySetResult(new ProcessResult(outputBuilder.ToString(), errorBuilder.ToString(), process.ExitCode));
}
};
process.Start();
onStart?.Invoke(process.Id);
process.BeginOutputReadLine();
process.BeginErrorReadLine();
var canceledTcs = new TaskCompletionSource<object?>();
await using var _ = cancellationToken.Register(() => canceledTcs.TrySetResult(null));
var result = await Task.WhenAny(processLifetimeTask.Task, canceledTcs.Task);
if (result == canceledTcs.Task)
{
if (dumpDirectoryPath != null)
{
var dumpFilePath = Path.Combine(dumpDirectoryPath, $"{Path.GetFileName(filename)}.{process.Id}.dmp");
// Capture a process dump if the dumpDirectory is set
await CaptureDumpAsync(process.Id, dumpFilePath);
}
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
sys_kill(process.Id, sig: 2); // SIGINT
var cancel = new CancellationTokenSource();
await Task.WhenAny(processLifetimeTask.Task, Task.Delay(TimeSpan.FromSeconds(5), cancel.Token));
cancel.Cancel();
}
if (!process.HasExited)
{
process.CloseMainWindow();
if (!process.HasExited)
{
process.Kill();
}
}
}
return await processLifetimeTask.Task;
}
}
}