Gibt es ein asynchrones Äquivalent zu Process.Start?

141

Gibt es, wie der Titel schon sagt, ein Äquivalent zu Process.Start(ermöglicht das Ausführen einer anderen Anwendung oder Batchdatei), auf das ich warten kann?

Ich spiele mit einer kleinen Konsolen-App und dies schien der perfekte Ort zu sein, um Async zu verwenden und zu warten, aber ich kann keine Dokumentation für dieses Szenario finden.

Was ich denke, ist etwas in diese Richtung:

void async RunCommand()
{
    var result = await Process.RunAsync("command to run");
}
Linkerro
quelle
2
Warum verwenden Sie WaitForExit nicht einfach für das zurückgegebene Process-Objekt?
SimpleVar
2
Übrigens klingt es eher so, als ob Sie nach einer "synchronisierten" Lösung suchen, als nach einer "asynchronen" Lösung, sodass der Titel irreführend ist.
SimpleVar
2
@YoryeNathan - lol. In der Tat Process.Start ist asynchron und das OP scheint eine synchrone Version zu wollen.
Oded
10
Das OP spricht über die neuen Schlüsselwörter async / await in C # 5
Aquinas
4
Ok, ich habe meinen Beitrag aktualisiert, um ein bisschen klarer zu sein. Die Erklärung, warum ich das will, ist einfach. Stellen Sie sich ein Szenario vor, in dem Sie einen externen Befehl (etwa 7zip) ausführen und dann den Ablauf der Anwendung fortsetzen müssen. Dies ist genau das, was async / await erleichtern sollte, und dennoch scheint es keine Möglichkeit zu geben, einen Prozess auszuführen und auf sein Beenden zu warten.
Linkerro

Antworten:

196

Process.Start()Startet nur den Prozess, wartet nicht, bis er abgeschlossen ist, daher macht es wenig Sinn, ihn zu erstellen async. Wenn Sie es trotzdem tun möchten, können Sie so etwas tun await Task.Run(() => Process.Start(fileName)).

Wenn Sie jedoch asynchron auf den Abschluss des Prozesses warten möchten, können Sie das ExitedEreignis zusammen mit TaskCompletionSourcefolgenden Elementen verwenden :

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
quelle
36
Ich habe mich endlich daran gemacht, etwas auf Github zu setzen - es gibt keine Unterstützung für Stornierung / Timeout, aber es wird zumindest die Standardausgabe und den Standardfehler für Sie erfassen. github.com/jamesmanning/RunProcessAsTask
James Manning
3
Diese Funktionalität ist auch im MedallionShell NuGet-Paket verfügbar
ChaseMedallion
8
Wirklich wichtig: Die Reihenfolge, in der Sie die verschiedenen Eigenschaften festlegen processund process.StartInfoändern, was passiert, wenn Sie es ausführen .Start(). Wenn Sie beispielsweise anrufen, .EnableRaisingEvents = truebevor Sie die hier gezeigten StartInfoEigenschaften festlegen, funktionieren die Dinge wie erwartet. Wenn Sie es später einstellen, um es beispielsweise zusammenzuhalten .Exited, obwohl es zuvor aufgerufen wurde .Start(), funktioniert es nicht richtig - .Exitedwird sofort ausgelöst, anstatt darauf zu warten, dass der Prozess tatsächlich beendet wird. Ich weiß nicht warum, nur ein Wort der Vorsicht.
Chris Moschini
2
@svick Im Fensterformular process.SynchronizingObjectsollte die Formularkomponente festgelegt werden, um zu vermeiden, dass Methoden, die Ereignisse verarbeiten (z. B. Beendet, OutputDataReceived, ErrorDataReceived), für einen getrennten Thread aufgerufen werden.
KevinBui
4
Es ist wirklich sinnvoll zu wickeln Process.Startin Task.Run. Ein UNC-Pfad wird beispielsweise synchron aufgelöst. Dieser Ausschnitt kann bis zu 30 Sekunden dauern:Process.Start(@"\\live.sysinternals.com\whatever")
Jabe
55

Hier ist meine Einstellung , basierend auf der Antwort von svick . Es fügt eine Umleitung der Ausgabe, eine Beibehaltung des Exit-Codes und eine etwas bessere Fehlerbehandlung hinzu (Entsorgen des ProcessObjekts, auch wenn es nicht gestartet werden konnte):

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
quelle
1
habe gerade diese interessante Lösung gefunden. Da ich neu bei c # bin, bin ich mir nicht sicher, wie ich das verwenden soll async Task<int> RunProcessAsync(string fileName, string args). Ich habe dieses Beispiel angepasst und drei Objekte einzeln übergeben. Wie kann ich auf das Sammeln von Ereignissen warten? z.B. bevor meine Bewerbung endet .. vielen Dank
marrrschine
3
@marrrschine Ich verstehe nicht genau, was du meinst. Vielleicht solltest du eine neue Frage mit etwas Code beginnen, damit wir sehen können, was du versucht hast, und von dort aus fortfahren.
Ohad Schneider
4
Fantastische Antwort. Vielen Dank an svick für die Grundlagen und an Ohad für diese sehr nützliche Erweiterung.
Gordon Bean
1
@SuperJMN beim Lesen des Codes ( referencesource.microsoft.com/#System/services/monitoring/… ) Ich glaube nicht, Disposedass der Ereignishandler auf Null gesetzt wird. Wenn Sie also Disposedie Referenz aufrufen, aber behalten, glaube ich, dass dies ein Leck wäre. Wenn jedoch keine Verweise mehr auf das ProcessObjekt vorhanden sind und es (Müll) gesammelt wird, gibt es niemanden, der auf die Ereignishandlerliste verweist. Es wird also gesammelt, und jetzt gibt es keine Verweise auf die Delegierten, die früher in der Liste waren, sodass sie schließlich Müll gesammelt bekommen.
Ohad Schneider
1
@ SuperJMN: Interessanterweise ist es komplizierter / leistungsfähiger als das. Zum einen Disposewerden einige Ressourcen bereinigt, aber es wird nicht verhindert, dass eine durchgesickerte Referenz in der Nähe bleibt process. Sie werden feststellen, dass processsich dies auf die Handler bezieht, der Handler jedoch Exitedauch auf process. In einigen Systemen würde diese Zirkelreferenz die Speicherbereinigung verhindern, aber der in .NET verwendete Algorithmus würde es dennoch ermöglichen, alles zu bereinigen, solange alles auf einer "Insel" ohne externe Referenzen lebt.
TheRubberDuck
4

Hier ist ein anderer Ansatz. Ähnliches Konzept wie die Antworten von svick und Ohad, jedoch unter Verwendung einer Erweiterungsmethode für den ProcessTyp.

Verlängerungsmethode:

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

Beispiel für einen Anwendungsfall in einer enthaltenen Methode:

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

Ich habe eine Klasse aufgebaut, um einen Prozess zu starten, und sie ist in den letzten Jahren aufgrund verschiedener Anforderungen gewachsen. Während der Verwendung habe ich einige Probleme mit der Process-Klasse beim Entsorgen und sogar Lesen des ExitCodes festgestellt. Das ist also alles von meiner Klasse festgelegt.

Die Klasse hat mehrere Möglichkeiten, zum Beispiel das Lesen der Ausgabe, das Starten als Administrator oder ein anderer Benutzer, das Abfangen von Ausnahmen und das Starten all dieser asynchronen Inkl. Stornierung. Schön ist, dass das Lesen der Ausgabe auch während der Ausführung möglich ist.

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

Ich denke, alles, was Sie verwenden sollten, ist Folgendes:

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

Anwendungsbeispiel:

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.
quelle
Was CancellationTokenbringt es , ein zu akzeptieren , wenn das Abbrechen nicht Killder Prozess ist?
Theodor Zoulias
CancellationTokenin der WaitForExitAsyncMethode wird einfach benötigt, um eine Wartezeit abbrechen oder eine Zeitüberschreitung einstellen zu können. Das Beenden eines Prozesses kann erfolgen in StartProcessAsync: `` `try {await process.WaitForExitAsync (cancellationToken); } catch (OperationCanceledException) {process.Kill (); } `` `
Konstantin S.
Meiner Meinung CancellationTokennach sollte das Abbrechen des Tokens zum Abbrechen des Vorgangs führen, wenn eine Methode a akzeptiert , und nicht zum Abbrechen des Wartens. Dies würde der Aufrufer der Methode normalerweise erwarten. Wenn der Anrufer nur das Warten abbrechen und den Vorgang weiterhin im Hintergrund ausführen möchte, ist dies recht einfach extern ( hier ist eine Erweiterungsmethode AsCancelable, die genau das tut).
Theodor Zoulias
Ich denke, dass diese Entscheidung vom Anrufer getroffen werden sollte (speziell für diesen Fall, da diese Methode mit Warten beginnt, im Allgemeinen stimme ich Ihnen zu), wie im neuen Verwendungsbeispiel.
Konstantin S.
0

Ich bin wirklich besorgt über die Entsorgung des Prozesses, was ist mit dem Warten auf asynchrones Beenden? Dies ist mein Vorschlag (basierend auf dem vorherigen):

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

Verwenden Sie es dann folgendermaßen:

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
quelle