Skip to content

Instantly share code, notes, and snippets.

@NSouth
Forked from Indigo744/ProcessAsyncHelper.cs
Last active April 1, 2024 08:56
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save NSouth/6d44d07db97df7d41bce33ac3117fdeb to your computer and use it in GitHub Desktop.
Save NSouth/6d44d07db97df7d41bce33ac3117fdeb 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/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; } = "";
}
}
@wk-done
Copy link

wk-done commented Apr 1, 2024

Hi @NSouth, thanks for this.

However, the current state doesn't compile.

  1. The generic return value is of type ProcessHelperResult but the name of the result class is ProcessHelperResult
  2. The static class ProcessAsyncHelper cannot contain the non static method RunProcessAsync

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