web-dev-qa-db-ja.com

StandardOutput.ReadToEnd()がハングする

外部プログラムを頻繁に使用し、その出力を読み取るプログラムがあります。通常のプロセスリダイレクト出力を使用するとうまく機能しますが、何らかの理由で1つの特定の引数が読み込もうとするとハングします。エラーメッセージは表示されません。例外はありません。もちろん、集中化された関数を使用して、プログラムからの出力を呼び出して読み取ります。これは次のとおりです。

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

ハングする行はresult = proc.StandardOutput.ReadToEnd();ですが、毎回ではなく、特定の引数( "start-server")が送信された場合のみです。他のすべての引数は問題なく動作します-値を読み取って返します。それがハングする方法も奇妙です。フリーズしたり、エラーなどを与えたりせず、処理を停止します。呼び出し元の関数に戻らないことを除いて、「return」コマンドであるかのように、インターフェイスがまだ稼働している状態ですべてを停止します。誰もこれを前に経験しましたか?誰も私が何を試してみるべきか考えていますか?私はそれがストリーム自体の中で予期しないものであると仮定していますが、とにかくそれを読み取るためにこれを処理/無視できる方法はありますか?

45
Elad Avron

BeginOutputReadLine()で提案されたソリューションは良い方法ですが、そのような状況では、非同期出力が完全に終了する前にプロセスが(_WaitForExit()を使用して)終了するため、適用できません。

それで、私はそれを同期的に実装しようとしましたが、解決策はStreamReaderクラスのPeek()メソッドを使用することにあります。 Peek() > -1のチェックを追加して、 MSDN記事 で説明されているようにストリームの終わりではないことを確認し、最終的に動作し、ハングを停止します!

コードは次のとおりです。

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();
53
Fedor

問題は、ReadToEndストリームとStandardOutputストリームの両方で同期StandardErrorメソッドを使用していることです。これにより、潜在的なデッドロックが発生する可能性があります。これは [〜#〜] msdn [〜#〜] でも説明されています。解決策はそこで説明されています。基本的には、非同期バージョン BeginOutputReadLine を使用して、StandardOutputストリームのデータを読み取ります。

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

BeginOutputReadLineを使用した非同期読み取りの実装は、「 ProcessStartInfoが "WaitForExit"に掛かっている」を参照してください。なぜ?

17
Daniel Hilgarth

次のようなものはどうですか:

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

同じデッドロックの問題がありました。このコードスニペットは私のために働いた。

        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

エラーがぶら下がっているのと同じ種類の問題がありました。

ダニエル・ヒルガースへのあなたの回答に基づいて、私はそれらのコードを使用しようとさえしませんでしたが、それらは私のために働いていただろうと思います。

私はもっ​​と素晴らしい出力をできるようにしたいので、最終的には両方の出力をバックグラウンドスレッドで行うことを決めました。

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

これは私にとってはうまくいき、読み取りにタイムアウトを使用する必要がなくなりました。

2

エレガントで私のために働いたものは次のとおりです:

_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();
_

この答えは here であり、トリックはFlush()Close()を標準入力で使用することです。

2

受け入れられた答えの解決策は私にとってはうまくいきませんでした。デッドロックを回避するためにタスクを使用する必要がありました。

//Code to start process here

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

process.WaitForExit();

次のGetStreamOutput関数を使用します。

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

   return outputReadTask.Result;
}
2
Meta-Knight

WindowsフォームとTextBox(またはRichTextBox)を使用してエラーを表示し、プロセスがリアルタイムで出力する(process.StandardOutput/process.StandardError)。

デッドロックなしで両方のストリームを読み取るためにOutputDataReceived()/ErrorDataReceived()を使用する必要があります。そうでなければ、デッドロックを回避する方法はありません。 「Answer」タグを保持し、最新の「いいね」が最も多いのですが、私にとってはうまくいきません。

ただし、RichTextBox(またはTextBox)を使用してデータを出力する場合、発生する別の問題は、データをテキストボックスに実際にリアルタイムで書き込む方法です(到着後)。バックグラウンドスレッドOutputDataReceived()/ErrorDataReceived()のいずれかの内部のデータへのアクセスを受け取り、メインスレッドからのみAppendText()できます。

私が最初に試したのは、バックグラウンドスレッドからprocess.Start()を呼び出してから、メインスレッドが実行されている間にBeginInvoke() => AppendText()/OutputDataReceived()スレッドでErrorDataReceived()を呼び出すことでしたprocess.WaitForExit()

しかし、これにより私のフォームがフリーズし、最終的には永遠にハングアップしました。数日間試した後、私は以下の解決策になりましたが、それはかなりうまくいくようです。

簡単に言えば、メインスレッドは常にそのコレクションからメッセージを抽出してテキストボックスに追加しようとする一方で、メッセージをOutputDataReceived()/ErrorDataReceived()スレッド内の並行コレクションに追加する必要があります。

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

このアプローチの唯一の欠点は、プロセスがprocess.Start()process.BeginErrorReadLine()/process.BeginOutputReadLine()の間でメッセージの書き込みを開始するときに、非常にまれなケースでメッセージを失う可能性があるという事実です。念頭に置いてください。これを回避する唯一の方法は、完全なストリームを読み取り、プロセスが終了したときにのみストリームにアクセスすることです。

0
cubrman