Existe-t-il un équivalent asynchrone de Process.Start?

141

Comme le titre l'indique, y a-t-il un équivalent à Process.Start(vous permet d'exécuter une autre application ou un autre fichier batch) que je peux attendre?

Je joue avec une petite application console et cela semblait être l'endroit idéal pour utiliser async et attendre, mais je ne trouve aucune documentation pour ce scénario.

Ce que je pense, c'est quelque chose du genre:

void async RunCommand()
{
    var result = await Process.RunAsync("command to run");
}
linkerro
la source
2
Pourquoi n'utilisez-vous pas simplement WaitForExit sur l'objet Process renvoyé?
SimpleVar
2
Et au fait, on dirait plus que vous recherchez une solution «synchronisée» plutôt qu'une solution «asynchrone», le titre est donc trompeur.
SimpleVar
2
@YoryeNathan - lol. En effet, Process.Start c'est asynchrone et l'OP semble vouloir une version synchrone.
Oded
10
L'OP parle des nouveaux mots clés async /
await
4
Ok, j'ai mis à jour mon message pour être un peu plus clair. L'explication de la raison pour laquelle je veux cela est simple. Imaginez un scénario dans lequel vous devez exécuter une commande externe (quelque chose comme 7zip), puis continuer le flux de l'application. C'est exactement ce que async / await était censé faciliter et pourtant il ne semble y avoir aucun moyen d'exécuter un processus et d'attendre sa sortie.
linkerro

Réponses:

197

Process.Start()ne fait que démarrer le processus, il n'attend pas qu'il se termine, donc cela n'a pas beaucoup de sens de le faire async. Si vous voulez toujours le faire, vous pouvez faire quelque chose comme await Task.Run(() => Process.Start(fileName)).

Mais, si vous souhaitez attendre de manière asynchrone la fin du processus, vous pouvez utiliser l' Exitedévénement avec 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
la source
36
J'ai finalement réussi à coller quelque chose sur github pour cela - il n'a pas de support d'annulation / timeout, mais il rassemblera au moins la sortie standard et l'erreur standard pour vous. github.com/jamesmanning/RunProcessAsTask
James Manning
3
Cette fonctionnalité est également disponible dans le package NuGet MedallionShell
ChaseMedallion
8
Vraiment important: l'ordre dans lequel vous définissez les différentes propriétés processet process.StartInfochange ce qui se passe lorsque vous l'exécutez avec .Start(). Si vous appelez par exemple .EnableRaisingEvents = trueavant de définir les StartInfopropriétés comme indiqué ici, les choses fonctionnent comme prévu. Si vous le définissez plus tard, par exemple pour le conserver avec .Exited, même si vous l'appelez auparavant .Start(), il ne fonctionne pas correctement - se .Exiteddéclenche immédiatement au lieu d'attendre que le processus se termine réellement. Je ne sais pas pourquoi, juste un mot d'avertissement.
Chris Moschini
2
@svick Dans la forme de fenêtre, process.SynchronizingObjectdoit être défini sur le composant de formulaires pour éviter que les méthodes qui gèrent les événements (comme Exited, OutputDataReceived, ErrorDataReceived) ne soient appelées sur un thread séparé.
KevinBui
4
Il ne fait réellement sens pour envelopper Process.Startdans Task.Run. Un chemin UNC, par exemple, sera résolu de manière synchrone. Cet extrait peut prendre jusqu'à 30 secondes pour se terminer:Process.Start(@"\\live.sysinternals.com\whatever")
Jabe
55

