-
-
Save jewijaya/f2350ce52afc71d024b9608c0aaba2fd to your computer and use it in GitHub Desktop.
The right way to run external process in .NET (async version)
This file contains hidden or 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
/// <summary> | |
/// Process helper with asynchronous interface | |
/// - Based on https://gist.github.com/georg-jung/3a8703946075d56423e418ea76212745 | |
/// - 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="fileName">Process filename to be run</param> | |
/// <param name="arguments">Process aguments</param> | |
/// <param name="stdOut">Starndar output writet</param> | |
/// <param name="stdErr">Standard Error output writer</param> | |
/// <param name="timeoutMs">The timeout in milliseconds (null for no timeout)</param> | |
/// <param name="workingDir">The working directory for process run</param> | |
/// <returns>Result object</returns> | |
public static async Task<int> RunAsync( | |
string fileName, | |
string arguments = null, | |
TextWriter stdOut = null, | |
TextWriter stdErr = null, | |
int? timeoutMs = null, | |
string workingDir = null) | |
{ | |
int exitCode = -1; | |
var startInfo = new ProcessStartInfo() | |
{ | |
CreateNoWindow = true, | |
Arguments = arguments, | |
FileName = fileName, | |
RedirectStandardOutput = stdOut != null, | |
RedirectStandardError = stdErr != null, | |
UseShellExecute = false, | |
WorkingDirectory = workingDir | |
}; | |
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 === | |
if (process.StartInfo.RedirectStandardOutput) | |
{ | |
var stdOutCloseEvent = new TaskCompletionSource<bool>(); | |
process.OutputDataReceived += (s, e) => | |
{ | |
if (e.Data == null) | |
{ | |
stdOutCloseEvent.TrySetResult(true); | |
} | |
else | |
{ | |
stdOut.WriteLine(e.Data); | |
} | |
}; | |
processTasks.Add(stdOutCloseEvent.Task); | |
} | |
else | |
{ | |
// STDOUT is not redirected, so we won't look for it | |
} | |
// === STDERR handling === | |
if (process.StartInfo.RedirectStandardError) | |
{ | |
var stdErrCloseEvent = new TaskCompletionSource<bool>(); | |
process.ErrorDataReceived += (s, e) => | |
{ | |
if (e.Data == null) | |
{ | |
stdErrCloseEvent.TrySetResult(true); | |
} | |
else | |
{ | |
stdErr.WriteLine(e.Data); | |
} | |
}; | |
processTasks.Add(stdErrCloseEvent.Task); | |
} | |
else | |
{ | |
// STDERR is not redirected, so we won't look for it | |
} | |
// === START OF PROCESS === | |
if (!process.Start()) | |
{ | |
return process.ExitCode; | |
} | |
// 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 | |
exitCode = process.ExitCode; | |
} | |
else | |
{ | |
// -> Timeout, let's kill the process | |
try | |
{ | |
stdOut.WriteLine("Kill process [{0}] {1}", process.Id, process.ProcessName); | |
process.Kill(); | |
} | |
catch | |
{ | |
// ignored | |
} | |
} | |
} | |
return exitCode; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment