ProcessStartInfo suspendu à «WaitForExit»? Pourquoi?


187

J'ai le code suivant:

info = new System.Diagnostics.ProcessStartInfo("TheProgram.exe", String.Join(" ", args));
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.RedirectStandardOutput = true;
info.UseShellExecute = false;
System.Diagnostics.Process p = System.Diagnostics.Process.Start(info);
p.WaitForExit();
Console.WriteLine(p.StandardOutput.ReadToEnd()); //need the StandardOutput contents

Je sais que la sortie du processus que je commence est d'environ 7 Mo de long. L'exécuter dans la console Windows fonctionne correctement. Malheureusement, par programme, cela se bloque indéfiniment à WaitForExit. Notez également que le code ne se bloque PAS pour les sorties plus petites (comme 3 Ko).

Est-il possible que le StandardOutput interne dans ProcessStartInfo ne puisse pas mettre en mémoire tampon 7 Mo? Si oui, que dois-je faire à la place? Sinon, qu'est-ce que je fais de mal?


une solution finale avec le code source complet à ce sujet?
Kiquenet

2
Je rencontre le même problème et
voici

6
Oui, solution finale: permutez les deux dernières lignes. C'est dans le manuel .
Amit Naidu le

4
from msdn: L'exemple de code évite une condition de blocage en appelant p.StandardOutput.ReadToEnd avant p.WaitForExit. Une condition de blocage peut se produire si le processus parent appelle p.WaitForExit avant p.StandardOutput.ReadToEnd et que le processus enfant écrit suffisamment de texte pour remplir le flux redirigé. Le processus parent attendrait indéfiniment la fin du processus enfant. Le processus enfant attendrait indéfiniment que le parent lise à partir du flux StandardOutput complet.
Carlos Liu

c'est un peu ennuyeux à quel point il est complexe de faire cela correctement. J'ai été ravi de contourner ce problème avec des redirections de ligne de commande plus simples> outputfile :)
eglasius

Réponses:


393

Le problème est que si vous redirigez StandardOutputet / ou StandardErrorla mémoire tampon interne peut devenir pleine. Quel que soit l'ordre que vous utilisez, il peut y avoir un problème:

  • Si vous attendez que le processus se StandardOutputtermine avant de lire, le processus peut bloquer toute tentative d’écriture, de sorte que le processus ne se termine jamais.
  • Si vous lisez à l' StandardOutputaide de ReadToEnd, votre processus peut se bloquer si le processus ne se ferme jamais StandardOutput(par exemple s'il ne se termine jamais, ou s'il est bloqué en écriture StandardError).

La solution consiste à utiliser des lectures asynchrones pour garantir que la mémoire tampon ne soit pas pleine. Pour éviter les blocages et collecter toutes les sorties des deux StandardOutputet StandardErrorvous pouvez le faire:

EDIT: Voir les réponses ci-dessous pour savoir comment éviter une ObjectDisposedException si le délai d'expiration se produit.

using (Process process = new Process())
{
    process.StartInfo.FileName = filename;
    process.StartInfo.Arguments = arguments;
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.RedirectStandardError = true;

    StringBuilder output = new StringBuilder();
    StringBuilder error = new StringBuilder();

    using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
    using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
    {
        process.OutputDataReceived += (sender, e) => {
            if (e.Data == null)
            {
                outputWaitHandle.Set();
            }
            else
            {
                output.AppendLine(e.Data);
            }
        };
        process.ErrorDataReceived += (sender, e) =>
        {
            if (e.Data == null)
            {
                errorWaitHandle.Set();
            }
            else
            {
                error.AppendLine(e.Data);
            }
        };

        process.Start();

        process.BeginOutputReadLine();
        process.BeginErrorReadLine();

        if (process.WaitForExit(timeout) &&
            outputWaitHandle.WaitOne(timeout) &&
            errorWaitHandle.WaitOne(timeout))
        {
            // Process completed. Check process.ExitCode here.
        }
        else
        {
            // Timed out.
        }
    }
}

11
Je n'avais aucune idée que la redirection de la sortie causait le problème, mais bien sûr. J'ai passé 4 heures à me marteler la tête à ce sujet et je l'ai corrigé en 5 minutes après avoir lu votre message. Bon travail!
Ben Gripka

1
@AlexPeck Le problème s'exécutait en tant qu'application console. Hans Passant a identifié le problème ici: stackoverflow.com/a/16218470/279516
Bob Horn

5
chaque fois que l'invite de commande se ferme, cela apparaît: Une exception non gérée de type "System.ObjectDisposed" s'est produite dans mscorlib.dll Informations supplémentaires: Le handle sécurisé a été fermé
user1663380

3
Nous avons eu un problème similaire à celui décrit par @ user1663380 ci-dessus. Pensez-vous qu'il est possible que les usinginstructions pour les gestionnaires d'événements doivent être au - dessus de l' usinginstruction pour le processus lui-même?
Dan Forbes

2
Je ne pense pas que les poignées d'attente soient nécessaires. Comme pour msdn, terminez simplement avec la version sans délai d'attente de WaitForExit: Lorsque la sortie standard a été redirigée vers des gestionnaires d'événements asynchrones, il est possible que le traitement de la sortie ne soit pas terminé lorsque cette méthode est retournée. Pour vous assurer que la gestion des événements asynchrones a été effectuée, appelez la surcharge WaitForExit () qui ne prend aucun paramètre après avoir reçu un vrai de cette surcharge.
Patrick

98

La documentation pour Process.StandardOutputdit à lire avant d'attendre sinon vous pouvez bloquer, extrait de code copié ci-dessous:

 // Start the child process.
 Process p = new Process();
 // Redirect the output stream of the child process.
 p.StartInfo.UseShellExecute = false;
 p.StartInfo.RedirectStandardOutput = true;
 p.StartInfo.FileName = "Write500Lines.exe";
 p.Start();
 // Do not wait for the child process to exit before
 // reading to the end of its redirected stream.
 // p.WaitForExit();
 // Read the output stream first and then wait.
 string output = p.StandardOutput.ReadToEnd();
 p.WaitForExit();

14
Je ne suis pas sûr à 100% si ce n'est que le résultat de mon environnement, mais j'ai trouvé que si vous avez défini RedirectStandardOutput = true;et que vous ne l'utilisez pas, p.StandardOutput.ReadToEnd();vous obtenez un blocage / blocage.
Chris S

3
Vrai. J'étais dans une situation similaire. Je redirigeais StandardError sans raison lors de la conversion avec ffmpeg dans un processus, il écrivait suffisamment dans le flux StandardError pour créer un blocage.
Léon Pelletier

Cela se bloque toujours pour moi même avec la redirection et la lecture de la sortie standard.
user3791372

@ user3791372 Je suppose que cela n'est applicable que si le tampon derrière StandardOutput n'est pas complètement rempli. Ici, le MSDN ne rend pas justice. Un excellent article que je vous recommanderais de lire est à: dzone.com/articles/async-io-and-threadpool
Cary

19

La réponse de Mark Byers est excellente, mais je voudrais simplement ajouter ce qui suit:

Les délégués OutputDataReceivedet ErrorDataReceiveddoivent être supprimés avant outputWaitHandleet errorWaitHandleêtre supprimés. Si le processus continue à produire des données après le dépassement du délai d'attente, puis se termine, les variables outputWaitHandleet errorWaitHandleseront accessibles après avoir été supprimées.

(Pour info, je devais ajouter cette mise en garde comme réponse car je ne pouvais pas commenter son message.)


2
Peut-être serait-il préférable d'appeler CancelOutputRead ?
Mark Byers

Ajouter le code modifié de Mark à cette réponse serait plutôt génial! J'ai exactement le même problème à la minute.
ianbailey

8
@ianbailey Le moyen le plus simple de résoudre ce problème est de mettre l'utilisation (Process p ...) à l'intérieur de l'utilisation (AutoResetEvent errorWaitHandle ...)
Didier A.

18

Il s'agit d'une solution plus moderne attendue, basée sur la bibliothèque parallèle de tâches (TPL) pour .NET 4.5 et supérieur.

Exemple d'utilisation

try
{
    var exitCode = await StartProcess(
        "dotnet", 
        "--version", 
        @"C:\",
        10000, 
        Console.Out, 
        Console.Out);
    Console.WriteLine($"Process Exited with Exit Code {exitCode}!");
}
catch (TaskCanceledException)
{
    Console.WriteLine("Process Timed Out!");
}

la mise en oeuvre

public static async Task<int> StartProcess(
    string filename,
    string arguments,
    string workingDirectory= null,
    int? timeout = null,
    TextWriter outputTextWriter = null,
    TextWriter errorTextWriter = null)
{
    using (var process = new Process()
    {
        StartInfo = new ProcessStartInfo()
        {
            CreateNoWindow = true,
            Arguments = arguments,
            FileName = filename,
            RedirectStandardOutput = outputTextWriter != null,
            RedirectStandardError = errorTextWriter != null,
            UseShellExecute = false,
            WorkingDirectory = workingDirectory
        }
    })
    {
        var cancellationTokenSource = timeout.HasValue ?
            new CancellationTokenSource(timeout.Value) :
            new CancellationTokenSource();

        process.Start();

        var tasks = new List<Task>(3) { process.WaitForExitAsync(cancellationTokenSource.Token) };
        if (outputTextWriter != null)
        {
            tasks.Add(ReadAsync(
                x =>
                {
                    process.OutputDataReceived += x;
                    process.BeginOutputReadLine();
                },
                x => process.OutputDataReceived -= x,
                outputTextWriter,
                cancellationTokenSource.Token));
        }

        if (errorTextWriter != null)
        {
            tasks.Add(ReadAsync(
                x =>
                {
                    process.ErrorDataReceived += x;
                    process.BeginErrorReadLine();
                },
                x => process.ErrorDataReceived -= x,
                errorTextWriter,
                cancellationTokenSource.Token));
        }

        await Task.WhenAll(tasks);
        return process.ExitCode;
    }
}

/// <summary>
/// Waits asynchronously for the process to exit.
/// </summary>
/// <param name="process">The process to wait for cancellation.</param>
/// <param name="cancellationToken">A cancellation token. If invoked, the task will return
/// immediately as cancelled.</param>
/// <returns>A Task representing waiting for the process to end.</returns>
public static Task WaitForExitAsync(
    this Process process,
    CancellationToken cancellationToken = default(CancellationToken))
{
    process.EnableRaisingEvents = true;

    var taskCompletionSource = new TaskCompletionSource<object>();

    EventHandler handler = null;
    handler = (sender, args) =>
    {
        process.Exited -= handler;
        taskCompletionSource.TrySetResult(null);
    };
    process.Exited += handler;

    if (cancellationToken != default(CancellationToken))
    {
        cancellationToken.Register(
            () =>
            {
                process.Exited -= handler;
                taskCompletionSource.TrySetCanceled();
            });
    }

    return taskCompletionSource.Task;
}

/// <summary>
/// Reads the data from the specified data recieved event and writes it to the
/// <paramref name="textWriter"/>.
/// </summary>
/// <param name="addHandler">Adds the event handler.</param>
/// <param name="removeHandler">Removes the event handler.</param>
/// <param name="textWriter">The text writer.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static Task ReadAsync(
    this Action<DataReceivedEventHandler> addHandler,
    Action<DataReceivedEventHandler> removeHandler,
    TextWriter textWriter,
    CancellationToken cancellationToken = default(CancellationToken))
{
    var taskCompletionSource = new TaskCompletionSource<object>();

    DataReceivedEventHandler handler = null;
    handler = new DataReceivedEventHandler(
        (sender, e) =>
        {
            if (e.Data == null)
            {
                removeHandler(handler);
                taskCompletionSource.TrySetResult(null);
            }
            else
            {
                textWriter.WriteLine(e.Data);
            }
        });

    addHandler(handler);

    if (cancellationToken != default(CancellationToken))
    {
        cancellationToken.Register(
            () =>
            {
                removeHandler(handler);
                taskCompletionSource.TrySetCanceled();
            });
    }

    return taskCompletionSource.Task;
}

2
Réponse la meilleure et la plus complète à ce jour
TermoTux

1
Pour une raison, c'était la seule solution qui fonctionnait pour moi, l'application a arrêté de pendre.
Jack

1
Il semble que vous ne gérez pas la condition où le processus se termine après son démarrage, mais avant que l'événement Exited ne soit attaché. Ma suggestion - démarrer le processus après toutes les inscriptions.
Stas Boyarincev du

@StasBoyarincev Merci, mis à jour. J'avais oublié de mettre à jour la réponse StackOverflow avec ce changement.
Muhammad Rehan Saeed

1
@MuhammadRehanSaeed Encore une autre chose - il ne semble pas permis d'appeler process.BeginOutputReadLine () ou process.BeginErrorReadLine () avant process.Start. Dans ce cas, j'obtiens l'erreur: StandardOut n'a pas été redirigé ou le processus n'a pas encore commencé.
Stas Boyarincev

17

Le problème avec ObjectDisposedException non gérée se produit lorsque le processus est expiré. Dans ce cas, les autres parties de la condition:

if (process.WaitForExit(timeout) 
    && outputWaitHandle.WaitOne(timeout) 
    && errorWaitHandle.WaitOne(timeout))

ne sont pas exécutés. J'ai résolu ce problème de la manière suivante:

using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
{
    using (Process process = new Process())
    {
        // preparing ProcessStartInfo

        try
        {
            process.OutputDataReceived += (sender, e) =>
                {
                    if (e.Data == null)
                    {
                        outputWaitHandle.Set();
                    }
                    else
                    {
                        outputBuilder.AppendLine(e.Data);
                    }
                };
            process.ErrorDataReceived += (sender, e) =>
                {
                    if (e.Data == null)
                    {
                        errorWaitHandle.Set();
                    }
                    else
                    {
                        errorBuilder.AppendLine(e.Data);
                    }
                };

            process.Start();

            process.BeginOutputReadLine();
            process.BeginErrorReadLine();

            if (process.WaitForExit(timeout))
            {
                exitCode = process.ExitCode;
            }
            else
            {
                // timed out
            }

            output = outputBuilder.ToString();
        }
        finally
        {
            outputWaitHandle.WaitOne(timeout);
            errorWaitHandle.WaitOne(timeout);
        }
    }
}

1
par souci d'exhaustivité, il manque ce paramètre pour configurer les redirections vers true
knocte

et j'ai supprimé les délais d'expiration à ma fin car le processus peut demander une entrée utilisateur (par exemple, tapez quelque chose), donc je ne veux pas exiger que l'utilisateur soit rapide
knocte

Pourquoi avez-vous changé outputet errorà outputBuilder? Quelqu'un peut-il s'il vous plaît fournir une réponse complète qui fonctionne?
Marko Avlijaš

System.ObjectDisposedException: la poignée de sécurité a été fermée se produit également sur cette version pour moi
Matt

8

Rob y a répondu et m'a épargné encore quelques heures d'essais. Lisez le tampon de sortie / d'erreur avant d'attendre:

