Created
February 7, 2022 12:39
-
-
Save OskarKlintrot/3947da8d31c4fa275525e4afc4138e28 to your computer and use it in GitHub Desktop.
ProcessRunner
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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