Voici mon avis, basé sur la réponse de svick . Il ajoute la redirection de sortie, la rétention du code de sortie et une gestion des erreurs légèrement meilleure (en supprimant l' Processobjet même s'il n'a pas pu être démarré):

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
la source
1
vient de trouver cette solution intéressante. Comme je suis nouveau dans c #, je ne sais pas comment utiliser le async Task<int> RunProcessAsync(string fileName, string args). J'ai adapté cet exemple et passé trois objets un par un. Comment puis-je attendre de lever des événements? par exemple. avant que mon application ne s'arrête .. merci beaucoup
marrrschine
3
@marrrschine Je ne comprends pas exactement ce que vous voulez dire, vous devriez peut-être commencer une nouvelle question avec du code pour que nous puissions voir ce que vous avez essayé et continuer à partir de là.
Ohad Schneider du
4
Réponse fantastique. Merci svick pour jeter les bases et merci Ohad pour cette extension très utile.
Gordon Bean
1
@SuperJMN en lisant le code ( referencesource.microsoft.com/#System/services/monitoring/… ) Je ne crois pas que Disposele gestionnaire d'événements annule, donc théoriquement si vous avez appelé Disposemais gardé la référence, je pense que ce serait une fuite. Cependant, lorsqu'il n'y a plus de références à l' Processobjet et qu'il est (garbage) collecté, personne ne pointe vers la liste des gestionnaires d'événements. Donc, il est collecté, et maintenant il n'y a plus de références aux délégués qui étaient dans la liste, donc finalement ils sont récupérés.
Ohad Schneider
1
@SuperJMN: Fait intéressant, c'est plus compliqué / puissant que cela. D'une part, Disposenettoie certaines ressources, mais n'empêche pas une référence divulguée de rester process. En fait, vous remarquerez que cela processfait référence aux gestionnaires, mais le Exitedgestionnaire a également une référence à process. Dans certains systèmes, cette référence circulaire empêcherait le garbage collection, mais l'algorithme utilisé dans .NET permettrait toujours de tout nettoyer tant que tout vit sur un «îlot» sans références extérieures.
TheRubberDuck
4

Voici une autre approche. Concept similaire aux réponses de svick et Ohad mais en utilisant une méthode d'extension sur leProcess type.

Méthode d'extension:

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

Exemple de cas d'utilisation dans une méthode contenant:

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
la source
4

J'ai construit une classe pour démarrer un processus et elle s'est développée au cours des dernières années en raison de diverses exigences. Pendant l'utilisation, j'ai découvert plusieurs problèmes avec la classe Process avec l'élimination et même la lecture de l'ExitCode. Donc, tout est réglé par ma classe.

La classe a plusieurs possibilités, par exemple lire la sortie, démarrer en tant qu'administrateur ou utilisateur différent, attraper des exceptions et également démarrer tout cela incl. Asynchrone. Annulation. Bien, la lecture de la sortie est également possible pendant l'exécution.

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
la source
1

Je pense que tout ce que vous devriez utiliser est ceci:

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

Exemple d'utilisation:

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.
la source
Quel est l'intérêt d'accepter un CancellationToken, si l'annulation ne résout pas Killle processus?
Theodor Zoulias le
CancellationTokendans la WaitForExitAsyncméthode est nécessaire simplement pour pouvoir annuler une attente ou définir un délai. Tuer un processus peut être fait dans StartProcessAsync: `` `` try {wait process.WaitForExitAsync (cancelToken); } catch (OperationCanceledException) {process.Kill (); } `` ``
Konstantin S.
Mon opinion est que lorsqu'une méthode accepte un CancellationToken, l'annulation du token devrait entraîner l'annulation de l'opération, pas l'annulation de l'attente. C'est ce à quoi s'attendrait normalement l'appelant de la méthode. Si l'appelant souhaite annuler uniquement l'attente et laisser l'opération s'exécuter en arrière-plan, il est assez facile de le faire en externe ( voici une méthode d'extension AsCancelablequi fait exactement cela).
Theodor Zoulias
Je pense que cette décision doit être prise par l'appelant (spécifiquement pour ce cas, car cette méthode commence par Wait, en général je suis d'accord avec vous), comme dans le nouvel exemple d'utilisation.
Konstantin S.
0

Je suis vraiment inquiet pour l'élimination du processus, qu'en est-il d'attendre la sortie asynchrone ?, voici ma proposition (basée sur la précédente):

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

Ensuite, utilisez-le comme ceci:

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
la source