¿Hay algún equivalente asíncrono de Process.Start?

141

Como sugiere el título, ¿hay un equivalente a Process.Start(le permite ejecutar otra aplicación o archivo por lotes) que pueda esperar?

Estoy jugando con una pequeña aplicación de consola y este parecía ser el lugar perfecto para usar async y esperar, pero no puedo encontrar ninguna documentación para este escenario.

Lo que estoy pensando es algo en este sentido:

void async RunCommand()
{
    var result = await Process.RunAsync("command to run");
}
linkerro
fuente
2
¿Por qué no usarás WaitForExit en el objeto Process devuelto?
SimpleVar
2
Y, por cierto, suena más como si estuviera buscando una solución "sincronizada", en lugar de una solución "asíncrona", por lo que el título es engañoso.
SimpleVar
2
@YoryeNathan - jajaja. De hecho, Process.Start es asíncrono y el OP parece querer una versión sincrónica.
Oded
10
El OP está hablando de las nuevas palabras clave asíncronas / aguardadas en C # 5
Aquino
44
Ok, he actualizado mi publicación para ser un poco más clara. La explicación de por qué quiero esto es simple. Imagine un escenario en el que debe ejecutar un comando externo (algo así como 7zip) y luego continuar el flujo de la aplicación. Esto es exactamente lo que async / await estaba destinado a facilitar y, sin embargo, parece que no hay forma de ejecutar un proceso y esperar su salida.
linkerro

Respuestas:

196

Process.Start()solo comienza el proceso, no espera hasta que termine, por lo que no tiene mucho sentido hacerlo async. Si todavía quieres hacerlo, puedes hacer algo como await Task.Run(() => Process.Start(fileName)).

Pero, si desea esperar asincrónicamente a que finalice el proceso, puede usar el Exitedevento junto con 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
fuente
36
Finalmente pude poner algo en github para esto: no tiene ningún soporte de cancelación / tiempo de espera, pero al menos reunirá la salida estándar y el error estándar para usted. github.com/jamesmanning/RunProcessAsTask
James Manning
3
Esta funcionalidad también está disponible en el paquete MedallionShell NuGet
ChaseMedallion
8
Realmente importante: el orden en el que establece las diferentes propiedades processy process.StartInfocambia lo que sucede cuando lo ejecuta .Start(). Si, por ejemplo, llama .EnableRaisingEvents = trueantes de configurar las StartInfopropiedades como se ve aquí, las cosas funcionan como se espera. Si lo configura más tarde, por ejemplo, para mantenerlo junto .Exited, aunque lo llame antes .Start(), no funciona correctamente: se .Exiteddispara de inmediato en lugar de esperar a que el Proceso se cierre realmente. No sé por qué, solo una palabra de precaución.
Chris Moschini
2
@svick En el formulario de ventana, process.SynchronizingObjectdebe establecerse en el componente de formularios para evitar que los métodos que manejan eventos (como Exited, OutputDataReceived, ErrorDataReceived) se invoquen en subprocesos separados.
KevinBui
44
Se hace realidad tiene sentido para envolver Process.Starten Task.Run. Una ruta UNC, por ejemplo, se resolverá sincrónicamente. Este fragmento puede tardar hasta 30 segundos en completarse:Process.Start(@"\\live.sysinternals.com\whatever")
Jabe
55

Aquí está mi opinión, basada en la respuesta de svick . Agrega redirección de salida, retención de código de salida y un manejo de errores ligeramente mejor (desechando el Processobjeto incluso si no se pudo iniciar):

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
fuente
1
Acabo de encontrar esta solución interesante. Como soy nuevo en c #, no estoy seguro de cómo usar el async Task<int> RunProcessAsync(string fileName, string args). Adapte este ejemplo y paso tres objetos uno por uno. ¿Cómo puedo esperar para levantar eventos? p.ej. antes de que mi aplicación se detenga ... muchas gracias
marrrschine
3
@marrrschine No entiendo exactamente lo que quieres decir, tal vez deberías comenzar una nueva pregunta con algún código para que podamos ver lo que intentaste y continuar desde allí.
Ohad Schneider
44
Fantástica respuesta. Gracias svick por sentar las bases y gracias Ohad por esta expansión tan útil.
Gordon Bean
1
@SuperJMN leyendo el código ( referencesource.microsoft.com/#System/services/monitoring/… ) No creo que Disposeanule el controlador de eventos, por lo que, en teoría, si llamó Disposepero mantuvo la referencia, creo que sería una fuga. Sin embargo, cuando no hay más referencias al Processobjeto y se obtiene (basura), no hay nadie que apunte a la lista de controladores de eventos. Por lo tanto, se recolecta, y ahora no hay referencias a los delegados que solían estar en la lista, por lo que finalmente se recolecta la basura.
Ohad Schneider
1
@SuperJMN: Curiosamente, es más complicado / poderoso que eso. Por un lado, Disposelimpia algunos recursos, pero no evita que se guarde una referencia filtrada process. De hecho, notará que se processrefiere a los controladores, pero el Exitedcontrolador también tiene una referencia process. En algunos sistemas, esa referencia circular evitaría la recolección de basura, pero el algoritmo utilizado en .NET aún permitiría que todo se limpiara siempre que todo viva en una "isla" sin referencias externas.
TheRubberDuck
4

Aquí hay otro enfoque. Concepto similar a las respuestas de svick y Ohad pero usando un método de extensión en el Processtipo.

Método de extensión:

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

Ejemplo de caso de uso en un método contenedor:

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

He creado una clase para comenzar un proceso y fue creciendo en los últimos años debido a varios requisitos. Durante el uso descubrí varios problemas con la clase Process con la eliminación e incluso la lectura del ExitCode. Así que todo está arreglado por mi clase.

La clase tiene varias posibilidades, por ejemplo, leer la salida, comenzar como administrador o usuario diferente, capturar excepciones y también iniciar todo esto asíncrono incl. Cancelación. Lo bueno es que la salida de lectura también es posible durante la ejecución.

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
fuente
1

Creo que todo lo que debes usar es esto:

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

Ejemplo 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.
fuente
¿Cuál es el punto de aceptar un CancellationToken, si cancelarlo no es Killel proceso?
Theodor Zoulias
CancellationTokenen el WaitForExitAsyncmétodo es necesario simplemente para poder cancelar una espera o establecer un tiempo de espera. La eliminación de un proceso se puede realizar en StartProcessAsync: `` `` try {await process.WaitForExitAsync (cancellationToken); } catch (OperationCanceledException) {process.Kill (); } `` `
Konstantin S.
Mi opinión es que cuando un método acepta un CancellationToken, cancelar el token debería resultar en la cancelación de la operación, no en la cancelación de la espera. Esto es lo que la persona que llama del método normalmente esperaría. Si la persona que llama desea cancelar solo la espera y dejar que la operación siga ejecutándose en segundo plano, es bastante fácil hacerlo externamente ( aquí hay un método de extensión AsCancelableque está haciendo exactamente eso).
Theodor Zoulias
Creo que la persona que llama debe tomar esta decisión (específicamente para este caso, porque este método comienza con Wait, en general estoy de acuerdo con usted), como en el nuevo Ejemplo de uso.
Konstantin S.
0

Estoy realmente preocupado por la eliminación del proceso, ¿qué pasa con la espera de salida asíncrona ?, esta es mi propuesta (basada en la 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;
    }        
}

Luego, úsalo así:

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
fuente