終了を待機しているときにプロセスがハングすることがある


13

終了を待つ間にプロセスがハングする理由は何ですか?

このコードは、内部で多くのアクションを実行するpowershellスクリプトを開始する必要があります。たとえば、MSBuildを介してコードの再コンパイルを開始しますが、おそらく問題は、出力が多すぎて、Power Shellスクリプトが正しく実行された後でも終了を待機している間にスタックすることです。

このコードは正常に動作することもあれば、スタックすることもあるので、「奇妙」です。

コードは次の場所でハングします:

process.WaitForExit(ProcessTimeOutMiliseconds);

Powershellスクリプトは1-2秒程度で実行されますが、タイムアウトは19秒です。

public static (bool Success, string Logs) ExecuteScript(string path, int ProcessTimeOutMiliseconds, params string[] args)
{
    StringBuilder output = new StringBuilder();
    StringBuilder error = new StringBuilder();

    using (var outputWaitHandle = new AutoResetEvent(false))
    using (var errorWaitHandle = new AutoResetEvent(false))
    {
        try
        {
            using (var process = new Process())
            {
                process.StartInfo = new ProcessStartInfo
                {
                    WindowStyle = ProcessWindowStyle.Hidden,
                    FileName = "powershell.exe",
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    UseShellExecute = false,
                    Arguments = $"-ExecutionPolicy Bypass -File \"{path}\"",
                    WorkingDirectory = Path.GetDirectoryName(path)
                };

                if (args.Length > 0)
                {
                    var arguments = string.Join(" ", args.Select(x => $"\"{x}\""));
                    process.StartInfo.Arguments += $" {arguments}";
                }

                output.AppendLine($"args:'{process.StartInfo.Arguments}'");

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

                process.WaitForExit(ProcessTimeOutMiliseconds);

                var logs = output + Environment.NewLine + error;

                return process.ExitCode == 0 ? (true, logs) : (false, logs);
            }
        }
        finally
        {
            outputWaitHandle.WaitOne(ProcessTimeOutMiliseconds);
            errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds);
        }
    }
}

脚本:

start-process $args[0] App.csproj -Wait -NoNewWindow

[string]$sourceDirectory  = "\bin\Debug\*"
[int]$count = (dir $sourceDirectory | measure).Count;

If ($count -eq 0)
{
    exit 1;
}
Else
{
    exit 0;
}

どこ

$args[0] = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe"

編集する

@ingenのソリューションに、実行を再試行する小さなラッパーを追加し、MSビルドがハングアップしました

public static void ExecuteScriptRx(string path, int processTimeOutMilliseconds, out string logs, out bool success, params string[] args)
{
    var current = 0;
    int attempts_count = 5;
    bool _local_success = false;
    string _local_logs = "";

    while (attempts_count > 0 && _local_success == false)
    {
        Console.WriteLine($"Attempt: {++current}");
        InternalExecuteScript(path, processTimeOutMilliseconds, out _local_logs, out _local_success, args);
        attempts_count--;
    }

    success = _local_success;
    logs = _local_logs;
}

InternalExecuteScriptingenのコードはどこにありますか


どの行で実際にプロセスがハングしますか?コードをさらに詳しく紹介します
Mr.AF

@ Mr.AFあなたが正しい-完了。
Joelty

1
Powershellの実際の呼び出しは1つですが、提供していないのは、Powershell内で処理しようとしているスクリプトの実際の残りの部分です。powershell自体を呼び出すことは問題ではありませんが、実行しようとしていることの範囲内です。投稿を編集して、実行しようとしている明示的な呼び出し/コマンドを入力します。
DRapp

1
エラーを再現してみたのは本当に奇妙なことです。20回程度の試みでランダムに2回発生し、再度トリガーすることができません。
KiKoS

1
@Joelty、おもしろいおもしろい、あなたはそのRxアプローチがうまくいったと言っていますか?それがどのように処理されたかを知りたい
Clint

回答:


9

関連する投稿で受け入れられた回答の要約から始めましょう。

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

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

しかし、受け入れられた答えでさえ、特定の場合には実行の順序に苦労します。

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

Rxが本当に優れているのは、このような状況で複数のイベントを調整したい場合です。

