ChatGPT解决这个技术问题 Extra ChatGPT

Is there any async equivalent of Process.Start?

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");
}
Why won't you just use WaitForExit on the returned Process object?
And by the way, sounds more like you're looking for a "synced" solution, rather than an "async" solution, so the title is misleading.
@YoryeNathan - lol. Indeed, Process.Start is async and the OP appears to want a synchronous version.
The OP is talking about the new async/await keywords in C# 5
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.

s
svick

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

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. github.com/jamesmanning/RunProcessAsTask
This functionality is also available in the MedallionShell NuGet package
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.
@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.
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")
C
Community

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

just found this interesting solution. As I am new to c# I'm not sure how to use the async Task<int> 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 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.
Fantastic answer. Thank you svick for laying the groundwork and thank you Ohad for this very useful expansion.
@SuperJMN reading the code (referencesource.microsoft.com/#System/services/monitoring/…) 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.
@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.
H
Hieu

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

Moving from .net core 3.1 to .net 5
A
Apfelkuacha

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

B
Brandon

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
}

K
Konstantin S.

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

What's the point of accepting a CancellationToken, if canceling it doesn't Kill the process?
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(); } ```
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 is an extension method AsCancelable that is doing just that).
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.
S
Shahin Dohan

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

J
Johann Medina

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