Skip to content

Instantly share code, notes, and snippets.

@OskarKlintrot
Created February 7, 2022 12:39
Show Gist options
  • Save OskarKlintrot/3947da8d31c4fa275525e4afc4138e28 to your computer and use it in GitHub Desktop.
Save OskarKlintrot/3947da8d31c4fa275525e4afc4138e28 to your computer and use it in GitHub Desktop.
ProcessRunner
using System.Diagnostics;
namespace PRunner;
public interface IProcessRunner
{
Task<ProcessResult> ExecuteProcess(
string executable,
string arguments,
CancellationToken cancellationToken,
RunConfiguration configuration = default,
bool lowPriority = false,
Action<Process>? onProcessStartHandler = null,
Action<DataReceivedEventArgs>? onOutputDataReceived = null,
Action<DataReceivedEventArgs>? onErrorDataReceived = null);
}
public enum RunConfiguration
{
None,
CaptureOutput,
DisplayWindow,
RunAsPrivileged
}
public sealed record ProcessResult(
int ExitCode,
IReadOnlyCollection<string> OutputLines,
IReadOnlyCollection<string> ErrorLines);
using System.Collections.ObjectModel;
using System.Diagnostics;
// Based on https://github.com/dotnet/roslyn/blob/main/src/Tools/Source/RunTests/ProcessRunner.cs
namespace PRunner;
internal sealed class ProcessRunner : IProcessRunner
{
public Task<ProcessResult> ExecuteProcess(
string executable,
string arguments,
CancellationToken cancellationToken,
RunConfiguration configuration = default,
bool lowPriority = false,
Action<System.Diagnostics.Process>? onProcessStartHandler = null,
Action<DataReceivedEventArgs>? onOutputDataReceived = null,
Action<DataReceivedEventArgs>? onErrorDataReceived = null)
{
var errorLines = new List<string>();
var outputLines = new List<string>();
var process = new System.Diagnostics.Process();
var tcs = new TaskCompletionSource<ProcessResult>(TaskCreationOptions.RunContinuationsAsynchronously);
var processStartInfo = CreateProcessStartInfo(executable, arguments, configuration);
process.EnableRaisingEvents = true;
process.StartInfo = processStartInfo;
process.OutputDataReceived += (s, e) =>
{
if (e.Data != null)
{
onOutputDataReceived?.Invoke(e);
outputLines.Add(e.Data);
}
};
process.ErrorDataReceived += (s, e) =>
{
if (e.Data != null)
{
onErrorDataReceived?.Invoke(e);
errorLines.Add(e.Data);
}
};
process.Exited += (s, e) =>
{
// We must call WaitForExit to make sure we've received all OutputDataReceived/ErrorDataReceived calls
// or else we'll be returning a list we're still modifying. For paranoia, we'll start a task here rather
// than enter right back into the Process type and start a wait which isn't guaranteed to be safe.
Task.Run(() =>
{
process.WaitForExit();
var result = new ProcessResult(
process.ExitCode,
new ReadOnlyCollection<string>(outputLines),
new ReadOnlyCollection<string>(errorLines));
tcs.TrySetResult(result);
}, cancellationToken);
};
_ = cancellationToken.Register(() =>
{
// If the underlying process is still running, we should kill it
if (tcs.TrySetCanceled() && !process.HasExited)
{
try
{
process.Kill();
}
catch (InvalidOperationException)
{
// Ignore, since the process is already dead
}
}
});
process.Start();
onProcessStartHandler?.Invoke(process);
if (lowPriority)
{
process.PriorityClass = ProcessPriorityClass.BelowNormal;
}
if (processStartInfo.RedirectStandardOutput)
{
process.BeginOutputReadLine();
}
if (processStartInfo.RedirectStandardError)
{
process.BeginErrorReadLine();
}
return tcs.Task;
}
private static ProcessStartInfo CreateProcessStartInfo(
string executable,
string arguments,
RunConfiguration configuration)
{
var processStartInfo = new ProcessStartInfo(executable, arguments);
switch (configuration)
{
case RunConfiguration.CaptureOutput:
processStartInfo.UseShellExecute = false;
processStartInfo.RedirectStandardOutput = true;
processStartInfo.RedirectStandardError = true;
break;
case RunConfiguration.DisplayWindow:
processStartInfo.CreateNoWindow = false;
processStartInfo.UseShellExecute = true;
break;
case RunConfiguration.RunAsPrivileged:
processStartInfo.Verb = "runas";
processStartInfo.UseShellExecute = true;
break;
}
return processStartInfo;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment