175

Like the title suggests, is there an equivalent to Process.Start (allows you run another application or batch file) that I can await?

I'm playing with a small console app and this seemed like the perfect place to be using async and await but I can't find any documentation for this scenario.

What I'm thinking is something along these lines:

void async RunCommand()
{
    var result = await Process.RunAsync("command to run");
}
linkerro
  • 5,318
  • 3
  • 25
  • 29
  • 2
    Why won't you just use WaitForExit on the returned Process object? – SimpleVar May 28 '12 at 18:30
  • 3
    And by the way, sounds more like you're looking for a "synced" solution, rather than an "async" solution, so the title is misleading. – SimpleVar May 28 '12 at 18:31
  • 2
    @YoryeNathan - lol. Indeed, `Process.Start` _is_ async and the OP appears to want a synchronous version. – Oded May 28 '12 at 18:32
  • Or maybe you don't want to wait for exit, but wait for "loaded"? Please clarify, OP. – SimpleVar May 28 '12 at 18:34
  • 14
    The OP is talking about the new async/await keywords in C# 5 – aquinas May 28 '12 at 18:40
  • 1
    @aquinas, yes, but that doesn't explain much, because asynchronous `Process.Start()` doesn't make sense. – svick May 28 '12 at 18:47
  • 4
    Ok, I've updated my post to be a bit more clear. The explanation for why I want this is simple. Picture a scenario where you have to run an external command (something like 7zip) and then continue the flow of the application. This is exactly what async/await was meant to facilitate and yet there seems to be no way to run a process and await it's exit. – linkerro May 29 '12 at 06:21
  • What if I had two processes that I want to run back-to-back but asynchronously just like the answer below? How would I go about doing that? – squashed.bugaboo Jun 16 '16 at 20:56
  • Related: [process.WaitForExit() asynchronously](https://stackoverflow.com/questions/470256/process-waitforexit-asynchronously) – Theodor Zoulias Jan 20 '21 at 07:01

8 Answers8

238

Process.Start() only starts the process, it doesn't wait until it finishes, so it doesn't make much sense to make it async. If you still want to do it, you can do something like await Task.Run(() => Process.Start(fileName)).

But, if you want to asynchronously wait for the process to finish, you can use the Exited event together with TaskCompletionSource:

static Task<int> RunProcessAsync(string fileName)
{
    var tcs = new TaskCompletionSource<int>();

    var process = new Process
    {
        StartInfo = { FileName = fileName },
        EnableRaisingEvents = true
    };

    process.Exited += (sender, args) =>
    {
        tcs.SetResult(process.ExitCode);
        process.Dispose();
    };

    process.Start();

    return tcs.Task;
}
svick
  • 236,525
  • 50
  • 385
  • 514
  • 40
    I finally got around to sticking something up on github for this - it doesn't have any cancellation/timeout support, but it'll gather the standard output and standard error for you, at least. https://github.com/jamesmanning/RunProcessAsTask – James Manning Dec 03 '12 at 05:54
  • 4
    This functionality is also available in the [MedallionShell](https://github.com/madelson/MedallionShell) NuGet package – ChaseMedallion Aug 29 '14 at 23:47
  • 11
    Really important: The order in which you set the various properties on `process` and `process.StartInfo` changes what happens when you run it with `.Start()`. If you for example call `.EnableRaisingEvents = true` before setting `StartInfo` properties as seen here, things work as expected. If you set it later, for example to keep it together with `.Exited`, even though you call it before `.Start()`, it fails to work properly - `.Exited` fires immediately rather than waiting for the Process to actually exit. Do not know why, just a word of caution. – Chris Moschini Oct 01 '14 at 06:15
  • If the method "RunProcessAsync" also had another call (say to a File method writing out a log file that uses some output from the process), is that OK to include it in this method? – squashed.bugaboo May 19 '16 at 22:36
  • @squashed.bugaboo Yeah, I don't see why it wouldn't be OK. But it depends on what exactly are you going to do. – svick May 19 '16 at 22:45
  • @svick: Nothing really, it is just a call to: File.WriteAllText(LogFile, process.StandardOutput.ReadToEnd()). Perhaps I could also move this to exited scope inside an if(process.ExitCode != 0) {} block? Further my example differs a little in that the whole process instantiation is inside a try {} block; so I assume in the catch block (in case of exception), I can set the tcs result to false and return the task as usual, correct? – squashed.bugaboo May 20 '16 at 14:05
  • @squashed.bugaboo That actually is problematic. `ReadToEnd()` will block, so doing that would mean `RunProcessAsync()` itself would block, defeating the purpose of making it async. If you're fine with blocking, you don't need `RunProcessAsync()`, you can use `WaitForExit()` instead. – svick May 20 '16 at 14:08
  • Oh.. but I need to make it async. No I don't want it to block the calling thread (i.e., I want users to be able to continue using the application while this process runs). – squashed.bugaboo May 20 '16 at 14:09
  • @squashed.bugaboo I think this is getting too long for comments. Consider asking a new question about this. – svick May 20 '16 at 14:10
  • Actually I found there is functions like "ReadToEndAsync" in the built-ins. I guess I'll use those. – squashed.bugaboo May 20 '16 at 14:11
  • I posted my detailed question here: http://stackoverflow.com/questions/37349973/asynchronous-method-in-a-c-sharp-class-that-executes-a-process – squashed.bugaboo May 20 '16 at 15:00
  • 3
    @svick In window form, `process.SynchronizingObject` should be set to forms component to avoid methods that handle events (such as Exited, OutputDataReceived, ErrorDataReceived) are called on separated thread. – KevinBui Jan 21 '18 at 05:03
  • 6
    It **does** actually make sense to wrap `Process.Start` in `Task.Run`. A UNC path, for example, will be resolved synchronously. This snippet can take up to 30 sec to complete: `Process.Start(@"\\live.sysinternals.com\whatever")` – Jabe Jun 19 '18 at 09:54
  • @Jabe That's a corner case which won't matter most of the time. It's certainly not what the question was asking about. – svick Jun 19 '18 at 11:53
  • 1
    can i just use `await Task.Run(() => { process.Start(); process.WaitForExit(); } ); ` ? – Toolkit Aug 12 '19 at 06:52
  • @Toolkit Doing that will block a thread, so it's preferable that you don't do that. – svick Aug 12 '19 at 07:59
  • 1
    Also usually a good idea to specify `RunContinuationsAsynchronously` when constructing the `TaskCompletionSource`, to guard against potential for synchronous continuations to lead to deadlock. – theStrawMan Sep 13 '19 at 05:40
  • 2
    According to the comments of [this answer](https://stackoverflow.com/questions/64675016/how-can-i-have-an-async-stream-return-with-2-data-sources/64676891#64676891), the `Exited` event can be raised before all handlers of a `Process`'s other events (like `OutputDataReceived`) have completed. On the other hand the `WaitForExit` guarantees that after waiting it, all `Process`'s activity will be ended. It seems that there was simply no good solution to this problem before the advent of [`WaitForExitAsync`](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexitasync). – Theodor Zoulias Jan 20 '21 at 07:15
62

Here's my take, based on svick's answer. It adds output redirection, exit code retention, and slightly better error handling (disposing the Process object even if it could not be started):

public static async Task<int> RunProcessAsync(string fileName, string args)
{
    using (var process = new Process
    {
        StartInfo =
        {
            FileName = fileName, Arguments = args,
            UseShellExecute = false, CreateNoWindow = true,
            RedirectStandardOutput = true, RedirectStandardError = true
        },
        EnableRaisingEvents = true
    })
    {
        return await RunProcessAsync(process).ConfigureAwait(false);
    }
}    
private static Task<int> RunProcessAsync(Process process)
{
    var tcs = new TaskCompletionSource<int>();

    process.Exited += (s, ea) => tcs.SetResult(process.ExitCode);
    process.OutputDataReceived += (s, ea) => Console.WriteLine(ea.Data);
    process.ErrorDataReceived += (s, ea) => Console.WriteLine("ERR: " + ea.Data);

    bool started = process.Start();
    if (!started)
    {
        //you may allow for the process to be re-used (started = false) 
        //but I'm not sure about the guarantees of the Exited event in such a case
        throw new InvalidOperationException("Could not start process: " + process);
    }

    process.BeginOutputReadLine();
    process.BeginErrorReadLine();

    return tcs.Task;
}
Community
  • 1
  • 1
Ohad Schneider
  • 36,600
  • 15
  • 168
  • 198
  • 1
    just found this interesting solution. As I am new to c# I'm not sure how to use the `async Task RunProcessAsync(string fileName, string args)`. I adapted this example and pass three objects one by one. How can I await raising events? eg. before my application stopps.. thanks a lot – marrrschine Jul 05 '16 at 14:30
  • 4
    @marrrschine I don't understand exactly what you mean, perhaps you should start a new question with some code so we can see what you tried and continue from there. – Ohad Schneider Jul 06 '16 at 08:12
  • 4
    Fantastic answer. Thank you svick for laying the groundwork and thank you Ohad for this very useful expansion. – Gordon Bean Sep 21 '16 at 16:34
  • You assign event handlers to some `process` events (`Exited`, `OutputDataReceived`, `ErrorDataReceived`). The handlers aren't removed from the events, so it this code has a memory leak. Please, correct me if I'm wrong. – SuperJMN Jul 04 '18 at 06:44
  • @SuperJMN when the `process` object is collected, so do the only references to the handlers, so they are eligible for collection as well... – Ohad Schneider Jul 04 '18 at 20:13
  • I didn't know that! so the Dispose of process also cleans up the assigned handlers in the events? – SuperJMN Jul 05 '18 at 08:03
  • 1
    @SuperJMN reading the code (https://referencesource.microsoft.com/#System/services/monitoring/system/diagnosticts/Process.cs,ae95910311db8a07) I don't believe `Dispose` nulls the event handler, so theoretically if you called `Dispose` but kept the reference around, I believe that would be a leak. However, when there are no more references to the `Process` object and it gets (garbage) collected, there is no one that points to the event handler list. So it gets collected, and now there are no references to the delegates that used to be in the list, so finally they get garbage collected. – Ohad Schneider Jul 05 '18 at 18:18
  • 1
    @SuperJMN: Interestingly, it's more complicated/powerful than that. For one, `Dispose` cleans up some resources, but doesn't prevent a leaked reference from keeping `process` around. In fact, you'll notice that `process` refers to the handlers, but the `Exited` handler also has a reference to `process`. In some systems, that circular reference would prevent garbage collection, but the algorithm used in .NET would still allow it to all be cleaned up so long as everything lives on an "island" with no outside references. – TheRubberDuck Nov 04 '19 at 17:01
27

In .Net 5.0, there is an official built-in WaitForExitAsync method, so you don't have to implement yourself. Also, Start method now accepts arguments as IEnumerable<string> (which is similar to other programming languages like Python/Golang).

Here is an example:

public static async Task YourMethod() {
    var p = Process.Start("bin_name", new[]{"arg1", "arg2", "arg3"});
    await p.WaitForExitAsync().ConfigureAwait(false);
    // more code;
}
Hieu
  • 7,138
  • 2
  • 42
  • 34
4

I have built a class to start a process and it was growing over the last years because of various requirements. During the usage I found out several issues with the Process class with disposing and even reading the ExitCode. So this is all fixed by my class.

The class has several possibilities, for example reading output, start as Admin or different user, catch Exceptions and also start all this asynchronous incl. Cancellation. Nice is that reading output is also possible during execution.

public class ProcessSettings
{
    public string FileName { get; set; }
    public string Arguments { get; set; } = "";
    public string WorkingDirectory { get; set; } = "";
    public string InputText { get; set; } = null;
    public int Timeout_milliseconds { get; set; } = -1;
    public bool ReadOutput { get; set; }
    public bool ShowWindow { get; set; }
    public bool KeepWindowOpen { get; set; }
    public bool StartAsAdministrator { get; set; }
    public string StartAsUsername { get; set; }
    public string StartAsUsername_Password { get; set; }
    public string StartAsUsername_Domain { get; set; }
    public bool DontReadExitCode { get; set; }
    public bool ThrowExceptions { get; set; }
    public CancellationToken CancellationToken { get; set; }
}

public class ProcessOutputReader   // Optional, to get the output while executing instead only as result at the end
{
    public event TextEventHandler OutputChanged;
    public event TextEventHandler OutputErrorChanged;
    public void UpdateOutput(string text)
    {
        OutputChanged?.Invoke(this, new TextEventArgs(text));
    }
    public void UpdateOutputError(string text)
    {
        OutputErrorChanged?.Invoke(this, new TextEventArgs(text));
    }
    public delegate void TextEventHandler(object sender, TextEventArgs e);
    public class TextEventArgs : EventArgs
    {
        public string Text { get; }
        public TextEventArgs(string text) { Text = text; }
    }
}

public class ProcessResult
{
    public string Output { get; set; }
    public string OutputError { get; set; }
    public int ExitCode { get; set; }
    public bool WasCancelled { get; set; }
    public bool WasSuccessful { get; set; }
}

public class ProcessStarter
{
    public ProcessResult Execute(ProcessSettings settings, ProcessOutputReader outputReader = null)
    {
        return Task.Run(() => ExecuteAsync(settings, outputReader)).GetAwaiter().GetResult();
    }

    public async Task<ProcessResult> ExecuteAsync(ProcessSettings settings, ProcessOutputReader outputReader = null)
    {
        if (settings.FileName == null) throw new ArgumentNullException(nameof(ProcessSettings.FileName));
        if (settings.Arguments == null) throw new ArgumentNullException(nameof(ProcessSettings.Arguments));

        var cmdSwitches = "/Q " + (settings.KeepWindowOpen ? "/K" : "/C");

        var arguments = $"{cmdSwitches} {settings.FileName} {settings.Arguments}";
        var startInfo = new ProcessStartInfo("cmd", arguments)
        {
            UseShellExecute = false,
            RedirectStandardOutput = settings.ReadOutput,
            RedirectStandardError = settings.ReadOutput,
            RedirectStandardInput = settings.InputText != null,
            CreateNoWindow = !(settings.ShowWindow || settings.KeepWindowOpen),
        };
        if (!string.IsNullOrWhiteSpace(settings.StartAsUsername))
        {
            if (string.IsNullOrWhiteSpace(settings.StartAsUsername_Password))
                throw new ArgumentNullException(nameof(ProcessSettings.StartAsUsername_Password));
            if (string.IsNullOrWhiteSpace(settings.StartAsUsername_Domain))
                throw new ArgumentNullException(nameof(ProcessSettings.StartAsUsername_Domain));
            if (string.IsNullOrWhiteSpace(settings.WorkingDirectory))
                settings.WorkingDirectory = Path.GetPathRoot(Path.GetTempPath());

            startInfo.UserName = settings.StartAsUsername;
            startInfo.PasswordInClearText = settings.StartAsUsername_Password;
            startInfo.Domain = settings.StartAsUsername_Domain;
        }
        var output = new StringBuilder();
        var error = new StringBuilder();
        if (!settings.ReadOutput)
        {
            output.AppendLine($"Enable {nameof(ProcessSettings.ReadOutput)} to get Output");
        }
        if (settings.StartAsAdministrator)
        {
            startInfo.Verb = "runas";
            startInfo.UseShellExecute = true;  // Verb="runas" only possible with ShellExecute=true.
            startInfo.RedirectStandardOutput = startInfo.RedirectStandardError = startInfo.RedirectStandardInput = false;
            output.AppendLine("Output couldn't be read when started as Administrator");
        }
        if (!string.IsNullOrWhiteSpace(settings.WorkingDirectory))
        {
            startInfo.WorkingDirectory = settings.WorkingDirectory;
        }
        var result = new ProcessResult();
        var taskCompletionSourceProcess = new TaskCompletionSource<bool>();

        var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true };
        try
        {
            process.OutputDataReceived += (sender, e) =>
            {
                if (e?.Data != null)
                {
                    output.AppendLine(e.Data);
                    outputReader?.UpdateOutput(e.Data);
                }
            };
            process.ErrorDataReceived += (sender, e) =>
            {
                if (e?.Data != null)
                {
                    error.AppendLine(e.Data);
                    outputReader?.UpdateOutputError(e.Data);
                }
            };
            process.Exited += (sender, e) =>
            {
                try { (sender as Process)?.WaitForExit(); } catch (InvalidOperationException) { }
                taskCompletionSourceProcess.TrySetResult(false);
            };

            var success = false;
            try
            {
                process.Start();
                success = true;
            }
            catch (System.ComponentModel.Win32Exception ex)
            {
                if (ex.NativeErrorCode == 1223)
                {
                    error.AppendLine("AdminRights request Cancelled by User!! " + ex);
                    if (settings.ThrowExceptions) taskCompletionSourceProcess.SetException(ex); else taskCompletionSourceProcess.TrySetResult(false);
                }
                else
                {
                    error.AppendLine("Win32Exception thrown: " + ex);
                    if (settings.ThrowExceptions) taskCompletionSourceProcess.SetException(ex); else taskCompletionSourceProcess.TrySetResult(false);
                }
            }
            catch (Exception ex)
            {
                error.AppendLine("Exception thrown: " + ex);
                if (settings.ThrowExceptions) taskCompletionSourceProcess.SetException(ex); else taskCompletionSourceProcess.TrySetResult(false);
            }
            if (success && startInfo.RedirectStandardOutput)
                process.BeginOutputReadLine();
            if (success && startInfo.RedirectStandardError)
                process.BeginErrorReadLine();
            if (success && startInfo.RedirectStandardInput)
            {
                var writeInputTask = Task.Factory.StartNew(() => WriteInputTask());
            }

            async void WriteInputTask()
            {
                var processRunning = true;
                await Task.Delay(50).ConfigureAwait(false);
                try { processRunning = !process.HasExited; } catch { }
                while (processRunning)
                {
                    if (settings.InputText != null)
                    {
                        try
                        {
                            await process.StandardInput.WriteLineAsync(settings.InputText).ConfigureAwait(false);
                            await process.StandardInput.FlushAsync().ConfigureAwait(false);
                            settings.InputText = null;
                        }
                        catch { }
                    }
                    await Task.Delay(5).ConfigureAwait(false);
                    try { processRunning = !process.HasExited; } catch { processRunning = false; }
                }
            }

            if (success && settings.CancellationToken != default(CancellationToken))
                settings.CancellationToken.Register(() => taskCompletionSourceProcess.TrySetResult(true));
            if (success && settings.Timeout_milliseconds > 0)
                new CancellationTokenSource(settings.Timeout_milliseconds).Token.Register(() => taskCompletionSourceProcess.TrySetResult(true));

            var taskProcess = taskCompletionSourceProcess.Task;
            await taskProcess.ConfigureAwait(false);
            if (taskProcess.Result == true) // process was cancelled by token or timeout
            {
                if (!process.HasExited)
                {
                    result.WasCancelled = true;
                    error.AppendLine("Process was cancelled!");
                    try
                    {
                        process.CloseMainWindow();
                        await Task.Delay(30).ConfigureAwait(false);
                        if (!process.HasExited)
                        {
                            process.Kill();
                        }
                    }
                    catch { }
                }
            }
            result.ExitCode = -1;
            if (!settings.DontReadExitCode)     // Reason: sometimes, like when timeout /t 30 is started, reading the ExitCode is only possible if the timeout expired, even if process.Kill was called before.
            {
                try { result.ExitCode = process.ExitCode; }
                catch { output.AppendLine("Reading ExitCode failed."); }
            }
            process.Close();
        }
        finally { var disposeTask = Task.Factory.StartNew(() => process.Dispose()); }    // start in new Task because disposing sometimes waits until the process is finished, for example while executing following command: ping -n 30 -w 1000 127.0.0.1 > nul
        if (result.ExitCode == -1073741510 && !result.WasCancelled)
        {
            error.AppendLine($"Process exited by user!");
        }
        result.WasSuccessful = !result.WasCancelled && result.ExitCode == 0;
        result.Output = output.ToString();
        result.OutputError = error.ToString();
        return result;
    }
}
Coden
  • 2,579
  • 1
  • 18
  • 25
3

Here's another approach. Similar concept to svick and Ohad's answers but using an extension method on the Process type.

Extension method:

public static Task RunAsync(this Process process)
{
    var tcs = new TaskCompletionSource<object>();
    process.EnableRaisingEvents = true;
    process.Exited += (s, e) => tcs.TrySetResult(null);
    // not sure on best way to handle false being returned
    if (!process.Start()) tcs.SetException(new Exception("Failed to start process."));
    return tcs.Task;
}

Example use case in a containing method:

public async Task ExecuteAsync(string executablePath)
{
    using (var process = new Process())
    {
        // configure process
        process.StartInfo.FileName = executablePath;
        process.StartInfo.UseShellExecute = false;
        process.StartInfo.CreateNoWindow = true;
        // run process asynchronously
        await process.RunAsync();
        // do stuff with results
        Console.WriteLine($"Process finished running at {process.ExitTime} with exit code {process.ExitCode}");
    };// dispose process
}
Brandon
  • 1,566
  • 2
  • 14
  • 22
3

I think all you should use is this:

using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace Extensions
{
    public static class ProcessExtensions
    {
        public static async Task<int> WaitForExitAsync(this Process process, CancellationToken cancellationToken = default)
        {
            process = process ?? throw new ArgumentNullException(nameof(process));
            process.EnableRaisingEvents = true;

            var completionSource = new TaskCompletionSource<int>();

            process.Exited += (sender, args) =>
            {
                completionSource.TrySetResult(process.ExitCode);
            };
            if (process.HasExited)
            {
                return process.ExitCode;
            }

            using var registration = cancellationToken.Register(
                () => completionSource.TrySetCanceled(cancellationToken));

            return await completionSource.Task.ConfigureAwait(false);
        }
    }
}

Usage example:

public static async Task<int> StartProcessAsync(ProcessStartInfo info, CancellationToken cancellationToken = default)
{
    path = path ?? throw new ArgumentNullException(nameof(path));
    if (!File.Exists(path))
    {
        throw new ArgumentException(@"File is not exists", nameof(path));
    }

    using var process = Process.Start(info);
    if (process == null)
    {
        throw new InvalidOperationException("Process is null");
    }

    try
    {
        return await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
    }
    catch (OperationCanceledException)
    {
        process.Kill();

        throw;
    }
}
Konstantin S.
  • 1,307
  • 14
  • 19
  • 1
    What's the point of accepting a `CancellationToken`, if canceling it doesn't [`Kill`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.kill) the process? – Theodor Zoulias Apr 18 '20 at 11:30
  • 1
    `CancellationToken` in the `WaitForExitAsync` method is needed simply to be able to cancel a wait or set a timeout. Killing a process can be done in `StartProcessAsync`: ``` try { await process.WaitForExitAsync(cancellationToken); } catch(OperationCanceledException) { process.Kill(); } ``` – Konstantin S. Apr 18 '20 at 21:53
  • 1
    My opinion is that when a method accepts a `CancellationToken`, canceling the token should result to the canceling of the operation, not to the canceling of the awaiting. This is what the caller of the method would normally expect. If the caller wants to cancel just the awaiting, and let the operation still running in the background, it is quite easy to do externally ([here](https://stackoverflow.com/questions/59243161/is-there-a-way-i-can-cause-a-running-method-to-stop-immediately-with-a-cts-cance/59267214#59267214) is an extension method `AsCancelable` that is doing just that). – Theodor Zoulias Apr 18 '20 at 22:16
  • I think that this decision should be made by the caller (specifically for this case, because this method starts with Wait, in general I agree with you), as in the new Usage Example. – Konstantin S. Apr 18 '20 at 22:37
2

In .NET 5 you can call WaitForExitAsync, but in .NET Framework that method doesn't exist.

I would suggest (even if you're using .NET 5+) the CliWrap library which provides async support out of the box (and hopefully handles all the race conditions) and makes it easy to do things like piping and routing output.

I only recently discovered it and I must say I really like it so far!

Silly example:

var cmd = Cli.Wrap(@"C:\test\app.exe")
    .WithArguments("-foo bar")
    .WithStandardOutputPipe(PipeTarget.ToFile(@"C:\test\stdOut.txt"))
    .WithStandardErrorPipe(PipeTarget.ToDelegate(s => Debug.WriteLine(s)));

var result = await cmd.ExecuteAsync(cancellationToken);
Debug.WriteLine(result.ExitCode);
Shahin Dohan
  • 6,149
  • 3
  • 41
  • 58
-1

Im really worried about disposal of process, what about wait for exit async?, this is my proposal (based on previous):

public static class ProcessExtensions
{
    public static Task WaitForExitAsync(this Process process)
    {
        var tcs = new TaskCompletionSource<object>();
        process.EnableRaisingEvents = true;
        process.Exited += (s, e) => tcs.TrySetResult(null);
        return process.HasExited ? Task.CompletedTask : tcs.Task;
    }        
}

Then, use it like this:

public static async Task<int> ExecAsync(string command, string args)
{
    ProcessStartInfo psi = new ProcessStartInfo();
    psi.FileName = command;
    psi.Arguments = args;

    using (Process proc = Process.Start(psi))
    {
        await proc.WaitForExitAsync();
        return proc.ExitCode;
    }
}
iojancode
  • 610
  • 6
  • 7