// Read the output stream first and then wait.
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();

1
mais que se passe-t-il si plus de données viennent après que vous avez appelé WaitForExit()?
knocte

@knocte basé sur mes tests, ReadToEndou des méthodes similaires (comme StandardOutput.BaseStream.CopyTo) reviendra après TOUTES les données sont lues. rien ne viendra après
S.Serpooshan

vous dites que ReadToEnd () attend également la sortie?
knocte

2
@knocte vous essayez de donner un sens à une API créée par Microsoft?
aaaaaa

Le problème de la page MSDN correspondante est qu'elle n'explique pas que le tampon derrière StandardOutput peut devenir plein et que dans cette situation, l'enfant doit arrêter d'écrire et attendre que le tampon soit vidé (le parent a lu les données dans le tampon) . ReadToEnd () ne peut lire de manière synchronisée que jusqu'à ce que le tampon soit fermé ou que le tampon soit plein, ou que l'enfant se termine avec le tampon non plein. Telle est ma compréhension.
Cary

7

Nous avons également ce problème (ou une variante).

Essayez ce qui suit:

1) Ajoutez un délai à p.WaitForExit (nnnn); où nnnn est en millisecondes.

2) Placez l'appel ReadToEnd avant l'appel WaitForExit. C'est ce que nous avons vu MS recommander.


5

Crédit à EM0 pour https://stackoverflow.com/a/17600012/4151626

Les autres solutions (y compris EM0) sont toujours bloquées pour mon application, en raison de délais d'expiration internes et de l'utilisation de StandardOutput et StandardError par l'application générée. Voici ce qui a fonctionné pour moi:

Process p = new Process()
{
  StartInfo = new ProcessStartInfo()
  {
    FileName = exe,
    Arguments = args,
    UseShellExecute = false,
    RedirectStandardOutput = true,
    RedirectStandardError = true
  }
};
p.Start();

string cv_error = null;
Thread et = new Thread(() => { cv_error = p.StandardError.ReadToEnd(); });
et.Start();

string cv_out = null;
Thread ot = new Thread(() => { cv_out = p.StandardOutput.ReadToEnd(); });
ot.Start();

p.WaitForExit();
ot.Join();
et.Join();

Edit: ajout de l'initialisation de StartInfo à l'exemple de code


C'est ce que j'utilise et je n'ai plus jamais eu de problèmes avec une impasse.
Roemer le

3

Je l'ai résolu de cette façon:

            Process proc = new Process();
            proc.StartInfo.FileName = batchFile;
            proc.StartInfo.UseShellExecute = false;
            proc.StartInfo.CreateNoWindow = true;
            proc.StartInfo.RedirectStandardError = true;
            proc.StartInfo.RedirectStandardInput = true;
            proc.StartInfo.RedirectStandardOutput = true;
            proc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;      
            proc.Start();
            StreamWriter streamWriter = proc.StandardInput;
            StreamReader outputReader = proc.StandardOutput;
            StreamReader errorReader = proc.StandardError;
            while (!outputReader.EndOfStream)
            {
                string text = outputReader.ReadLine();                    
                streamWriter.WriteLine(text);
            }

            while (!errorReader.EndOfStream)
            {                   
                string text = errorReader.ReadLine();
                streamWriter.WriteLine(text);
            }

            streamWriter.Close();
            proc.WaitForExit();

J'ai redirigé les entrées, les sorties et les erreurs et j'ai géré la lecture à partir des flux de sortie et d'erreur. Cette solution fonctionne pour le SDK 7- 8.1, à la fois pour Windows 7 et Windows 8


2
Elina: merci pour votre réponse. Il y a quelques notes au bas de ce document MSDN ( msdn.microsoft.com/en-us/library/… ) qui avertissent des blocages potentiels si vous lisez jusqu'à la fin des flux stdout et stderr redirigés de manière synchrone. Il est difficile de dire si votre solution est sensible à ce problème. De plus, il semble que vous renvoyez la sortie stdout / stderr du processus en tant qu'entrée. Pourquoi? :)
Matthew Piatt

3

J'ai essayé de créer une classe qui résoudrait votre problème en utilisant la lecture de flux asynchrone, en tenant compte des réponses de Mark Byers, Rob, stevejay. Ce faisant, j'ai réalisé qu'il y avait un bogue lié à la lecture du flux de sortie du processus asynchrone.

J'ai signalé ce bogue chez Microsoft: https://connect.microsoft.com/VisualStudio/feedback/details/3119134

Résumé:

Vous ne pouvez pas faire ça:

process.BeginOutputReadLine (); process.Start ();

Vous recevrez System.InvalidOperationException: StandardOut n'a pas été redirigé ou le processus n'a pas encore démarré.

=================================================== =================================================== =========================

Ensuite, vous devez démarrer la lecture de sortie asynchrone après le démarrage du processus:

process.Start (); process.BeginOutputReadLine ();

Pour ce faire, créez une condition de concurrence, car le flux de sortie peut recevoir des données avant de le définir sur asynchrone:

process.Start(); 
// Here the operating system could give the cpu to another thread.  
// For example, the newly created thread (Process) and it could start writing to the output
// immediately before next line would execute. 
// That create a race condition.
process.BeginOutputReadLine();

