web-dev-qa-db-fra.com

StandardOutput.ReadToEnd () se bloque

J'ai un programme qui utilise fréquemment un programme externe et lit ses sorties . Il fonctionne assez bien en utilisant votre sortie de redirection de processus habituelle, mais un argument spécifique pour une raison quelconque se bloque lorsque j'essaie de le lire, pas de message d'erreur - pas d'exception , il s’arrête juste quand il atteint cette ligne . J'utilise bien sûr une fonction centralisée pour appeler et lire la sortie du programme, qui est la suivante:

public string ADBShell(string adbInput)
{
    try
    {
        //Create Empty values
        string result = string.Empty;
        string error = string.Empty;
        string output = string.Empty;
        System.Diagnostics.ProcessStartInfo procStartInfo 
            = new System.Diagnostics.ProcessStartInfo(toolPath + "adb.exe");

        procStartInfo.Arguments = adbInput;
        procStartInfo.RedirectStandardOutput = true;
        procStartInfo.RedirectStandardError = true;
        procStartInfo.UseShellExecute = false;
        procStartInfo.CreateNoWindow = true;
        procStartInfo.WorkingDirectory = toolPath;
        System.Diagnostics.Process proc = new System.Diagnostics.Process();
        proc.StartInfo = procStartInfo;
        proc.Start();
        // Get the output into a string
        proc.WaitForExit();
        result = proc.StandardOutput.ReadToEnd();
        error = proc.StandardError.ReadToEnd();  //Some ADB outputs use this
        if (result.Length > 1)
        {
            output += result;
        }
        if (error.Length > 1)
        {
            output += error;
        }
        Return output;
    }
    catch (Exception objException)
    {
        throw objException;
    }
}

La ligne qui se bloque est result = proc.StandardOutput.ReadToEnd();, mais encore une fois, pas à chaque fois, uniquement lorsqu'un argument spécifique est envoyé ("start-server"). Tous les autres arguments fonctionnent parfaitement - il lit la valeur et le renvoie . La façon dont il se bloque est aussi étrange. Il ne gèle pas ou ne donne pas une erreur ou quoi que ce soit, il arrête simplement le traitement. Comme si c’était une commande 'return', sauf qu’elle ne retourne même pas à la fonction d’appel, elle arrête tout avec l’interface toujours active . Quelqu'un en a-t-il déjà fait l'expérience? Quelqu'un a une idée de ce que je devrais essayer? Je suppose qu'il y a quelque chose d'inattendu dans le flux lui-même, mais y a-t-il un moyen de gérer/ignorer cela pour qu'il le lise quand même?

41
Elad Avron

Les solutions proposées avec BeginOutputReadLine() sont un bon moyen, mais dans de telles situations, cela n’est pas applicable, car le processus (certainement avec l’utilisation de WaitForExit()) se termine plus tôt que la sortie asynchrone s’est terminée complètement.

J'ai donc essayé de l'implémenter de manière synchrone et j'ai trouvé que la solution consistait à utiliser la méthode Peek() de la classe StreamReader. J'ai ajouté check for Peek() > -1 pour m'assurer que ce n'est pas la fin du flux comme dans article MSDN décrit et enfin ça marche et arrête de raccrocher!

Voici le code:

var process = new Process();
process.StartInfo.CreateNoWindow = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.WorkingDirectory = @"C:\test\";
process.StartInfo.FileName = "test.exe";
process.StartInfo.Arguments = "your arguments here";

process.Start();
var output = new List<string>();

while (process.StandardOutput.Peek() > -1)
{
    output.Add(process.StandardOutput.ReadLine());
}

while (process.StandardError.Peek() > -1)
{
    output.Add(process.StandardError.ReadLine());
}
process.WaitForExit();
50
Fedor

Le problème est que vous utilisez les méthodes synchrones ReadToEnd sur les flux StandardOutput et StandardError. Cela peut conduire à une impasse potentielle que vous rencontrez. Ceci est même décrit dans MSDN . La solution est décrite ici. En gros, il s’agit de: Utilisez la version asynchrone BeginOutputReadLine pour lire les données du flux StandardOutput:

p.BeginOutputReadLine();
string error = p.StandardError.ReadToEnd();
p.WaitForExit();

Implémentation de la lecture asynchrone en utilisant BeginOutputReadLine voir dans ProcessStartInfo suspendu à "WaitForExit"? Pourquoi?

15
Daniel Hilgarth

Qu'en est-il quelque chose comme:

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

