-
-
Save Indigo744/b5f3bd50df4b179651c876416bf70d0a to your computer and use it in GitHub Desktop.
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; } = ""; | |
} | |
} |
Indeed! Thanks for reporting, I have corrected the issue 😉
Updated : removed useless Task.run()
and add ConfigureAwait(false)
on awaiting.
Awesome. I used this to make a version that supports providing stdIn
: https://gist.github.com/NSouth/6d44d07db97df7d41bce33ac3117fdeb
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.
Two issues:
- 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 - You are not cancelling the
Task.Delay
, so it creates a timer that won't be cleaned until it actually fire.
@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.
FYI, changed *Builder.Append()
to *Builder.AppendLine()
to preserve lines.
FYI .NET 5 and .NET 6 have a native Process.WaitForExitAsync()
that can be used instead of the workaround used here.
Works great but is there a typo in line 96 - should it be
if (process.StartInfo.RedirectStandardOutput)
?