- 
      
- 
        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; | |
| } | |
| } | 
holy hell! Thank you
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?
Спасиба for sharing this! I was looking for something similar and this is just what I wanna do :)