Rxの.NET実装はSystem.Reactive NuGetパッケージとして利用可能であることに注意してください。

Rxがイベントの操作を容易にする方法を見てみましょう。

// Subscribe to OutputData
Observable.FromEventPattern<DataReceivedEventArgs>(process, nameof(Process.OutputDataReceived))
    .Subscribe(
        eventPattern => output.AppendLine(eventPattern.EventArgs.Data),
        exception => error.AppendLine(exception.Message)
    ).DisposeWith(disposables);

FromEventPatternイベントの個別の発生を統一されたストリーム(別名:監視可能)にマッピングできます。これにより、パイプラインでイベントを処理できます(LINQのようなセマンティクスを使用)。Subscribeここで使用される過負荷が設けられているAction<EventPattern<...>>Action<Exception>。監視されたイベントが発生するたびに、そのsenderand argsはによってラップされEventPattern、それを介してプッシュされAction<EventPattern<...>>ます。パイプラインで例外が発生したときにAction<Exception>使用されます。

Eventパターンの欠点の1つは、このユースケース(および参照されている投稿のすべての回避策)で明確に示されていますが、イベントハンドラーをサブスクライブ解除するタイミングと場所が明確でないことです。

Rxを使用IDisposableすると、サブスクリプションを作成したときに戻ってきます。廃棄すると、サブスクリプションは事実上終了します。追加によりDisposeWith(から借り拡張メソッドRxUI)、我々は複数追加することができますIDisposableし、SをCompositeDisposable(名前のdisposablesコードサンプルで)。すべて完了したら、1回の呼び出しですべてのサブスクリプションを終了できdisposables.Dispose()ます。

確かに、Rxでできることは何もありません。バニラ.NETではできません。結果として得られるコードは、関数型の考え方に適応した後は、かなり簡単に推論できます。

public static void ExecuteScriptRx(string path, int processTimeOutMilliseconds, out string logs, out bool success, params string[] args)
{
    StringBuilder output = new StringBuilder();
    StringBuilder error = new StringBuilder();

    using (var process = new Process())
    using (var disposables = new CompositeDisposable())
    {
        process.StartInfo = new ProcessStartInfo
        {
            WindowStyle = ProcessWindowStyle.Hidden,
            FileName = "powershell.exe",
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            Arguments = $"-ExecutionPolicy Bypass -File \"{path}\"",
            WorkingDirectory = Path.GetDirectoryName(path)
        };

        if (args.Length > 0)
        {
            var arguments = string.Join(" ", args.Select(x => $"\"{x}\""));
            process.StartInfo.Arguments += $" {arguments}";
        }

        output.AppendLine($"args:'{process.StartInfo.Arguments}'");

        // Raise the Process.Exited event when the process terminates.
        process.EnableRaisingEvents = true;

        // Subscribe to OutputData
        Observable.FromEventPattern<DataReceivedEventArgs>(process, nameof(Process.OutputDataReceived))
            .Subscribe(
                eventPattern => output.AppendLine(eventPattern.EventArgs.Data),
                exception => error.AppendLine(exception.Message)
            ).DisposeWith(disposables);

        // Subscribe to ErrorData
        Observable.FromEventPattern<DataReceivedEventArgs>(process, nameof(Process.ErrorDataReceived))
            .Subscribe(
                eventPattern => error.AppendLine(eventPattern.EventArgs.Data),
                exception => error.AppendLine(exception.Message)
            ).DisposeWith(disposables);

        var processExited =
            // Observable will tick when the process has gracefully exited.
            Observable.FromEventPattern<EventArgs>(process, nameof(Process.Exited))
                // First two lines to tick true when the process has gracefully exited and false when it has timed out.
                .Select(_ => true)
                .Timeout(TimeSpan.FromMilliseconds(processTimeOutMilliseconds), Observable.Return(false))
                // Force termination when the process timed out
                .Do(exitedSuccessfully => { if (!exitedSuccessfully) { try { process.Kill(); } catch {} } } );

        // Subscribe to the Process.Exited event.
        processExited
            .Subscribe()
            .DisposeWith(disposables);

        // Start process(ing)
        process.Start();

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

        // Wait for the process to terminate (gracefully or forced)
        processExited.Take(1).Wait();

        logs = output + Environment.NewLine + error;
        success = process.ExitCode == 0;
    }
}

イベントをオブザーバブルにマップする最初の部分についてはすでに説明したので、すぐに重要な部分にジャンプできます。ここでは、監視processExited変数を変数に割り当てます。これは、2回以上使用したいためです。

まず、それをアクティブ化するときに、を呼び出しSubscribeます。そして、後でその最初の値を「待ちたい」とき。

var processExited =
    // Observable will tick when the process has gracefully exited.
    Observable.FromEventPattern<EventArgs>(process, nameof(Process.Exited))
        // First two lines to tick true when the process has gracefully exited and false when it has timed out.
        .Select(_ => true)
        .Timeout(TimeSpan.FromMilliseconds(processTimeOutMilliseconds), Observable.Return(false))
        // Force termination when the process timed out
        .Do(exitedSuccessfully => { if (!exitedSuccessfully) { try { process.Kill(); } catch {} } } );

// Subscribe to the Process.Exited event.
processExited
    .Subscribe()
    .DisposeWith(disposables);

// Start process(ing)
...

// Wait for the process to terminate (gracefully or forced)
processExited.Take(1).Wait();

OPの問題の1つは、process.WaitForExit(processTimeOutMiliseconds)タイムアウトしたときにプロセスが終了することを想定していることです。MSDNから:

関連付けられたプロセスが終了するまで、指定されたミリ秒数待機するようにプロセスコンポーネントに指示します。

代わりに、タイムアウトすると、現在のスレッドに制御を戻します(つまり、ブロックを停止します)。プロセスがタイムアウトした場合は、手動で強制終了する必要があります。タイムアウトが発生したことを知るには、Process.ExitedイベントをprocessExited処理対象のオブザーバブルにマップします。このようにして、Doオペレーターの入力を準備できます。

コードはかなり自明です。exitedSuccessfullyプロセスが正常に終了した場合。そうでない場合exitedSuccessfully、強制終了する必要があります。注process.Kill()非同期に実行され、REF 発言。ただし、process.WaitForExit()直後に呼び出すと、再びデッドロックの可能性が開かれます。そのため、強制終了の場合でもusing、出力が中断または破損していると見なすことができるため、スコープが終了したときにすべての使い捨て部品をクリーンアップすることをお勧めします。

try catch構築物を、あなたが揃ってきた例外的なケース(しゃれが意図していない)のために予約されてprocessTimeOutMilliseconds完全にプロセスが必要とする実際の時間で。つまり、Process.Exitedイベントとタイマーの間で競合状態が発生します。この現象が発生する可能性は、の非同期の性質によって再び拡大されprocess.Kill()ます。テスト中に一度遭遇しました。


完全を期すために、DisposeWith拡張メソッド。

/// <summary>
/// Extension methods associated with the IDisposable interface.
/// </summary>
public static class DisposableExtensions
{
    /// <summary>
    /// Ensures the provided disposable is disposed with the specified <see cref="CompositeDisposable"/>.
    /// </summary>
    public static T DisposeWith<T>(this T item, CompositeDisposable compositeDisposable)
        where T : IDisposable
    {
        if (compositeDisposable == null)
        {
            throw new ArgumentNullException(nameof(compositeDisposable));
        }

        compositeDisposable.Add(item);
        return item;
    }
}

4
私見、確かに賞金の価値があります。いい答えで、RXのトピックについてのいいイントロです。
ケツァルコアトル

ありがとう!!! あなたExecuteScriptRxhangs完璧に処理します。残念なことにハングはまだ起こりますがExecuteScriptRx、実行する小さなラッパーを追加しただけRetryで問題なく実行されます。MSBUILDがハングする理由は、@ Clintの回答である可能性があります。PS:そのコードは私を愚かに感じさせました<lol>それは私が初めて見るものですSystem.Reactive.Linq;
Joelty

ラッパーのコードはメインポストにあります
Joelty

3

以下のために利益の読者の私は2つのセクションにこれを分割するつもりです

セクションA:問題と同様のシナリオの処理方法

セクションB:問題の再現と解決策

セクションA:問題

この問題が発生すると、プロセスはタスクマネージャーに表示され、2〜3秒後に(正常に)消えてから、タイムアウトになるまで待機してから、例外がスローされます。

&以下のシナリオ4を参照

あなたのコードで:

  1. Process.WaitForExit(ProcessTimeOutMiliseconds); これにより、タイムアウトまたは終了Processするまで待機します。これは、最初に実行されます。
  2. OutputWaitHandle.WaitOne(ProcessTimeOutMiliseconds)そしてerrorWaitHandle.WaitOne(ProcessTimeOutMiliseconds); これであなたを待っているOutputDataErrorDataストリームは、その完全に信号を送るために操作を読んで
  3. Process.ExitCode == 0 終了時にプロセスのステータスを取得します

さまざまな設定とその警告:

  • シナリオ1(ハッピーパス):タイムアウト前にプロセスが完了するため、stdoutputとstderrorもタイムアウト前に終了し、すべてが正常です。
  • シナリオ2:プロセス、OutputWaitHandleおよびErrorWaitHandleがタイムアウトするが、stdoutputおよびstderrorがまだ読み取られており、WaitHandlerがタイムアウトした後に完了する。これは別の例外につながりますObjectDisposedException()
  • シナリオ3:プロセスが最初にタイムアウト(19秒)しますが、stdoutおよびstderrorが実行中の場合、WaitHandlerのタイムアウト(19秒)を待つため、+ 19秒の遅延が追加されます。
  • シナリオ4:プロセスがタイムアウトし、コードが時期尚早にクエリを実行しようとするProcess.ExitCodeと、エラーが発生しますSystem.InvalidOperationException: Process must exit before requested information can be determined

私はこのシナリオを数十回テストしましたが、うまく機能します。テスト中に次の設定が使用されました

  • 約2〜15個のプロジェクトのビルドを開始することにより、出力ストリームのサイズは5KB〜198KBの範囲
  • タイムアウトウィンドウ内での早期のタイムアウトとプロセスの終了


更新されたコード

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

    //First waiting for ReadOperations to Timeout and then check Process to Timeout
    if (!outputWaitHandle.WaitOne(ProcessTimeOutMiliseconds) && !errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds)
        && !process.WaitForExit(ProcessTimeOutMiliseconds)  )
    {
        //To cancel the Read operation if the process is stil reading after the timeout this will prevent ObjectDisposeException
        process.CancelOutputRead();
        process.CancelErrorRead();

        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine("Timed Out");
        Logs = output + Environment.NewLine + error;
       //To release allocated resource for the Process
        process.Close();
        return  (false, logs);
    }

    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine("Completed On Time");
    Logs = output + Environment.NewLine + error;
    ExitCode = process.ExitCode.ToString();
    // Close frees the memory allocated to the exited process
    process.Close();

    //ExitCode now accessible
    return process.ExitCode == 0 ? (true, logs) : (false, logs);
    }
}
finally{}

編集:

MSBuildで何時間も遊んだ後、ようやく自分のシステムで問題を再現することができました


セクションB:問題の再現と解決策

MSBuildに-m[:number]、ビルド時に使用する同時プロセスの最大数を指定するために使用されるスイッチがあります。

これが有効になっている場合、MSBuildは、ビルドが完了した後でも存続するノードをいくつか生成します。今、 Process.WaitForExit(milliseconds)決して終了せず、最終的にタイムアウトする

いくつかの方法でこれを解決することができました

  • CMDを介して間接的にMSBuildプロセスを起動します

    $path1 = """C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\MSBuild.exe"" ""C:\Users\John\source\repos\Test\Test.sln"" -maxcpucount:3"
    $cmdOutput = cmd.exe /c $path1  '2>&1'
    $cmdOutput
  • MSBuildを引き続き使用しますが、必ずnodeReuseをFalseに設定してください

    $filepath = "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\MSBuild.exe"
    $arg1 = "C:\Users\John\source\repos\Test\Test.sln"
    $arg2 = "-m:3"
    $arg3 = "-nr:False"
    
    Start-Process -FilePath $filepath -ArgumentList $arg1,$arg2,$arg3 -Wait -NoNewWindow
  • 並列ビルドが有効になっていない場合でも、CMDWaitForExitを介してビルドを起動することにより、プロセスがハングするのを防ぐことができるため、ビルドプロセスに直接依存しません。

    $path1 = """C:\....\15.0\Bin\MSBuild.exe"" ""C:\Users\John\source\Test.sln"""
    $cmdOutput = cmd.exe /c $path1  '2>&1'
    $cmdOutput

MSBuildノードをあまり多く配置したくないので、2番目の方法をお勧めします。


したがって、上で述べたように、ありがとうございます。これにより、"-nr:False","-m:3"MSBuildのハングのような動作が修正されたようです。これにより、Rx solutionプロセス全体がある程度信頼できるようになります(これから表示されます)。私は両方の答えを受け入れるか、2つの賞金を与えることができるといいのですが
Joelty

@ジョエルティ私はRx他の解決策のアプローチが適用せずに問題を解決できるかどうかを知りたかっただけ-nr:False" ,"-m:3"です。私の理解では、セクション1で説明したデッドロックやその他のものからの無期限の待機を処理します。セクション2の根本原因は、あなたが直面した問題の根本的な原因であると私が信じているものです;)私は間違っているかもしれません。私は尋ねました、時間だけが教えてくれます...乾杯!!
クリント

3

問題は、StandardOutputまたはStandardError、あるいはその両方をリダイレクトすると、内部バッファーがいっぱいになる可能性があることです。

前述の問題を解決するには、別のスレッドでプロセスを実行します。私はWaitForExitを使用していません。プロセスのExitCodeを非同期に返すプロセス終了イベントを使用して、プロセスが完了したことを確認します。

public async Task<int> RunProcessAsync(params string[] args)
    {
        try
        {
            var tcs = new TaskCompletionSource<int>();

            var process = new Process
            {
                StartInfo = {
                    FileName = 'file path',
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    Arguments = "shell command",
                    UseShellExecute = false,
                    CreateNoWindow = true
                },
                EnableRaisingEvents = true
            };


            process.Exited += (sender, args) =>
            {
                tcs.SetResult(process.ExitCode);
                process.Dispose();
            };

            process.Start();
            // Use asynchronous read operations on at least one of the streams.
            // Reading both streams synchronously would generate another deadlock.
            process.BeginOutputReadLine();
            string tmpErrorOut = await process.StandardError.ReadToEndAsync();
            //process.WaitForExit();


            return await tcs.Task;
        }
        catch (Exception ee) {
            Console.WriteLine(ee.Message);
        }
        return -1;
    }

上記のコードは、コマンドライン引数を指定してFFMPEG.exeを呼び出し、テスト済みです。私はmp4ファイルをmp3ファイルに変換し、失敗することなく一度に1000以上のビデオを実行していました。残念ながら、私は直接のパワーシェルの経験はありませんが、これが役立つことを願っています。


最初の試行で失敗した(スタックした)他のソリューションと同様に、このコードは奇妙です(他の5つの試行と同様に、さらにテストします)。ところで、なぜあなたは上演しBegingOutputReadline、次に上演ReadToEndAsyncStandardErrorますか?
Joelty

OPはすでに非同期で読み取りを行っているため、コンソールバッファーのデッドロックが問題になることはほとんどありません。
yaakov

0

これが問題かどうかはわかりませんが、MSDNを見ると、非同期で出力をリダイレクトしているときに、オーバーロードされたWaitForExitに奇妙な点があるようです。MSDNの記事では、オーバーロードされたメソッドを呼び出した後、引数を取らないWaitForExitを呼び出すことを推奨しています。

ドキュメントページはこちらです。関連テキスト:

標準出力が非同期イベントハンドラーにリダイレクトされている場合、このメソッドが戻るときに出力処理が完了していない可能性があります。非同期イベント処理が完了したことを確認するには、このオーバーロードからtrueを受け取った後、パラメーターを取らないWaitForExit()オーバーロードを呼び出します。WindowsフォームアプリケーションでExitedイベントが正しく処理されるようにするには、SynchronizingObjectプロパティを設定します。

コードの変更は次のようになります。

if (process.WaitForExit(ProcessTimeOutMiliseconds))
{
  process.WaitForExit();
}

この回答process.WaitForExit()へのコメントで示されているように、いくつかの複雑な使い方があります
隠元
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.