Esiste un equivalente asincrono di Process.Start?


141

Come suggerisce il titolo, c'è un equivalente a Process.Start(ti permette di eseguire un'altra applicazione o file batch) che posso aspettare?

Sto giocando con una piccola app per console e questo sembrava il posto perfetto per utilizzare asincronizzazione e attendere ma non riesco a trovare alcuna documentazione per questo scenario.

Quello che sto pensando è qualcosa del genere:

void async RunCommand()
{
    var result = await Process.RunAsync("command to run");
}

2
Perché non usi semplicemente WaitForExit sull'oggetto Process restituito?
SimpleVar,

2
E a proposito, sembra più che tu stia cercando una soluzione "sincronizzata", piuttosto che una soluzione "asincrona", quindi il titolo è fuorviante.
SimpleVar,

2
@YoryeNathan - lol. In effetti, Process.Start è asincrono e l'OP sembra voler una versione sincrona.
Oded,

10
L'OP sta parlando delle nuove parole chiave asincrono / attesa in C # 5
aquinas

4
Ok, ho aggiornato il mio post per essere un po 'più chiaro. La spiegazione del perché lo voglio è semplice. Immagina uno scenario in cui devi eseguire un comando esterno (qualcosa come 7zip) e quindi continuare il flusso dell'applicazione. Questo è esattamente ciò che async / wait era destinato a facilitare e tuttavia non sembra esserci alcun modo per eseguire un processo e attendere la sua uscita.
Linkerro,

Risposte:


197

Process.Start()avvia solo il processo, non aspetta fino al termine, quindi non ha molto senso farlo async. Se vuoi ancora farlo, puoi fare qualcosa del genere await Task.Run(() => Process.Start(fileName)).

Ma, se si desidera attendere in modo asincrono il completamento del processo, è possibile utilizzare l' Exitedevento insieme a 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;
}

36
Finalmente sono riuscito ad attaccare qualcosa su Github per questo - non ha alcun supporto di annullamento / timeout, ma raccoglierà l'output standard e l'errore standard per te, almeno. github.com/jamesmanning/RunProcessAsTask
James Manning

3
Questa funzionalità è disponibile anche nel pacchetto NuGet di MedallionShell
ChaseMedallion,

8
Davvero importante: l'ordine in cui si impostano le varie proprietà processe process.StartInfocambia ciò che accade quando lo si esegue .Start(). Se ad esempio si chiama .EnableRaisingEvents = trueprima di impostare le StartInfoproprietà come mostrato qui, le cose funzionano come previsto. Se lo imposti in un secondo momento, ad esempio per mantenerlo insieme .Exited, anche se lo chiami prima .Start(), non funziona correttamente - si .Exitedattiva immediatamente anziché attendere che il processo venga effettivamente chiuso. Non so perché, solo una parola di cautela.
Chris Moschini,

2
@svick Nel modulo finestra, process.SynchronizingObjectdeve essere impostato sul componente moduli per evitare che i metodi che gestiscono gli eventi (come Exited, OutputDataReceived, ErrorDataReceived) vengano chiamati su thread separati.
KevinBui,

4
Essa ha in realtà ha senso per avvolgere Process.Startin Task.Run. Un percorso UNC, ad esempio, verrà risolto in modo sincrono. Il completamento di questo frammento può richiedere fino a 30 secondi:Process.Start(@"\\live.sysinternals.com\whatever")
Jabe,

55

Ecco la mia opinione , basata sulla risposta di svick . Aggiunge il reindirizzamento dell'output, la conservazione del codice di uscita e una gestione degli errori leggermente migliore (eliminando l' Processoggetto anche se non è stato possibile avviarlo):

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

1
ho appena trovato questa soluzione interessante. Dato che sono nuovo in c # non sono sicuro di come usare async Task<int> RunProcessAsync(string fileName, string args). Ho adattato questo esempio e ho passato tre oggetti uno per uno. Come posso aspettare la raccolta di eventi? per esempio. prima che la mia domanda si interrompa .. grazie mille
marrrschine,

3
@marrrschine Non capisco esattamente cosa intendi, forse dovresti iniziare una nuova domanda con del codice in modo che possiamo vedere cosa hai provato e continuare da lì.
Ohad Schneider,

4
Risposta fantastica. Grazie svick per aver posto le basi e grazie Ohad per questa utilissima espansione.
Gordon Bean,

1
@SuperJMN leggendo il codice ( riferimentiource.microsoft.com/#System/services/monitoring/… ) Non credo che Disposeannulli il gestore dell'evento, quindi teoricamente se tu chiamassi Disposema conservassi il riferimento, credo che sarebbe una perdita. Tuttavia, quando non ci sono più riferimenti Processall'oggetto e viene raccolto (immondizia), non c'è nessuno che punti all'elenco dei gestori di eventi. Quindi viene raccolto e ora non ci sono riferimenti ai delegati che erano nell'elenco, quindi alla fine vengono raccolti i rifiuti.
Ohad Schneider,

1
@SuperJMN: È interessante notare che è più complicato / potente di così. Per uno, Disposepulisce alcune risorse, ma non impedisce a un riferimento trapelato di rimanere in processgiro. In effetti, noterai che si processriferisce ai gestori, ma anche il Exitedgestore ha un riferimento process. In alcuni sistemi, quel riferimento circolare impedirebbe la garbage collection, ma l'algoritmo utilizzato in .NET consentirebbe comunque di ripulirlo tutto finché tutto vive su un '"isola" senza riferimenti esterni.
TheRubberDuck

4

Ecco un altro approccio. Concetto simile a svick e alle risposte di Ohad ma utilizzando un metodo di estensione sul Processtipo.

Metodo di estensione:

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

Esempio di caso d'uso in un metodo contenente:

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
}

4

Ho creato una classe per avviare un processo e negli ultimi anni è cresciuto a causa di vari requisiti. Durante l'uso ho scoperto diversi problemi con la classe Process con lo smaltimento e persino la lettura di ExitCode. Quindi questo è tutto risolto dalla mia classe.

La classe ha diverse possibilità, ad esempio la lettura dell'output, l'avvio come amministratore o utente diverso, rilevare le eccezioni e avviare anche tutto questo incl. Cancellazione. Bello è che l'output di lettura è possibile anche durante l'esecuzione.

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

1

Penso che tutto ciò che dovresti usare è questo:

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

Esempio di utilizzo:

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

Qual è il punto di accettare a CancellationToken, se la cancellazione non avviene Kill?
Theodor Zoulias,

CancellationTokennel WaitForExitAsyncmetodo è necessario semplicemente per poter annullare un'attesa o impostare un timeout. L'eliminazione di un processo può essere eseguita in StartProcessAsync: `` provare {waitit process.WaitForExitAsync (cancellazioneToken); } catch (OperationCanceledException) {process.Kill (); } ``
Konstantin S.

La mia opinione è che quando un metodo accetta un CancellationToken, l'annullamento del token dovrebbe comportare l'annullamento dell'operazione, non l'annullamento dell'attesa. Questo è ciò che il chiamante del metodo normalmente si aspetterebbe. Se il chiamante vuole annullare solo l'attesa e lasciare che l'operazione sia ancora in esecuzione in background, è abbastanza facile eseguire esternamente ( ecco un metodo di estensione AsCancelableche sta facendo proprio questo).
Theodor Zoulias,

Penso che questa decisione dovrebbe essere presa dal chiamante (in particolare per questo caso, perché questo metodo inizia con Wait, in generale sono d'accordo con te), come nel nuovo Esempio di utilizzo.
Konstantin S.

0

Sono davvero preoccupato per lo smaltimento del processo, per quanto riguarda l'attesa dell'uscita asincrona? Questa è la mia proposta (basata sul precedente):

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

Quindi, usalo in questo modo:

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;
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.