Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
The right way to run external process in .NET (async version)
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/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="startInfo">ProcessStartInfo object</param>
/// <param name="timeoutMs">The timeout in milliseconds (null for no timeout)</param>
/// <returns>Result object</returns>
public static async Task<Result> RunAsync(ProcessStartInfo startInfo, int? timeoutMs = null)
{
Result result = new Result();
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;
}
// 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; } = "";
}
}
@juusimaa

This comment has been minimized.

Copy link

@juusimaa juusimaa commented Jan 29, 2020

Works great but is there a typo in line 96 - should it be if (process.StartInfo.RedirectStandardOutput)?

@Indigo744

This comment has been minimized.

Copy link
Owner Author

@Indigo744 Indigo744 commented Jan 29, 2020

Indeed! Thanks for reporting, I have corrected the issue 😉

@Indigo744

This comment has been minimized.

Copy link
Owner Author

@Indigo744 Indigo744 commented Apr 9, 2020

Updated : removed useless Task.run() and add ConfigureAwait(false) on awaiting.

@NSouth

This comment has been minimized.

Copy link

@NSouth NSouth commented May 27, 2020

Awesome. I used this to make a version that supports providing stdIn: https://gist.github.com/NSouth/6d44d07db97df7d41bce33ac3117fdeb

@jewijaya

This comment has been minimized.

Copy link

@jewijaya jewijaya commented Jul 24, 2020

Thanks. I've modified a little bit here in my gist. Instead using StringBuilder, I use TextWriter to be able write directly to the console.
Also the creation of ProcessStartInfo instance will be responsible on ProcessAsyncHelper.RunAsync.

@NicolasDorier

This comment has been minimized.

Copy link

@NicolasDorier NicolasDorier commented Nov 9, 2020

Two issues:

  1. You should call process.WaitForExit() else on linux you will have a bunch of zombie process. Maybe dotnet does that internally, but I can't say
  2. You are not cancelling the Task.Delay, so it creates a timer that won't be cleaned until it actually fire.
@Indigo744

This comment has been minimized.

Copy link
Owner Author

@Indigo744 Indigo744 commented Nov 9, 2020

@NicolasDorier Thanks for your input, but I fail to understand why WaitForExit() is needed. Since it is not an async function, it would block the call. Can you point me through the documentation stating this need?

Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment