Existe algum equivalente assíncrono do Process.Start?

141

Como o título sugere, existe um equivalente Process.Start(permite executar outro aplicativo ou arquivo em lotes) que eu possa aguardar?

Estou jogando com um pequeno aplicativo de console e este parecia o lugar perfeito para usar assíncrono e aguardar, mas não consigo encontrar nenhuma documentação para esse cenário.

O que estou pensando é algo nesse sentido:

void async RunCommand()
{
    var result = await Process.RunAsync("command to run");
}
linkerro
fonte
2
Por que você não usa apenas WaitForExit no objeto Process retornado?
SimpleVar
2
E, a propósito, parece mais que você está procurando uma solução "sincronizada", em vez de uma solução "assíncrona"; portanto, o título é enganoso.
SimpleVar 28/05
2
@YoryeNathan - lol. De fato, Process.Start é assíncrono e o OP parece querer uma versão síncrona.
Oded
10
O OP está falando sobre as novas assíncronos / palavras-chave aguardam em C # 5
aquinas
4
Ok, atualizei minha postagem para ficar um pouco mais clara. A explicação de por que eu quero isso é simples. Imagine um cenário em que você precise executar um comando externo (algo como 7zip) e continue o fluxo do aplicativo. Isso é exatamente o que o assíncrono / espera espera facilitar e, no entanto, parece não haver maneira de executar um processo e aguardar sua saída.
Linkerro 29/05

Respostas:

196

Process.Start()apenas inicia o processo, não espera até terminar, portanto, não faz muito sentido fazê-lo async. Se você ainda deseja fazer isso, pode fazer algo assim await Task.Run(() => Process.Start(fileName)).

Mas, se você quiser aguardar assincronamente o processo terminar, poderá usar o Exitedevento junto com 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
fonte
36
Finalmente cheguei a colocar algo no github para isso - ele não tem suporte para cancelamento / tempo limite, mas reunirá a saída padrão e o erro padrão para você, pelo menos. github.com/jamesmanning/RunProcessAsTask
James Manning
3
Essa funcionalidade também está disponível no pacote NuGet do MedallionShell
ChaseMedallion
8
Realmente importante: a ordem em que você define as várias propriedades processe process.StartInfoaltera o que acontece quando você as executa .Start(). Se você, por exemplo, telefonar .EnableRaisingEvents = trueantes de definir as StartInfopropriedades conforme visto aqui, as coisas funcionarão conforme o esperado. Se você configurá-lo mais tarde, por exemplo, para mantê-lo junto .Exited, mesmo que você o tenha chamado antes .Start(), ele não funciona corretamente - é .Exitedacionado imediatamente, em vez de esperar que o Processo realmente saia. Não sei por que, apenas uma palavra de cautela.
Chris Moschini
2
@svick No formulário da janela, process.SynchronizingObjectdeve ser definido como componente de formulários para evitar que métodos que manipulam eventos (como Exited, OutputDataReceived, ErrorDataReceived) sejam chamados em threads separados.
KevinBui
4
Ele não realmente faz sentido para embrulhar Process.Startem Task.Run. Um caminho UNC, por exemplo, será resolvido de forma síncrona. Esse snippet pode levar até 30 segundos para ser concluído:Process.Start(@"\\live.sysinternals.com\whatever")
Jabe 19/06
55

Aqui está a minha opinião, com base na resposta de svick . Ele adiciona redirecionamento de saída, retenção de código de saída e tratamento de erros um pouco melhor (descartando o Processobjeto mesmo que não possa ser iniciado):

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;
}
Ohad Schneider
fonte
1
acabou de encontrar esta solução interessante. Como eu sou novo no c #, não sei como usar o async Task<int> RunProcessAsync(string fileName, string args). Eu adaptei este exemplo e passo três objetos um por um. Como posso esperar para levantar eventos? por exemplo. diante dos meus Stopps aplicação .. muito obrigado
marrrschine
3
@ marrrschine Não entendo exatamente o que você quer dizer, talvez você deva iniciar uma nova pergunta com algum código para que possamos ver o que você tentou e continuar a partir daí.
Ohad Schneider
4
Resposta fantástica. Obrigado svick por lançar as bases e obrigado Ohad por essa expansão muito útil.
Gordon Bean
1
@SuperJMN lendo o código ( referenceource.microsoft.com/#System/services/monitoring/… ) Não acredito que Disposeanule o manipulador de eventos; portanto, teoricamente, se você ligasse, Disposemas mantivesse a referência, acredito que seria um vazamento. No entanto, quando não há mais referências ao Processobjeto e ele é coletado (lixo), não há ninguém que aponte para a lista de manipuladores de eventos. Portanto, ele é coletado e agora não há referências aos delegados que estavam na lista; portanto, eles são coletados como lixo.
Ohad Schneider
1
@SuperJMN: Curiosamente, é mais complicado / poderoso do que isso. Por um lado, Disposelimpa alguns recursos, mas não impede que uma referência vazada permaneça por processperto. De fato, você notará que processse refere aos manipuladores, mas o Exitedmanipulador também tem uma referência a process. Em alguns sistemas, essa referência circular impediria a coleta de lixo, mas o algoritmo usado no .NET ainda permitiria que tudo fosse limpo, desde que tudo viva em uma "ilha" sem referências externas.
TheRubberDuck
4

Aqui está outra abordagem. Conceito semelhante às respostas de svick e Ohad, mas usando um método de extensão no Processtipo.

Método de extensão:

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

Exemplo de caso de uso em um método que contém:

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
fonte
4

Eu criei uma classe para iniciar um processo e ele estava crescendo nos últimos anos devido a vários requisitos. Durante o uso, descobri vários problemas com a classe Process ao descartar e até ler o ExitCode. Então, tudo isso foi corrigido pela minha turma.

A classe tem várias possibilidades, por exemplo, ler saída, iniciar como administrador ou usuário diferente, capturar exceções e também iniciar tudo isso incl. Cancelamento. Bom é que a saída de leitura também é possível durante a execução.

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;
    }
}
Apfelkuacha
fonte
1

Eu acho que tudo que você deve usar é o seguinte:

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

Exemplo de uso:

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.
fonte
Qual é o sentido de aceitar um CancellationToken, se cancelá-lo não é Killo processo?
Theodor Zoulias
CancellationTokenno WaitForExitAsyncmétodo é necessário simplesmente para poder cancelar uma espera ou definir um tempo limite. A matança de um processo pode ser feita em StartProcessAsync: `` `tente {aguarde processo.WaitForExitAsync (cancellationToken); } catch (OperationCanceledException) {process.Kill (); } `` `
Konstantin S.
Minha opinião é que, quando um método aceita a CancellationToken, o cancelamento do token deve resultar no cancelamento da operação, não no cancelamento da espera. Isso é o que o chamador do método normalmente esperaria. Se o chamador deseja cancelar apenas a espera e deixar a operação ainda em execução em segundo plano, é bastante fácil fazer isso externamente ( aqui está um método de extensão AsCancelableque está fazendo exatamente isso).
Theodor Zoulias
Penso que esta decisão deve ser tomada pelo chamador (especificamente neste caso, porque esse método começa com Aguardar, em geral, concordo com você), como no novo Exemplo de uso.
Konstantin S.
0

Estou realmente preocupado com a eliminação do processo, que tal esperar pela saída assíncrona ?, esta é a minha proposta (com base no anterior):

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

Em seguida, use-o assim:

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;
    }
}
Johann Medina
fonte