Skip to content

Instantly share code, notes, and snippets.

@jewijaya
Forked from Indigo744/ProcessAsyncHelper.cs
Last active July 24, 2020 01:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jewijaya/f2350ce52afc71d024b9608c0aaba2fd to your computer and use it in GitHub Desktop.
Save jewijaya/f2350ce52afc71d024b9608c0aaba2fd to your computer and use it in GitHub Desktop.
The right way to run external process in .NET (async version)
/// <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