=================================================== =================================================== =========================

Ensuite, certaines personnes pourraient dire qu'il vous suffit de lire le flux avant de le définir sur asynchrone. Mais le même problème se produit. Il y aura une condition de concurrence entre la lecture synchrone et la mise en mode asynchrone du flux.

=================================================== =================================================== =========================

Il n'y a aucun moyen de réaliser une lecture asynchrone sûre d'un flux de sortie d'un processus de la manière dont "Process" et "ProcessStartInfo" ont été conçus.

Vous feriez probablement mieux d'utiliser la lecture asynchrone comme suggéré par d'autres utilisateurs pour votre cas. Mais vous devez être conscient que vous pourriez manquer certaines informations en raison de conditions de course.


1

Je pense que c'est une approche simple et meilleure (nous n'avons pas besoin AutoResetEvent):

public static string GGSCIShell(string Path, string Command)
{
    using (Process process = new Process())
    {
        process.StartInfo.WorkingDirectory = Path;
        process.StartInfo.FileName = Path + @"\ggsci.exe";
        process.StartInfo.CreateNoWindow = true;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.RedirectStandardInput = true;
        process.StartInfo.UseShellExecute = false;

        StringBuilder output = new StringBuilder();
        process.OutputDataReceived += (sender, e) =>
        {
            if (e.Data != null)
            {
                output.AppendLine(e.Data);
            }
        };

        process.Start();
        process.StandardInput.WriteLine(Command);
        process.BeginOutputReadLine();


        int timeoutParts = 10;
        int timeoutPart = (int)TIMEOUT / timeoutParts;
        do
        {
            Thread.Sleep(500);//sometimes halv scond is enough to empty output buff (therefore "exit" will be accepted without "timeoutPart" waiting)
            process.StandardInput.WriteLine("exit");
            timeoutParts--;
        }
        while (!process.WaitForExit(timeoutPart) && timeoutParts > 0);

        if (timeoutParts <= 0)
        {
            output.AppendLine("------ GGSCIShell TIMEOUT: " + TIMEOUT + "ms ------");
        }

        string result = output.ToString();
        return result;
    }
}

C'est vrai, mais ne devriez-vous pas aussi .FileName = Path + @"\ggsci.exe" + @" < obeycommand.txt"simplifier votre code? Ou peut-être quelque chose d'équivalent "echo command | " + Path + @"\ggsci.exe"si vous ne voulez vraiment pas utiliser un fichier obeycommand.txt séparé.
Amit Naidu le

3
Votre solution n'a pas besoin d'AutoResetEvent mais vous interrogez. Lorsque vous interrogez au lieu d'utiliser des événements (lorsqu'ils sont disponibles), vous utilisez le processeur sans raison et cela indique que vous êtes un mauvais programmeur. Votre solution est vraiment mauvaise par rapport à l'autre utilisant AutoResetEvent. (Mais je ne vous ai pas donné -1 parce que vous avez essayé d'aider!).
Eric Ouellet

1

Aucune des réponses ci-dessus ne fait le travail.

La solution Rob se bloque et la solution 'Mark Byers' obtient l'exception supprimée (j'ai essayé les «solutions» des autres réponses).

J'ai donc décidé de proposer une autre solution:

public void GetProcessOutputWithTimeout(Process process, int timeoutSec, CancellationToken token, out string output, out int exitCode)
{
    string outputLocal = "";  int localExitCode = -1;
    var task = System.Threading.Tasks.Task.Factory.StartNew(() =>
    {
        outputLocal = process.StandardOutput.ReadToEnd();
        process.WaitForExit();
        localExitCode = process.ExitCode;
    }, token);

    if (task.Wait(timeoutSec, token))
    {
        output = outputLocal;
        exitCode = localExitCode;
    }
    else
    {
        exitCode = -1;
        output = "";
    }
}

using (var process = new Process())
{
    process.StartInfo = ...;
    process.Start();
    string outputUnicode; int exitCode;
    GetProcessOutputWithTimeout(process, PROCESS_TIMEOUT, out outputUnicode, out exitCode);
}

Ce code débogué et fonctionne parfaitement.


1
Bien! notez simplement que le paramètre token n'est pas fourni lors de l'appel de GetProcessOutputWithTimeoutmethod.
S.Serpooshan

1

introduction

La réponse actuellement acceptée ne fonctionne pas (lance une exception) et il y a trop de solutions de contournement mais pas de code complet. Cela fait évidemment perdre beaucoup de temps aux gens parce que c'est une question populaire.

En combinant la réponse de Mark Byers et la réponse de Karol Tyl, j'ai écrit le code complet en fonction de la manière dont je souhaite utiliser la méthode Process.Start.

Usage

Je l'ai utilisé pour créer une boîte de dialogue de progression autour des commandes git. Voici comment je l'ai utilisé:

    private bool Run(string fullCommand)
    {
        Error = "";
        int timeout = 5000;

        var result = ProcessNoBS.Start(
            filename: @"C:\Program Files\Git\cmd\git.exe",
            arguments: fullCommand,
            timeoutInMs: timeout,
            workingDir: @"C:\test");

        if (result.hasTimedOut)
        {
            Error = String.Format("Timeout ({0} sec)", timeout/1000);
            return false;
        }

        if (result.ExitCode != 0)
        {
            Error = (String.IsNullOrWhiteSpace(result.stderr)) 
                ? result.stdout : result.stderr;
            return false;
        }

        return true;
    }

En théorie, vous pouvez également combiner stdout et stderr, mais je n'ai pas testé cela.

Code

public struct ProcessResult
{
    public string stdout;
    public string stderr;
    public bool hasTimedOut;
    private int? exitCode;

    public ProcessResult(bool hasTimedOut = true)
    {
        this.hasTimedOut = hasTimedOut;
        stdout = null;
        stderr = null;
        exitCode = null;
    }

    public int ExitCode
    {
        get 
        {
            if (hasTimedOut)
                throw new InvalidOperationException(
                    "There was no exit code - process has timed out.");

            return (int)exitCode;
        }
        set
        {
            exitCode = value;
        }
    }
}

public class ProcessNoBS
{
    public static ProcessResult Start(string filename, string arguments,
        string workingDir = null, int timeoutInMs = 5000,
        bool combineStdoutAndStderr = false)
    {
        using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
        using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
        {
            using (var process = new Process())
            {
                var info = new ProcessStartInfo();

                info.CreateNoWindow = true;
                info.FileName = filename;
                info.Arguments = arguments;
                info.UseShellExecute = false;
                info.RedirectStandardOutput = true;
                info.RedirectStandardError = true;

                if (workingDir != null)
                    info.WorkingDirectory = workingDir;

                process.StartInfo = info;

                StringBuilder stdout = new StringBuilder();
                StringBuilder stderr = combineStdoutAndStderr
                    ? stdout : new StringBuilder();

                var result = new ProcessResult();

                try
                {
                    process.OutputDataReceived += (sender, e) =>
                    {
                        if (e.Data == null)
                            outputWaitHandle.Set();
                        else
                            stdout.AppendLine(e.Data);
                    };
                    process.ErrorDataReceived += (sender, e) =>
                    {
                        if (e.Data == null)
                            errorWaitHandle.Set();
                        else
                            stderr.AppendLine(e.Data);
                    };

                    process.Start();

                    process.BeginOutputReadLine();
                    process.BeginErrorReadLine();

                    if (process.WaitForExit(timeoutInMs))
                        result.ExitCode = process.ExitCode;
                    // else process has timed out 
                    // but that's already default ProcessResult

                    result.stdout = stdout.ToString();
                    if (combineStdoutAndStderr)
                        result.stderr = null;
                    else
                        result.stderr = stderr.ToString();

                    return result;
                }
                finally
                {
                    outputWaitHandle.WaitOne(timeoutInMs);
                    errorWaitHandle.WaitOne(timeoutInMs);
                }
            }
        }
    }
}

Toujours obtenir System.ObjectDisposedException: la poignée de sécurité a également été fermée sur cette version.
Matt

1

Je sais que c'est vieux souper mais, après avoir lu cette page entière, aucune des solutions ne fonctionnait pour moi, même si je n'ai pas essayé Muhammad Rehan car le code était un peu difficile à suivre, même si je suppose qu'il était sur la bonne voie . Quand je dis que cela n'a pas fonctionné, ce n'est pas tout à fait vrai, parfois cela fonctionnait bien, je suppose que c'est quelque chose à voir avec la longueur de la sortie avant une marque EOF.

Quoi qu'il en soit, la solution qui a fonctionné pour moi était d'utiliser différents threads pour lire StandardOutput et StandardError et écrire les messages.

        StreamWriter sw = null;
        var queue = new ConcurrentQueue<string>();

        var flushTask = new System.Timers.Timer(50);
        flushTask.Elapsed += (s, e) =>
        {
            while (!queue.IsEmpty)
            {
                string line = null;
                if (queue.TryDequeue(out line))
                    sw.WriteLine(line);
            }
            sw.FlushAsync();
        };
        flushTask.Start();

        using (var process = new Process())
        {
            try
            {
                process.StartInfo.FileName = @"...";
                process.StartInfo.Arguments = $"...";
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardError = true;

                process.Start();

                var outputRead = Task.Run(() =>
                {
                    while (!process.StandardOutput.EndOfStream)
                    {
                        queue.Enqueue(process.StandardOutput.ReadLine());
                    }
                });

                var errorRead = Task.Run(() =>
                {
                    while (!process.StandardError.EndOfStream)
                    {
                        queue.Enqueue(process.StandardError.ReadLine());
                    }
                });

                var timeout = new TimeSpan(hours: 0, minutes: 10, seconds: 0);

                if (Task.WaitAll(new[] { outputRead, errorRead }, timeout) &&
                    process.WaitForExit((int)timeout.TotalMilliseconds))
                {
                    if (process.ExitCode != 0)
                    {
                        throw new Exception($"Failed run... blah blah");
                    }
                }
                else
                {
                    throw new Exception($"process timed out after waiting {timeout}");
                }
            }
            catch (Exception e)
            {
                throw new Exception($"Failed to succesfully run the process.....", e);
            }
        }
    }

