「WaitForExit」にぶら下がっているProcessStartInfo?どうして?


187

私は次のコードを持っています:

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

私が開始しているプロセスからの出力が約7MBであることを知っています。Windowsコンソールで実行すると問題なく動作します。残念ながら、これはプログラム上、WaitForExitで無期限にハングします。これは、より小さな出力(3KBなど)の場合、コードがハングしないことにも注意してください。

ProcessStartInfoの内部StandardOutputが7MBをバッファリングできない可能性はありますか?もしそうなら、代わりに私は何をすべきですか?そうでない場合、私は何を間違っていますか?


それに関する完全なソースコードを含む最終的な解決策はありますか?
Kiquenet

2
私はそれを解決することができた方法と同じ問題に遭遇すると、このstackoverflow.com/questions/2285288/...
Bedasso

6
はい、最終的な解決策:最後の2行を入れ替えます。マニュアルにあります
アミットナイドゥ2013年

4
msdnから:このコード例では、p.WaitForExitの前にp.StandardOutput.ReadToEndを呼び出すことでデッドロック状態を回避しています。親プロセスがp.StandardOutput.ReadToEndの前にp.WaitForExitを呼び出し、子プロセスがリダイレクトされたストリームを満たすのに十分なテキストを書き込むと、デッドロック状態が発生する可能性があります。親プロセスは、子プロセスが終了するまで無期限に待機します。子プロセスは、親がStandardOutputストリーム全体から読み取るのを無期限に待機します。
Carlos Liu

これを適切に行うのがいかに複雑であるかは少し迷惑です。より簡単なコマンドラインリダイレクトでそれを回避できることを嬉しく思いました> outputfile :)
eglasius

回答:


393

問題は、リダイレクトStandardOutputStandardError内部バッファがいっぱいになる可能性があることです。どの順序を使用しても、問題が発生する可能性があります。

  • プロセスが終了するのを待ってからプロセスを読み取るとStandardOutput、プロセスが書き込みをブロックする可能性があるため、プロセスが終了することはありません。
  • あなたから読めばStandardOutputReadToEndを使用して、その後、あなたのプロセスは、プロセスが閉じたことがない場合はブロックすることができますStandardOutput(それはへの書き込みをブロックされている場合、それが終了したことがない場合、たとえば、またはStandardError)。

解決策は、非同期読み取りを使用して、バッファーがいっぱいにならないようにすることです。デッドロックを回避し、両方からすべての出力を収集するにはStandardOutputStandardError次のようにします。

編集:タイムアウトが発生した場合にObjectDisposedExceptionを回避する方法については、以下の回答を参照してください。

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
出力をリダイレクトすることが問題の原因であるとは考えていませんでしたが、それは確かに問題でした。これに頭を叩いて4時間過ごし、投稿を読んでから5分後に修正しました。よくやった!
ベングリプカ2013年

1
@AlexPeck問題はこれをコンソールアプリとして実行していた。ハンスアンパッサンは、ここでの問題を識別:stackoverflow.com/a/16218470/279516
ボブ・ホーン

5
毎回コマンドプロンプトが閉じ、この表示されます。タイプの未処理の例外が「System.ObjectDisposed」がmscorlib.dll追加情報で発生しました:安全なハンドルが閉じられていた
user1663380

3
上記の@ user1663380で説明したのと同様の問題がありました。usingイベントハンドラーのステートメントをプロセス自体のステートメントの上に置く必要があると思いますusingか?
Dan Forbes

2
待機ハンドルは必要ないと思います。msdnに従って、WaitForExitの非タイムアウトバージョンで終了します。標準出力が非同期イベントハンドラーにリダイレクトされた場合、このメソッドが返されたときに出力処理が完了していない可能性があります。非同期イベント処理が完了したことを確認するには、このオーバーロードからtrueを受け取った後、パラメーターを取らないWaitForExit()オーバーロードを呼び出します。
Patrick

98

ドキュメントのためには、Process.StandardOutputあなたがそうでなければ、あなたがデッドロックすることができます待つ前に、下にコピーされたスニペット、読むことを言います:

 // 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
これが私の環境の結果であるかどうかは100%確実ではRedirectStandardOutput = true;ありませんが、設定して使用しないとp.StandardOutput.ReadToEnd();、デッドロック/ハングが発生します。
クリスS

3
そうだね。私も同じような状況でした。プロセスでffmpegを使用して変換するとき、理由もなくStandardErrorをリダイレクトしていましたが、StandardErrorストリームで十分に書き込んでデッドロックを作成していました。
レオンペルティエ

これは、標準出力をリダイレクトして読み取っても、依然としてハングします。
user3791372 2017

@ user3791372これは、StandardOutputの背後のバッファが完全に満たされていない場合にのみ当てはまると思います。ここでは、MSDNは正義を行いません。私が読むことをお勧めする素晴らしい記事は、次の場所
Cary

19

マーク・バイアーズの答えは素晴らしいですが、以下を追加します。

OutputDataReceivedそしてErrorDataReceived代表は前に除去する必要があるoutputWaitHandleerrorWaitHandle配置されます。タイムアウトが経過した後もプロセスがデータを出力し続けて終了した場合、outputWaitHandleおよびerrorWaitHandle変数は破棄された後にアクセスされます。

(参考までに、彼の投稿にはコメントできなかったため、この警告を回答として追加する必要がありました。)


2
おそらく、CancelOutputReadを呼び出す方が良いでしょう。
マーク・バイアーズ

マークの編集されたコードをこの答えに追加するのはかなり素晴らしいでしょう!その分、まったく同じ問題が発生しています。
ianbailey 2013年

8
@ianbaileyこれを解決する最も簡単な方法は、using(Process p ...)をusing(AutoResetEvent errorWaitHandle ...)内に置くことです
Didier A.

18

これは、.NET 4.5以降向けの、より現代的な待望のTask Parallel Library(TPL)ベースのソリューションです。

使用例

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

実装

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
これまでで最も完全な答え
TermoTux 2017

1
いくつかの理由で、これが私にとって有効な唯一の解決策であり、アプリケーションがハングしなくなりました。
Jack

1
プロセスが開始された後、Exitedイベントがアタッチされる前に終了する条件は処理していないようです。私の提案-すべての登録後にプロセスを開始します。
Stas Boyarincev

@StasBoyarincevありがとう、更新されました。StackOverflowの回答をこの変更で更新するのを忘れていました。
ムハンマドレーハンサイード

1
@MuhammadRehanSaeedさらに別のこと-process.Startの前にprocess.BeginOutputReadLine()またはprocess.BeginErrorReadLine()を呼び出すことはできません。この場合、エラーが発生します:StandardOutがリダイレクトされていないか、プロセスがまだ開始されていません。
Stas Boyarincev

17

未処理のObjectDisposedExceptionに関する問題は、プロセスがタイムアウトしたときに発生します。そのような場合、状態の他の部分:

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

実行されません。この問題を次の方法で解決しました。

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
完全を期すために、これにはtrueへのリダイレクトの設定が欠落しています
knocte '27 / 11/16

また、プロセスがユーザー入力(たとえば、何かを入力する)を要求する可能性があるため、タイムアウトを削除しました。ユーザーに高速である必要はありません
knocte

なぜ変更したoutputerrorしますかoutputBuilder?誰かがうまくいく完全な答えを提供できますか?
MarkoAvlijaš2017

System.ObjectDisposedException:安全なハンドルが閉じられましたが、このバージョンでも発生します
Matt

8

ロブはそれに答えて、私を数時間の裁判にかけました。待機する前に出力/エラーバッファーを読み取ります。

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

1
しかし、呼び出した後にさらにデータが来るとWaitForExit()どうなりますか?
knocte 2016年

テストに基づく@knocte ReadToEndまたは同様のメソッド(などStandardOutput.BaseStream.CopyTo)は、すべてのデータが読み取られた後に返されます。それ以降は何も
起こり

あなたはReadToEnd()も終了を待つと言っていますか?
knocte 2017

2
@knocteマイクロソフトが作成したAPIを理解しようとしていますか?
aaaaaa 2018

対応するMSDNページの問題は、StandardOutputの背後にあるバッファーがいっぱいになる可能性があることを説明していませんでした。 。ReadToEnd()は、バッファーが閉じられるか、バッファーがいっぱいになるまで、または子がバッファーがいっぱいではない状態で終了するまで、同期的にのみ読み取ることができます。それが私の理解です。
ケアリー

7

この問題(またはバリアント)もあります。

以下を試してください:

1)p.WaitForExit(nnnn);にタイムアウトを追加します。ここで、nnnnはミリ秒単位です。

2)ReadForEnd呼び出しをWaitForExit呼び出しの前に置きます。これ、MSが推奨するものです。


5

https://stackoverflow.com/a/17600012/4151626のEM0へのクレジット

他の解決策(EM0を含む)は、内部タイムアウトと、生成されたアプリケーションによるStandardOutputとStandardErrorの両方の使用が原因で、私のアプリケーションで依然としてデッドロックしました。これが私のために働いたものです:

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

編集:StartInfoの初期化をコードサンプルに追加


これは私が使用するもので、デッドロックの問題はもうありません。
Roemer

3

私はそれをこのように解決しました:

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

入力、出力、エラーの両方をリダイレクトし、出力ストリームとエラーストリームからの読み取りを処理しました。このソリューションは、Windows 7とWindows 8の両方のSDK 7- 8.1で機能します


2
エリナ:回答ありがとうございます。このMSDNドキュメント(msdn.microsoft.com/en-us/library/…)の下部には、リダイレクトされたstdoutストリームとstderrストリームの両方を同期して最後まで読んだ場合に潜在的なデッドロックについて警告するいくつかの注意事項があります。ソリューションがこの問題の影響を受けやすいかどうかを判断するのは困難です。また、プロセスのstdout / stderr出力を入力として直接送信しているようです。どうして?:)
Matthew Piatt

3

Mark Byers、Rob、stevejayの回答を考慮に入れて、非同期ストリーム読み取りを使用して問題を解決するクラスを作成しようとしました。そうすることで、非同期プロセスの出力ストリームの読み取りに関連するバグがあることに気付きました。

そのバグをマイクロソフトで報告しました:https : //connect.microsoft.com/VisualStudio/feedback/details/3119134

概要:

あなたはそれを行うことはできません:

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

System.InvalidOperationException:StandardOutがリダイレクトされていないか、プロセスがまだ開始されていません。

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

次に、プロセスの開始後に非同期出力の読み取りを開始する必要があります。

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

そうすることで、非同期に設定する前に出力ストリームがデータを受信できるため、競合状態を作ります。

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

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

次に、非同期に設定する前にストリームを読み取る必要があると言う人もいます。しかし、同じ問題が発生します。同期読み取りとストリームを非同期モードに設定する間に競合状態が発生します。

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

「Process」および「ProcessStartInfo」が実際に設計されている方法で、プロセスの出力ストリームの安全な非同期読み取りを実現する方法はありません。

あなたはおそらくあなたのケースのために他のユーザーによって提案されたように非同期読み込みを使う方が良いでしょう。ただし、競合状態により一部の情報を見逃す可能性があることに注意してください。


1

私はこれがシンプルでより良いアプローチだと思います(私たちは必要ありません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;
    }
}

本当ですが.FileName = Path + @"\ggsci.exe" + @" < obeycommand.txt"、コードを簡略化するためにもやってはいけませんか?または"echo command | " + Path + @"\ggsci.exe"、別のobeycommand.txtファイルを実際に使用したくない場合と同等の機能があります。
アミットナイドゥ2013年

3
ソリューションにはAutoResetEventは必要ありませんが、ポーリングします。イベントを使用する代わりにポーリングを実行する場合(使用可能な場合)、理由もなくCPUを使用していて、それがプログラマーの悪いことを示しています。AutoResetEventを使用する他のソリューションと比較すると、ソリューションは本当に悪いです。(しかし、あなたが助けようとしたので私はあなたに-1を与えませんでした!)。
Eric Ouellet

1

上記の答えはどれも仕事をしていません。

Robソリューションがハングし、「Mark Byers」ソリューションが破棄された例外を取得します(私は他の回答の「ソリューション」を試しました)。

だから私は別の解決策を提案することにしました:

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

このコードはデバッグされ、完全に機能します。


1
良い!GetProcessOutputWithTimeoutメソッドを呼び出すときにtokenパラメータが提供されないことに注意してください。
S.Serpooshan 2017

1

前書き

現在受け入れられている回答は機能せず(例外がスローされ)、回避策が多すぎますが、完全なコードはありません。これはよくある質問であるため、明らかに多くの人の時間を浪費しています。

Mark Byersの回答とKarol Tylの回答を組み合わせて、Process.Startメソッドの使用方法に基づいて完全なコードを記述しました。

使用法

私はそれを使ってgitコマンドの周りに進捗ダイアログを作成しました。これは私がそれを使用した方法です:

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

理論的には、stdoutとstderrを組み合わせることもできますが、私はテストしていません。

コード

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

それでもSystem.ObjectDisposedExceptionを取得する:このバージョンでもSafeハンドルが閉じられています。
Matt

1

私はこれが夕食の古いことを知っていますが、このページ全体を読んだ後、解決策はどれもうまくいきませんでした。 。うまくいかなかったと言うと完全に真実ではありませんが、うまくいく場合もありますが、EOFマークの前の出力の長さに関係していると思います。

とにかく、私にとってうまくいった解決策は、異なるスレッドを使用してStandardOutputとStandardErrorを読み取り、メッセージを書き込むことでした。

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

これがとても難しいかもしれないと思った人を助けることを願っています!


例外: sw.FlushAsync(): Object is not set to an instance of an object. sw is null. どのように/どこswで定義する必要がありますか?
ウォリック

1

ここですべての投稿を読んだ後、MarkoAvlijašの統合ソリューションに落ち着きました。 しかし、それは私の問題のすべてを解決しませんでした。

私たちの環境には、何百もの異なる.bat .cmd .exeなどのファイルを実行するようにスケジュールされたWindowsサービスがあり、それらは何年にもわたって蓄積され、さまざまな人々によってさまざまなスタイルで記述されています。私たちはプログラムとスクリプトの作成を制御することはできません。成功/失敗のスケジュール、実行、レポートを担当するだけです。

だから私はここで提案のほとんどすべてを試し、さまざまなレベルの成功を収めました。Markoの答えはほぼ完璧でしたが、サービスとして実行した場合、常に標準出力をキャプチャするとは限りませんでした。どうしてそうならないのかわからない。

すべてのケースで機能することがわかった唯一の解決策は次のとおりです。http//csharptest.net/319/using-the-processrunner-class/index.html


このライブラリを試します。コードのスコープを設定しましたが、デリゲートを賢く使用しているようです。Nugetにうまくパッケージされています。それは基本的にプロフェッショナリズムの悪臭を放っています。噛まれたら教えてくれます。
スティーブヒバート

ソースコードへのリンクは死んでいます。次回はコードを回答にコピーしてください。
Vitaly Zdanevich

1

すべての複雑さを回避するために使用する回避策:

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

だから私は一時ファイルを作成し、出力とエラーの両方をにリダイレクトして > outputfile > 2>&1、プロセスが完了した後でファイルを読み取ります。

他のソリューションは、出力で他のものを実行したいシナリオには適していますが、単純なものの場合、これにより多くの複雑さが回避されます。


1

私は多くの答えを読んで自分で作りました。これはいずれにせよ修正されるかはわかりませんが、私の環境では修正されます。WaitForExitを使用しておらず、WaitHandle.WaitAllを出力信号とエラー終了信号の両方で使用しています。誰かがそれで起こりうる問題を見るならば、私はうれしいです。それとも誰かを助けるか。タイムアウトを使用しないので、私にとってはより良いです。

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

これを使用してTask.Runでラップし、タイムアウトを処理しました。また、タイムアウト時にkillするためにプロセスIDを返します
plus5volt

0

非同期を使用すると、標準出力と標準エラーの両方を使用している場合でも、よりエレガントなソリューションを使用してデッドロックが発生しないようにすることができます。

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

Mark Byersの回答に基づいています。非同期メソッドでない場合は、string output = tStandardOutput.result;代わりに使用できますawait



-1

この投稿は古くなっているかもしれませんが、通常はハングする主な原因は、redirectStandardoutputのスタックオーバーフローが原因であるか、redirectStandarderrorがあることがわかりました。

出力データまたはエラーデータが大きいため、無期限に処理されているため、ハング時間が発生します。

この問題を解決するには:

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

11
問題は、人々がそれらのストリームにアクセスできるようにしたいので、人々がそれらを明示的にtrueに設定することです!そうでなければ、確かに私たちはそれらをfalseのままにすることができます。
user276648 2013年

-1

ここに投稿されたサンプルコードをリダイレクタと呼び、他のプログラムをリダイレクトされたものと呼びます。それが私なら、問題を再現するために使用できるテストリダイレクトプログラムを書くでしょう。

だから私はしました。テストデータには、ECMA-334 C#言語仕様v PDFを使用しました。約5MBです。以下はその重要な部分です。

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

datasize値は実際のファイルサイズと一致しませんが、それは重要ではありません。PDFファイルが行末で常にCRとLFの両方を使用するかどうかは明確ではありませんが、これは問題ではありません。他の大きなテキストファイルを使用してテストできます。

これを使用すると、大量のデータを書き込むときにサンプルリダイレクタコードがハングしますが、少量を書き込む場合はハングしません。

どういうわけかそのコードの実行を追跡しようと非常に努力しましたが、できませんでした。別のコンソールウィンドウを取得しようとするリダイレクトプログラムのコンソールの作成を無効にするリダイレクトプログラムの行をコメントアウトしましたが、できませんでした。

次に、新しいウィンドウ、親のウィンドウ、またはウィンドウなしでコンソールアプリを起動する方法を見つけました。したがって、あるコンソールプログラムがShellExecuteなしで別のコンソールプログラムを起動する場合、(簡単に)別のコンソールを作成することはできません。ShellExecuteはリダイレクトをサポートしていないため、他のプロセスにウィンドウを指定しなくても、コンソールを共有する必要があります。

リダイレクトされたプログラムがバッファをどこかで満たすと、データが読み取られるのを待つ必要があり、その時点でリダイレクタによってデータが読み取られない場合はデッドロックであると思います。

解決策は、ReadToEndを使用せず、データの書き込み中にデータを読み取ることですが、非同期読み取りを使用する必要はありません。解決策は非常に簡単です。以下は、5 MBのPDFで動作します。

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

別の可能性は、GUIプログラムを使用してリダイレクトを行うことです。上記のコードは、明らかな変更を除いて、WPFアプリケーションで機能します。


-3

私は同じ問題を抱えていましたが、理由は異なりました。ただし、Windows 8では発生しますが、Windows 7では発生しません。次の行が問題の原因であると思われます。

pProcess.StartInfo.UseShellExecute = False

解決策は、UseShellExecuteを無効にしないことでした。私は今、不要なシェルポップアップウィンドウを受け取りましたが、特別なことが何も起こらないのを待っているプログラムよりもはるかに優れています。そこで、次の回避策を追加しました。

pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden

今私を悩ませている唯一のことは、なぜこれが最初にWindows 8で起こっているのかということです。


1
UseShellExecute出力をリダイレクトする場合は、falseに設定する必要があります。
Brad Moore、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.