159 lines
5.1 KiB
C#
159 lines
5.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.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 async Task<ProcessResult> RunAsync(
|
|
string filename,
|
|
string arguments,
|
|
string? workingDirectory = 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;
|
|
}
|
|
|
|
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}"));
|
|
}
|
|
else
|
|
{
|
|
processLifetimeTask.TrySetResult(new ProcessResult(outputBuilder.ToString(), errorBuilder.ToString(), process.ExitCode));
|
|
}
|
|
};
|
|
|
|
process.Start();
|
|
onStart?.Invoke(process.Id);
|
|
|
|
process.BeginOutputReadLine();
|
|
process.BeginErrorReadLine();
|
|
|
|
var cancelledTcs = new TaskCompletionSource<object?>();
|
|
await using var _ = cancellationToken.Register(() => cancelledTcs.TrySetResult(null));
|
|
|
|
var result = await Task.WhenAny(processLifetimeTask.Task, cancelledTcs.Task);
|
|
|
|
if (result == cancelledTcs.Task)
|
|
{
|
|
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;
|
|
}
|
|
|
|
public static void KillProcess(int pid)
|
|
{
|
|
try
|
|
{
|
|
using var process = Process.GetProcessById(pid);
|
|
process?.Kill();
|
|
}
|
|
catch (ArgumentException) { }
|
|
catch (InvalidOperationException) { }
|
|
}
|
|
}
|
|
}
|