J'espère que cela aide quelqu'un, qui pensait que cela pourrait être si difficile!


Exception: sw.FlushAsync(): Object is not set to an instance of an object. sw is null. comment / où doit- swon définir?
wallyk

1

Après avoir lu tous les articles ici, je me suis installé sur la solution consolidée de Marko Avlijaš. Cependant , cela n'a pas résolu tous mes problèmes.

Dans notre environnement, nous avons un service Windows qui est programmé pour exécuter des centaines de fichiers .bat .cmd .exe, ... etc. différents qui se sont accumulés au fil des ans et ont été écrits par de nombreuses personnes et dans des styles différents. Nous n'avons aucun contrôle sur l'écriture des programmes et des scripts, nous sommes simplement responsables de la planification, de l'exécution et des rapports sur les succès / échecs.

J'ai donc essayé à peu près toutes les suggestions ici avec différents niveaux de succès. La réponse de Marko était presque parfaite, mais lorsqu'elle était exécutée en tant que service, elle ne capturait pas toujours stdout. Je n'ai jamais compris pourquoi pas.

La seule solution que nous avons trouvée qui fonctionne dans TOUS nos cas est la suivante: http://csharptest.net/319/using-the-processrunner-class/index.html


Je vais essayer cette bibliothèque. J'ai étudié le code et il semble utiliser les délégués de manière judicieuse. Il est joliment emballé dans Nuget. Cela pue fondamentalement le professionnalisme, quelque chose dont je ne pourrais jamais être accusé. Si ça mord, dira.
Steve Hibbert

Le lien vers le code source est mort. Veuillez copier la prochaine fois le code dans la réponse.
Vitaly Zdanevich

1

Solution de contournement que j'ai fini par utiliser pour éviter toute la complexité:

var outputFile = Path.GetTempFileName();
info = new System.Diagnostics.ProcessStartInfo("TheProgram.exe", String.Join(" ", args) + " > " + outputFile + " 2>&1");
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.UseShellExecute = false;
System.Diagnostics.Process p = System.Diagnostics.Process.Start(info);
p.WaitForExit();
Console.WriteLine(File.ReadAllText(outputFile)); //need the StandardOutput contents

Je crée donc un fichier temporaire, redirige à la fois la sortie et l'erreur vers celui-ci en utilisant > outputfile > 2>&1, puis je lis simplement le fichier une fois le processus terminé.

Les autres solutions conviennent pour les scénarios où vous souhaitez faire d'autres choses avec la sortie, mais pour des choses simples, cela évite beaucoup de complexité.


1

J'ai lu de nombreuses réponses et créé les miennes. Je ne suis pas sûr que celui-ci résoudra dans tous les cas, mais il corrige dans mon environnement. Je n'utilise tout simplement pas WaitForExit et j'utilise WaitHandle.WaitAll sur les signaux de sortie et de fin d'erreur. Je serai heureux, si quelqu'un voit des problèmes possibles avec cela. Ou si cela aidera quelqu'un. Pour moi, c'est mieux car n'utilise pas les délais d'attente.

private static int DoProcess(string workingDir, string fileName, string arguments)
{
    int exitCode;
    using (var process = new Process
    {
        StartInfo =
        {
            WorkingDirectory = workingDir,
            WindowStyle = ProcessWindowStyle.Hidden,
            CreateNoWindow = true,
            UseShellExecute = false,
            FileName = fileName,
            Arguments = arguments,
            RedirectStandardError = true,
            RedirectStandardOutput = true
        },
        EnableRaisingEvents = true
    })
    {
        using (var outputWaitHandle = new AutoResetEvent(false))
        using (var errorWaitHandle = new AutoResetEvent(false))
        {
            process.OutputDataReceived += (sender, args) =>
            {
                // ReSharper disable once AccessToDisposedClosure
                if (args.Data != null) Debug.Log(args.Data);
                else outputWaitHandle.Set();
            };
            process.ErrorDataReceived += (sender, args) =>
            {
                // ReSharper disable once AccessToDisposedClosure
                if (args.Data != null) Debug.LogError(args.Data);
                else errorWaitHandle.Set();
            };

            process.Start();
            process.BeginOutputReadLine();
            process.BeginErrorReadLine();

            WaitHandle.WaitAll(new WaitHandle[] { outputWaitHandle, errorWaitHandle });

            exitCode = process.ExitCode;
        }
    }
    return exitCode;
}

J'ai utilisé ceci et enveloppé avec Task.Run pour gérer le délai d'expiration, je renvoie également processid pour tuer à l'expiration du délai
plus5volt

0

Je pense qu'avec async, il est possible d'avoir une solution plus élégante et de ne pas avoir de blocages même en utilisant à la fois standardOutput et standardError:

using (Process process = new Process())
{
    process.StartInfo.FileName = filename;
    process.StartInfo.Arguments = arguments;
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.RedirectStandardError = true;

    process.Start();

    var tStandardOutput = process.StandardOutput.ReadToEndAsync();
    var tStandardError = process.StandardError.ReadToEndAsync();

    if (process.WaitForExit(timeout))
    {
        string output = await tStandardOutput;
        string errors = await tStandardError;

        // Process completed. Check process.ExitCode here.
    }
    else
    {
        // Timed out.
    }
}

Il est basé sur la réponse de Mark Byers. Si vous n'êtes pas dans une méthode asynchrone, vous pouvez utiliser à la string output = tStandardOutput.result;place deawait



-1

Ce message est peut-être obsolète, mais j'ai découvert que la principale raison pour laquelle il se bloque généralement est due à un débordement de pile pour le redirectStandardoutput ou si vous avez redirectStandarderror.

Comme les données de sortie ou les données d'erreur sont volumineuses, cela entraînera un temps de blocage car il est toujours en cours de traitement pour une durée indéfinie.

donc pour résoudre ce problème:

p.StartInfo.RedirectStandardoutput = False
p.StartInfo.RedirectStandarderror = False

11
Le problème est que les gens définissent explicitement ceux-ci sur true parce qu'ils veulent pouvoir accéder à ces flux! Sinon, nous pouvons simplement les laisser faux.
user276648

-1

Appelons l'exemple de code publié ici le redirecteur et l'autre programme le redirigé. Si c'était moi, j'écrirais probablement un programme de test redirigé qui peut être utilisé pour dupliquer le problème.

Alors je l'ai fait. Pour les données de test, j'ai utilisé le PDF de spécification du langage ECMA-334 C #; c'est environ 5 Mo. Ce qui suit est la partie importante de cela.

StreamReader stream = null;
try { stream = new StreamReader(Path); }
catch (Exception ex)
{
    Console.Error.WriteLine("Input open error: " + ex.Message);
    return;
}
Console.SetIn(stream);
int datasize = 0;
try
{
    string record = Console.ReadLine();
    while (record != null)
    {
        datasize += record.Length + 2;
        record = Console.ReadLine();
        Console.WriteLine(record);
    }
}
catch (Exception ex)
{
    Console.Error.WriteLine($"Error: {ex.Message}");
    return;
}

La valeur de la taille de données ne correspond pas à la taille réelle du fichier, mais cela n'a pas d'importance. Il n'est pas clair si un fichier PDF utilise toujours CR et LF à la fin des lignes, mais cela n'a pas d'importance pour cela. Vous pouvez utiliser n'importe quel autre fichier texte volumineux pour tester.

En utilisant cela, l'exemple de code de redirecteur se bloque lorsque j'écris la grande quantité de données, mais pas lorsque j'écris une petite quantité.

J'ai beaucoup essayé de retracer l'exécution de ce code et je n'ai pas pu. J'ai commenté les lignes du programme redirigé qui ont désactivé la création d'une console pour le programme redirigé pour essayer d'obtenir une fenêtre de console séparée, mais je ne pouvais pas.

Ensuite, j'ai trouvé Comment démarrer une application console dans une nouvelle fenêtre, la fenêtre du parent ou aucune fenêtre . Donc, apparemment, nous ne pouvons pas (facilement) avoir une console séparée lorsqu'un programme de console démarre un autre programme de console sans ShellExecute et puisque ShellExecute ne prend pas en charge la redirection, nous devons partager une console, même si nous ne spécifions aucune fenêtre pour l'autre processus.

Je suppose que si le programme redirigé remplit une mémoire tampon quelque part, il doit attendre que les données soient lues et si, à ce stade, aucune donnée n'est lue par le redirecteur, il s'agit d'un blocage.

La solution est de ne pas utiliser ReadToEnd et de lire les données pendant l'écriture des données, mais il n'est pas nécessaire d'utiliser des lectures asynchrones. La solution peut être assez simple. Ce qui suit fonctionne pour moi avec le PDF de 5 Mo.

ProcessStartInfo info = new ProcessStartInfo(TheProgram);
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.RedirectStandardOutput = true;
info.UseShellExecute = false;
Process p = Process.Start(info);
string record = p.StandardOutput.ReadLine();
while (record != null)
{
    Console.WriteLine(record);
    record = p.StandardOutput.ReadLine();
}
p.WaitForExit();

Une autre possibilité est d'utiliser un programme GUI pour effectuer la redirection. Le code précédent fonctionne dans une application WPF, sauf avec des modifications évidentes.


-3

J'avais le même problème, mais la raison était différente. Cela se produirait cependant sous Windows 8, mais pas sous Windows 7. La ligne suivante semble avoir causé le problème.

pProcess.StartInfo.UseShellExecute = False

La solution était de NE PAS désactiver UseShellExecute. J'ai maintenant reçu une fenêtre contextuelle Shell, ce qui est indésirable, mais bien mieux que le programme qui n'attend rien de particulier. J'ai donc ajouté la solution de contournement suivante pour cela:

pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden

Maintenant, la seule chose qui me dérange est de savoir pourquoi cela se produit sous Windows 8 en premier lieu.


1
Vous devez UseShellExecuteêtre défini sur false si vous souhaitez rediriger la sortie.
Brad Moore
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.