-
-
Save AlexMAS/276eed492bc989e13dcce7c78b9e179d to your computer and use it in GitHub Desktop.
using System; | |
using System.Diagnostics; | |
using System.Text; | |
using System.Threading.Tasks; | |
public static class ProcessAsyncHelper | |
{ | |
public static async Task<ProcessResult> ExecuteShellCommand(string command, string arguments, int timeout) | |
{ | |
var result = new ProcessResult(); | |
using (var process = new Process()) | |
{ | |
// If you run bash-script on Linux it is possible that ExitCode can be 255. | |
// To fix it you can try to add '#!/bin/bash' header to the script. | |
process.StartInfo.FileName = command; | |
process.StartInfo.Arguments = arguments; | |
process.StartInfo.UseShellExecute = false; | |
process.StartInfo.RedirectStandardInput = true; | |
process.StartInfo.RedirectStandardOutput = true; | |
process.StartInfo.RedirectStandardError = true; | |
process.StartInfo.CreateNoWindow = true; | |
var outputBuilder = new StringBuilder(); | |
var outputCloseEvent = new TaskCompletionSource<bool>(); | |
process.OutputDataReceived += (s, e) => | |
{ | |
// The output stream has been closed i.e. the process has terminated | |
if (e.Data == null) | |
{ | |
outputCloseEvent.SetResult(true); | |
} | |
else | |
{ | |
outputBuilder.AppendLine(e.Data); | |
} | |
}; | |
var errorBuilder = new StringBuilder(); | |
var errorCloseEvent = new TaskCompletionSource<bool>(); | |
process.ErrorDataReceived += (s, e) => | |
{ | |
// The error stream has been closed i.e. the process has terminated | |
if (e.Data == null) | |
{ | |
errorCloseEvent.SetResult(true); | |
} | |
else | |
{ | |
errorBuilder.AppendLine(e.Data); | |
} | |
}; | |
bool isStarted; | |
try | |
{ | |
isStarted = process.Start(); | |
} | |
catch (Exception error) | |
{ | |
// Usually it occurs when an executable file is not found or is not executable | |
result.Completed = true; | |
result.ExitCode = -1; | |
result.Output = error.Message; | |
isStarted = false; | |
} | |
if (isStarted) | |
{ | |
// Reads the output stream first and then waits because deadlocks are possible | |
process.BeginOutputReadLine(); | |
process.BeginErrorReadLine(); | |
// Creates task to wait for process exit using timeout | |
var waitForExit = WaitForExitAsync(process, timeout); | |
// Create task to wait for process exit and closing all output streams | |
var processTask = Task.WhenAll(waitForExit, outputCloseEvent.Task, errorCloseEvent.Task); | |
// Waits process completion and then checks it was not completed by timeout | |
if (await Task.WhenAny(Task.Delay(timeout), processTask) == processTask && waitForExit.Result) | |
{ | |
result.Completed = true; | |
result.ExitCode = process.ExitCode; | |
// Adds process output if it was completed with error | |
if (process.ExitCode != 0) | |
{ | |
result.Output = $"{outputBuilder}{errorBuilder}"; | |
} | |
} | |
else | |
{ | |
try | |
{ | |
// Kill hung process | |
process.Kill(); | |
} | |
catch | |
{ | |
} | |
} | |
} | |
} | |
return result; | |
} | |
private static Task<bool> WaitForExitAsync(Process process, int timeout) | |
{ | |
return Task.Run(() => process.WaitForExit(timeout)); | |
} | |
public struct ProcessResult | |
{ | |
public bool Completed; | |
public int? ExitCode; | |
public string Output; | |
} | |
} |
I made a generic one which can accept any ProcessStartInfo
objet and with or without timeout: https://gist.github.com/Indigo744/b5f3bd50df4b179651c876416bf70d0a
I also took some improvements from the various answer from https://stackoverflow.com/questions/470256/process-waitforexit-asynchronously
private static Task<bool> WaitForExitAsync(Process process, int timeout)
{
return Task.Run(() => process.WaitForExit(timeout));
}
You are still tying up a (background) thread, defeating the purpose of this being async in the first place.
Try something like this:
public static async Task WaitForExitAsync(this Process process, CancellationToken cancellationToken = default)
{
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
void Process_Exited(object sender, EventArgs e)
{
tcs.TrySetResult(true);
}
process.EnableRaisingEvents = true;
process.Exited += Process_Exited;
try
{
if (process.HasExited)
{
return;
}
using (cancellationToken.Register(() => tcs.TrySetCanceled()))
{
await tcs.Task.ConfigureAwait(false);
}
}
finally
{
process.Exited -= Process_Exited;
}
}
Thanks @davidathompson, I've updated my fork accordingly : https://gist.github.com/Indigo744/b5f3bd50df4b179651c876416bf70d0a (actually my Task.run()
were really useless).
@Indigo744 thanks for that, but if you don't call WaitForExit
on linux, it might create zombie processes.
Now maybe process.Exited
do it under the hood, but I am not sure so I would advise to at least call it.
Just a FYI, there's also this tiny library: https://github.com/adamralph/simple-exec
Can I run multiple processes in parallel with this, and how would I go by doing so in that case?
No, Stringbuilder isn't thread-safe. But you can always just build the strings yourself.
The fact that the stdout and stderr reads are using a TaskCancellationSource and the end is waiting on those makes this async process thread-safe.
I've created a modified version. It includes some improvements and bug fixes. See the gist first comment for details.
https://gist.github.com/tazlord/496e16698d4c4f90ea674dc2fdeb964a
I created a modified version of this too some years ago but I'd nowadays recommend also taking a look at https://github.com/Tyrrrz/CliWrap. It's a great library for async process execution and provides some quite smart features and API surface 👍🏻
Thanks. I saw all of the other versions in this thread. I also saw the library. It’s definitely very cool but sometimes all you need is a very lightweight function instead of adding an entire library to your project. My version was an attempt at meeting that goal.
Sure, you're definitely right about that. I just remember that, after investing some time in getting multiple subtleties right, found it harder to do it "right" than I originally thought it would be - and that I also didn't find CliWrap right away. Thus I decided to put the link here for reference :)
I fully agree that it's often better to rely on some easy to maintain piece of code instead of taking a dependency though. I guess it depends quite much on the specific scenario in this case.
what license is this under?
holy hell! Thank you