-
-
Save leslietoo/362538a1491dc483543ea1939e18c57b to your computer and use it in GitHub Desktop.
The right way to run external process in .NET (async version)
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.Generic; | |
using System.Diagnostics; | |
using System.Text; | |
using System.Threading.Tasks; | |
/// <summary> | |
/// Process helper with asynchronous interface | |
/// - Based on https://gist.github.com/Indigo744/b5f3bd50df4b179651c876416bf70d0a | |
/// - And on https://stackoverflow.com/questions/470256/process-waitforexit-asynchronously | |
/// </summary> | |
public static class ProcessAsyncHelper | |
{ | |
/// <summary> | |
/// Run a process asynchronously | |
/// <para>To capture STDOUT, set StartInfo.RedirectStandardOutput to TRUE</para> | |
/// <para>To capture STDERR, set StartInfo.RedirectStandardError to TRUE</para> | |
/// </summary> | |
/// <param name="startInfo">ProcessStartInfo object</param> | |
/// <param name="stdIn">If provided, will be written to stdIn</param> | |
/// <param name="timeoutMs">The timeout in milliseconds (null for no timeout)</param> | |
/// <returns>Result object</returns> | |
public async Task<ProcessHelperResult> RunProcessAsync(ProcessStartInfo startInfo, string stdIn = null, int? timeoutMs = null) | |
{ | |
var result = new Result(); | |
if (!string.IsNullOrWhiteSpace(stdIn)) | |
{ | |
startInfo.RedirectStandardInput = true; | |
} | |
using (var process = new Process() { StartInfo = startInfo, EnableRaisingEvents = true }) | |
{ | |
// List of tasks to wait for a whole process exit | |
List<Task> processTasks = new List<Task>(); | |
// === EXITED Event handling === | |
var processExitEvent = new TaskCompletionSource<object>(); | |
process.Exited += (sender, args) => | |
{ | |
processExitEvent.TrySetResult(true); | |
}; | |
processTasks.Add(processExitEvent.Task); | |
// === STDOUT handling === | |
var stdOutBuilder = new StringBuilder(); | |
if (process.StartInfo.RedirectStandardOutput) | |
{ | |
var stdOutCloseEvent = new TaskCompletionSource<bool>(); | |
process.OutputDataReceived += (s, e) => | |
{ | |
if (e.Data == null) | |
{ | |
stdOutCloseEvent.TrySetResult(true); | |
} | |
else | |
{ | |
stdOutBuilder.Append(e.Data); | |
} | |
}; | |
processTasks.Add(stdOutCloseEvent.Task); | |
} | |
else | |
{ | |
// STDOUT is not redirected, so we won't look for it | |
} | |
// === STDERR handling === | |
var stdErrBuilder = new StringBuilder(); | |
if (process.StartInfo.RedirectStandardError) | |
{ | |
var stdErrCloseEvent = new TaskCompletionSource<bool>(); | |
process.ErrorDataReceived += (s, e) => | |
{ | |
if (e.Data == null) | |
{ | |
stdErrCloseEvent.TrySetResult(true); | |
} | |
else | |
{ | |
stdErrBuilder.Append(e.Data); | |
} | |
}; | |
processTasks.Add(stdErrCloseEvent.Task); | |
} | |
else | |
{ | |
// STDERR is not redirected, so we won't look for it | |
} | |
// === START OF PROCESS === | |
if (!process.Start()) | |
{ | |
result.ExitCode = process.ExitCode; | |
return result; | |
} | |
// Read StdIn if provided | |
if (process.StartInfo.RedirectStandardInput) | |
{ | |
using (var writer = process.StandardInput) | |
{ | |
writer.Write(stdIn); | |
} | |
} | |
// Reads the output stream first as needed and then waits because deadlocks are possible | |
if (process.StartInfo.RedirectStandardOutput) | |
{ | |
process.BeginOutputReadLine(); | |
} | |
else | |
{ | |
// No STDOUT | |
} | |
if (process.StartInfo.RedirectStandardError) | |
{ | |
process.BeginErrorReadLine(); | |
} | |
else | |
{ | |
// No STDERR | |
} | |
// === ASYNC WAIT OF PROCESS === | |
// Process completion = exit AND stdout (if defined) AND stderr (if defined) | |
Task processCompletionTask = Task.WhenAll(processTasks); | |
// Task to wait for exit OR timeout (if defined) | |
Task<Task> awaitingTask = timeoutMs.HasValue | |
? Task.WhenAny(Task.Delay(timeoutMs.Value), processCompletionTask) | |
: Task.WhenAny(processCompletionTask); | |
// Let's now wait for something to end... | |
if ((await awaitingTask.ConfigureAwait(false)) == processCompletionTask) | |
{ | |
// -> Process exited cleanly | |
result.ExitCode = process.ExitCode; | |
} | |
else | |
{ | |
// -> Timeout, let's kill the process | |
try | |
{ | |
process.Kill(); | |
} | |
catch | |
{ | |
// ignored | |
} | |
} | |
// Read stdout/stderr | |
result.StdOut = stdOutBuilder.ToString(); | |
result.StdErr = stdErrBuilder.ToString(); | |
} | |
return result; | |
} | |
/// <summary> | |
/// Run process result | |
/// </summary> | |
public class Result | |
{ | |
/// <summary> | |
/// Exit code | |
/// <para>If NULL, process exited due to timeout</para> | |
/// </summary> | |
public int? ExitCode { get; set; } = null; | |
/// <summary> | |
/// Standard error stream | |
/// </summary> | |
public string StdErr { get; set; } = ""; | |
/// <summary> | |
/// Standard output stream | |
/// </summary> | |
public string StdOut { get; set; } = ""; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment