Skip to content

Instantly share code, notes, and snippets.

@Indigo744
Forked from georg-jung/ProcessAsyncHelper.cs
Last active September 1, 2024 18:35
Show Gist options
  • Save Indigo744/b5f3bd50df4b179651c876416bf70d0a to your computer and use it in GitHub Desktop.
Save Indigo744/b5f3bd50df4b179651c876416bf70d0a to your computer and use it in GitHub Desktop.
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.AppendLine(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.AppendLine(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
Copy link

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

@Indigo744
Copy link
Author

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

@Indigo744
Copy link
Author

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

@NSouth
Copy link

NSouth commented May 27, 2020

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

@jewijaya
Copy link

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
Copy link

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
Copy link
Author

@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.

@Indigo744
Copy link
Author

FYI, changed *Builder.Append() to *Builder.AppendLine() to preserve lines.

@mqudsi
Copy link

mqudsi commented Nov 12, 2021

FYI .NET 5 and .NET 6 have a native Process.WaitForExitAsync() that can be used instead of the workaround used here.

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