web-dev-qa-db-fra.com

ProcessStartInfo suspendu à "WaitForExit"? Pourquoi?

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 le processus que je commence débouche sur environ 7 Mo. L'exécuter dans la console Windows fonctionne bien. Malheureusement, par programmation, cela bloque indéfiniment chez WaitForExit. Notez aussi 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 7 Mo en mémoire tampon? Si oui, que devrais-je faire à la place? Si non, qu'est-ce que je fais mal?

163
Epaga

Le problème est que si vous redirigez StandardOutput et/ou StandardError, le tampon interne peut devenir saturé. Quel que soit l'ordre que vous utilisez, il peut y avoir un problème:

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

La solution consiste à utiliser des lectures asynchrones pour garantir que la mémoire tampon ne soit pas saturée. Pour éviter les blocages et récupérer toutes les sorties de StandardOutput et StandardError, vous pouvez procéder comme suit:

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

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.
        }
    }
}
348
Mark Byers

Le documentation for Process.StandardOutput dit de lire avant d’attendre, sinon vous pouvez bloquer, copier un extrait 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();
87
Rob

La réponse de Mark Byers est excellente, mais j'ajouterais simplement ce qui suit: les délégués OutputDataReceived et ErrorDataReceived doivent être supprimés avant que les paramètres outputWaitHandle et errorWaitHandle soient supprimés. Si le processus continue à émettre des données après l'expiration du délai imparti, puis se termine, les variables outputWaitHandle et errorWaitHandle seront accessibles après avoir été supprimées.

(Pour votre information, j'ai dû ajouter cette mise en garde comme une réponse, car je ne pouvais pas commenter son message.) 

18
stevejay

Le problème lié à l'exception ObjectDisposedException non gérée se produit lorsque le processus a 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 = 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
                    {
                        outputBuilder.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);
        }
    }
}
16
Karol Tyl

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

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
        }
    })
    {
        process.Start();
        var cancellationTokenSource = timeout.HasValue ?
            new CancellationTokenSource(timeout.Value) :
            new CancellationTokenSource();

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

Rob répondit et me sauva quelques heures d'essais supplémentaires. Lire le tampon de sortie/erreur avant d’attendre:

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

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

Essayez ce qui suit:

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

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

6
torial

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

Les autres solutions (y compris les EM0) sont toujours bloquées pour mon application, en raison de délais d'attente internes et de l'utilisation de StandardOutput et de 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 à un exemple de code 

4
ergohack

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 géré la lecture à partir des flux de sortie et d'erreur ..___ Cette solution fonctionne pour le SDK 7 8.1, pour Windows 7 et Windows 8. 

2
Elina Maliarsky

Introduction

La réponse actuellement acceptée ne fonctionne pas (exception au lancement) et il existe trop de solutions de contournement, mais pas de code complet. Évidemment, cela fait perdre beaucoup de temps aux gens, parce que c’est une question populaire.

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

Usage

Je l'ai utilisé pour créer un 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);
                }
            }
        }
    }
}
1
Marko Avlijaš

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

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

J'ai donc décidé de suggérer 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 a débogué et fonctionne parfaitement.

1
omriman12

Après avoir lu tous les articles, je me suis décidé pour 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 du compte-rendu des 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 au fond pourquoi pas.

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

1
flapster

Je pense que c’est une approche simple et meilleure (nous n’avons pas besoin de 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;
    }
}
1
Kuzman Marinov

Cet article est peut-être obsolète, mais j'ai découvert la principale raison pour laquelle il se bloque en raison d'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 provoquera un temps d'arrêt, car le traitement est toujours en cours pour une durée indéterminée.

donc pour résoudre ce problème:

p.StartInfo.RedirectStandardoutput = False
p.StartInfo.RedirectStandarderror = False
1
song

J'ai essayé de faire une classe qui résoudrait votre problème en utilisant la lecture de flux asynchrone, en prenant en compte les réponses de Mark Byers, Rob, stevejay. Ce faisant, j'ai réalisé qu'il existait 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 a pas été redirigé ou le processus n'a pas encore commencé.

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

Ensuite, vous devez démarrer la sortie asynchrone lue après le processus commencé:

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

Ce faisant, créez une condition de concurrence critique car le flux de sortie peut recevoir 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 pose. Là sera une condition de concurrence entre la lecture synchrone et définir le flux en mode asynchrone.

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

Il n'y a aucun moyen d'obtenir une lecture asynchrone sécurisée d'un flux de sortie d'un processus de la manière réelle "Process" et "ProcessStartInfo" a été conçu.

Vous utilisez probablement mieux la lecture asynchrone, comme le suggèrent d’autres utilisateurs pour votre cas. Mais sachez que vous pourriez manquer des informations en raison de la situation de compétition.

1
Eric Ouellet

Solution que j'ai finalement utilisée 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 en utilisant > outputfile > 2>&1, puis je lis le fichier à la fin du processus.

Les autres solutions conviennent parfaitement aux scénarios dans lesquels vous souhaitez effectuer d'autres tâches avec la sortie, mais pour les tâches simples, cela évite beaucoup de complexité.

1
eglasius

Je sais que c'est vieux, mais après avoir lu toute cette page, 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 . Lorsque je dis que cela n'a pas fonctionné, ce n'est pas tout à fait vrai. Parfois, cela fonctionnerait bien. Je suppose que cela a 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 consistait à 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 pense que cela pourrait être si difficile!

1
Alexis Coles

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

Alors j'ai fait. Pour les données de test, j'ai utilisé le fichier PDF ECMA-334 C # Language Specificationv; c'est environ 5MB. 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 datasize 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 les deux 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.

L'utilisation de cet 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 suivre en quelque sorte l'exécution de ce code et je ne pouvais pas. J'ai commenté les lignes du programme redirigé qui désactivait la création d'une console pour que le programme redirigé tente d'obtenir une fenêtre de console séparée, mais je ne pouvais pas.

Puis j’ai trouvé Comment démarrer une application console dans une nouvelle fenêtre, la fenêtre du parent ou pas de 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 comme 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 un 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 consiste à ne pas utiliser ReadToEnd et à lire les données en cours d'écriture, 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é consiste à utiliser un programme graphique pour effectuer la redirection. Le code précédent fonctionne dans une application WPF sauf avec des modifications évidentes.

0
user34660

Je pense qu'avec async, il est possible d'avoir une solution plus élégante et de ne pas avoir de blocage, 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 Mark Byers answer . Si vous n'êtes pas dans une méthode asynchrone, vous pouvez utiliser string output = tStandardOutput.result; au lieu de await

0
Yepeekai