process.OutputDataReceived += (sender, args) =>
                               {
                                    var outputData = args.Data;
                                    // ...
                                };
process.ErrorDataReceived += (sender, args) =>
                            {
                                var errorData = args.Data;
                                // ...
                            };
process.WaitForExit();
5
Cesario

J'ai eu le même problème d'impasse. Cet extrait de code a fonctionné pour moi.

        ProcessStartInfo startInfo = new ProcessStartInfo("cmd")
        {
            WindowStyle = ProcessWindowStyle.Hidden,
            UseShellExecute = false,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            CreateNoWindow = true
        };

        Process process = new Process();
        process.StartInfo = startInfo;
        process.Start();
        process.StandardInput.WriteLine("echo hi");
        process.StandardInput.WriteLine("exit");
        var output = process.StandardOutput.ReadToEnd();
        process.Dispose();
3
gwasterisk

J'ai eu le même genre de problème que l'erreur était juste en suspens.

D'après votre réponse à Daniel Hilgarth, je n'ai même pas essayé d'utiliser ces codes bien que je pense qu'ils auraient fonctionné pour moi.

Puisque je veux pouvoir faire des sorties plus sophistiquées, j'ai finalement décidé de le faire avec les deux sorties réalisées dans un fil d’arrière-plan.

public static class RunCommands
{
    #region Outputs Property

    private static object _outputsLockObject;
    private static object OutputsLockObject
    { 
        get
        {
            if (_outputsLockObject == null)
                Interlocked.CompareExchange(ref _outputsLockObject, new object(), null);
            return _outputsLockObject;
        }
    }

    private static Dictionary<object, CommandOutput> _outputs;
    private static Dictionary<object, CommandOutput> Outputs
    {
        get
        {
            if (_outputs != null)
                return _outputs;

            lock (OutputsLockObject)
            {
                _outputs = new Dictionary<object, CommandOutput>();
            }
            return _outputs;
        }
    }

    #endregion

    public static string GetCommandOutputSimple(ProcessStartInfo info, bool returnErrorIfPopulated = true)
    {
        // Redirect the output stream of the child process.
        info.UseShellExecute = false;
        info.CreateNoWindow = true;
        info.RedirectStandardOutput = true;
        info.RedirectStandardError = true;
        var process = new Process();
        process.StartInfo = info;
        process.ErrorDataReceived += ErrorDataHandler;
        process.OutputDataReceived += OutputDataHandler;

        var output = new CommandOutput();
        Outputs.Add(process, output);

        process.Start();

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

        // Wait for the process to finish reading from error and output before it is finished
        process.WaitForExit();

        Outputs.Remove(process);

        if (returnErrorIfPopulated && (!String.IsNullOrWhiteSpace(output.Error)))
        {
            return output.Error.TrimEnd('\n');
        }

        return output.Output.TrimEnd('\n');
    }

    private static void ErrorDataHandler(object sendingProcess, DataReceivedEventArgs errLine)
    {
        if (errLine.Data == null)
            return;

        if (!Outputs.ContainsKey(sendingProcess))
            return;

        var commandOutput = Outputs[sendingProcess];

        commandOutput.Error = commandOutput.Error + errLine.Data + "\n";
    }

    private static void OutputDataHandler(object sendingProcess, DataReceivedEventArgs outputLine)
    {
        if (outputLine.Data == null)
            return;

        if (!Outputs.ContainsKey(sendingProcess))
            return;

        var commandOutput = Outputs[sendingProcess];

        commandOutput.Output = commandOutput.Output + outputLine.Data + "\n";
    }
}
public class CommandOutput
{
    public string Error { get; set; }
    public string Output { get; set; }

    public CommandOutput()
    {
        Error = "";
        Output = "";
    }
}

Cela a fonctionné pour moi et m'a permis de ne pas avoir à utiliser un délai d'attente pour la lecture.

2
Matt Vukomanovic

Quelque chose d’élégant et qui a fonctionné pour moi est: 

Process nslookup = new Process()
{
   StartInfo = new ProcessStartInfo("nslookup")
   {
      RedirectStandardInput = true,
      RedirectStandardOutput = true,
      UseShellExecute = false,
      CreateNoWindow = true,
      WindowStyle = ProcessWindowStyle.Hidden
   }
};

nslookup.Start();
nslookup.StandardInput.WriteLine("set type=srv");
nslookup.StandardInput.WriteLine("_ldap._tcp.domain.local"); 

nslookup.StandardInput.Flush();
nslookup.StandardInput.Close();

string output = nslookup.StandardOutput.ReadToEnd();

nslookup.WaitForExit();
nslookup.Close();

Cette réponse, j’ai trouvé ici et l’astuce consiste à utiliser Flush() et Close() sur l’entrée standard.

2
dragan.stepanovic

La solution de la réponse acceptée n'a pas fonctionné pour moi. J'ai dû utiliser des tâches afin d'éviter l'impasse:

//Code to start process here

String outputResult = GetStreamOutput(process.StandardOutput);
String errorResult = GetStreamOutput(process.StandardError);

process.WaitForExit();

Avec une fonction GetStreamOutput comme suit:

private string GetStreamOutput(StreamReader stream)
{
   //Read output in separate task to avoid deadlocks
   var outputReadTask = Task.Run(() => stream.ReadToEnd());

   return outputReadTask.Result;
}
1
Meta-Knight

Juste au cas où quelqu'un tomberait sur cette question en voulant utiliser Windows Forms et TextBox (ou RichTextBox) pour afficher les erreurs et les sorties, le processus retourne en temps réel (comme ils sont écrits dans process.StandardOutput/process.StandardError).

Vous devez utiliser OutputDataReceived()/ErrorDataReceived() afin de lire les deux flux sans impasses mortes. Il n'y a aucun moyen (autant que je sache) d'éviter les impasses, sinon la réponse de Fedor, qui contient maintenant le tag "Réponse" et la plupart des préférences date, ne fait pas le tour pour moi.

Toutefois, lorsque vous utilisez RichTextBox (ou TextBox) pour générer les données, un autre problème que vous rencontrez est celui de savoir comment écrire les données dans la zone de texte en temps réel (une fois qu'elles sont arrivées). Vous recevez l'accès aux données à l'intérieur de l'un des threads d'arrière-plan OutputDataReceived()/ErrorDataReceived() et vous ne pouvez que AppendText() à partir du thread principal.

Ce que j’ai essayé d’abord, c’est d’appeler process.Start() à partir d’un thread d’arrière-plan, puis d’appeler BeginInvoke() => AppendText() dans les threads OutputDataReceived()/ErrorDataReceived() alors que le thread principal était process.WaitForExit()

Cependant, cela a conduit à mon gel de la forme et finalement suspendu pour l'éternité. Après quelques jours d’essai, j’ai fini avec la solution ci-dessous, cela semble bien fonctionner.

En bref, vous devez ajouter les messages à une collection concurrente dans les threads OutputDataReceived()/ErrorDataReceived(), tandis que le thread principal doit constamment essayer d'extraire des messages de cette collection et de les ajouter à la zone de texte:

            ProcessStartInfo startInfo
                = new ProcessStartInfo(File, mysqldumpCommand);

            process.StartInfo.FileName = File;
            process.StartInfo.Arguments = mysqldumpCommand;
            process.StartInfo.CreateNoWindow = true;
            process.StartInfo.UseShellExecute = false;
            process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
            process.StartInfo.RedirectStandardInput = false;
            process.StartInfo.RedirectStandardError = true;
            process.StartInfo.RedirectStandardOutput = true;
            process.StartInfo.StandardErrorEncoding = Encoding.UTF8;
            process.StartInfo.StandardOutputEncoding = Encoding.UTF8;
            process.EnableRaisingEvents = true;

            ConcurrentQueue<string> messages = new ConcurrentQueue<string>();

            process.ErrorDataReceived += (object se, DataReceivedEventArgs ar) =>
            {
                string data = ar.Data;
                if (!string.IsNullOrWhiteSpace(data))
                    messages.Enqueue(data);
            };
            process.OutputDataReceived += (object se, DataReceivedEventArgs ar) =>
            {
                string data = ar.Data;
                if (!string.IsNullOrWhiteSpace(data))
                    messages.Enqueue(data);
            };

            process.Start();
            process.BeginErrorReadLine();
            process.BeginOutputReadLine();
            while (!process.HasExited)
            {
                string data = null;
                if (messages.TryDequeue(out data))
                    UpdateOutputText(data, tbOutput);
                Thread.Sleep(5);
            }

            process.WaitForExit();

Le seul inconvénient de cette approche est le fait que vous pouvez perdre des messages dans un cas assez rare, lorsque le processus commence à les écrire entre process.Start() et process.BeginErrorReadLine()/process.BeginOutputReadLine(), gardez cela à l'esprit. Le seul moyen d'éviter cela est de lire l'intégralité des flux et (ou) d'accéder à ceux-ci uniquement à la fin du processus.

0
cubrman