Skip to content

Instantly share code, notes, and snippets.

@AlexMAS
Last active April 22, 2024 02:50
Show Gist options
  • Save AlexMAS/276eed492bc989e13dcce7c78b9e179d to your computer and use it in GitHub Desktop.
Save AlexMAS/276eed492bc989e13dcce7c78b9e179d to your computer and use it in GitHub Desktop.
The right way to run external process in .NET (async version)
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;
}
}
@CaspianCanuck
Copy link

Great, thank you for sharing this! It has saved me a ton of time and frustration.

@ottosson
Copy link

Can I run multiple processes in parallel with this, and how would I go by doing so in that case?

@svenso
Copy link

svenso commented Jan 1, 2019

Line 33 & 49 should be a comparation to null (e.Data == null) and not string.IsNullOrEmpty(e.Data).
Otherwise if the process outputs an empty linefeed, SetResult ist called twice which throws an exception.

@Balaji08
Copy link

Balaji08 commented Jan 9, 2019

its great

@Balaji08
Copy link

Balaji08 commented Jan 9, 2019

Line 33 & 49 should be a comparation to null (e.Data == null) and not string.IsNullOrEmpty(e.Data).
Otherwise if the process outputs an empty linefeed, SetResult ist called twice which throws an exception.

your right!!

@georg-jung
Copy link

I created a slightly improved version, which among other things fixes the bugs described by @svenso
https://gist.github.com/georg-jung/3a8703946075d56423e418ea76212745

@tpilitis
Copy link

Спасиба for sharing this! I was looking for something similar and this is just what I wanna do :)

@jamespritz
Copy link

holy hell! Thank you

@Indigo744
Copy link

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

@davidathompson
Copy link

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;
        }
    } 

@Indigo744
Copy link

Thanks @davidathompson, I've updated my fork accordingly : https://gist.github.com/Indigo744/b5f3bd50df4b179651c876416bf70d0a (actually my Task.run() were really useless).

@NicolasDorier
Copy link

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

@agehrke
Copy link

agehrke commented Nov 30, 2020

Just a FYI, there's also this tiny library: https://github.com/adamralph/simple-exec

@OFark
Copy link

OFark commented Jul 26, 2021

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.

@tazlord
Copy link

tazlord commented Jan 29, 2024

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

@georg-jung
Copy link

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 👍🏻

@tazlord
Copy link

tazlord commented Jan 29, 2024

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.

@georg-jung
Copy link

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.

@cmconti
Copy link

cmconti commented Jan 30, 2024

what license is this under?

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