2016-10-24 25 views
0

出力とエラーストリームにサブスクライブすると、競合状態になります。System.Diagnostics.Processです。ここ は、私が何をすべきかの最低限の例です。ゆっくりと動作をデバッグプロセスRedirectStandardErrorとRedirectStandardOutputを使用した場合の競合条件

private string execute(string command, string arguments, int mstimeout) 
    { 
     string report = string.Empty; 
     StringBuilder output = new StringBuilder(); 
     StringBuilder error = new StringBuilder(); 
     Process p = new Process(); 
     DataReceivedEventHandler ErrorDataReceived = (o, e) => { error.Append(e.Data); }; 
     DataReceivedEventHandler OutputDataReceived = (o, e) => { output.Append(e.Data); }; 
     try 
     { 
      p.StartInfo.FileName = command; 
      p.StartInfo.Arguments = arguments; 
      p.EnableRaisingEvents = true; 
      p.StartInfo.CreateNoWindow = true; 
      p.StartInfo.UseShellExecute = false; 
      p.StartInfo.RedirectStandardError = true; 
      p.StartInfo.RedirectStandardOutput = true; 
      p.OutputDataReceived += OutputDataReceived; 
      p.ErrorDataReceived += ErrorDataReceived; 
      p.Start(); 
      p.BeginErrorReadLine(); 
      p.BeginOutputReadLine(); 
      p.WaitForExit(mstimeout); 
      report = output.ToString() + "\n" + error.ToString(); 
     } 
     finally 
     { 
      p.OutputDataReceived -= OutputDataReceived; 
      p.ErrorDataReceived -= ErrorDataReceived; 
     } 
     return report; 
    } 

は、私はそれが望んだものです。停止せずに実行すると、レポートは空になります。

すべての出力が処理される前に、基本となるストリーミングオブジェクトが処理される競合状態があると仮定します。

すべての出力が処理されるまで待つことができますか?

+1

、それらは、スレッドプールのスレッドで提起されています。また、StringBuilder.Append()はスレッドセーフではありません。さらに、最も重大な問題はWaitForExit()* only *インターロックで、タイムアウトの-1を渡すとストリームの終わりに達することを保証します。タイムアウトを使用するのは、I/Oをリダイレクトするときや、タイマーやキル()を使用して自分のものを作るときには、良いアイデアではありません。 –

+0

@HansPassant私はあなたのところを見ます。私のユースケースでは、タイムアウトが発生した場合、レポートには関心がありません。 Stringbuilderの問題は私の問題です。後でStreamsを読むことはできますか?私は両方の上でReadAllLines()を使用しようとすると、デッドロックがあると聞きました。 – Johannes

+0

ストリームの読み込みを遅らせようとするとプロセスがデッドロックするだけで、stdout/stderrバッファがいっぱいになると処理が進まなくなります。あなたが何を恐れているのかわからない場合は、あなたのイベントハンドラに単に 'ロック 'が必要なので、そのうちの1つだけがビルダーに追加できます。 –

答えて

0

特定の状況で問題がタイムアウトしました。私はKillProcessに行く必要があります。

  if(!p.WaitForExit(mstimeout)) 
      { 
       p.Kill(); 
      } 

は良い測定のために私はneedetない可能性があり finally部分の清掃に投げました。

 finally 
     { 
      p.OutputDataReceived -= OutputDataReceived; 
      p.ErrorDataReceived -= ErrorDataReceived; 
      p.Dispose(); 
      p = null; 
      GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true); 
     } 

編集:以下のコメントが正しいと思われるので、私は最終的に一部を削除しました。

編集:必要な入力のためにタイムアウトが発生した深刻な問題がありました。私は黙っている別のコマンドを起動した。


は、今後の参考のために - 私は今、この上の和解:

private string execute(string command, string arguments, int mstimeout) 
    { 
     bool timeout = false; 
     string report = string.Empty; 
     StringBuilder output = new StringBuilder(); 
     StringBuilder error = new StringBuilder(); 
     Process p = new Process(); 
     DataReceivedEventHandler StoreError = (o, e) => { error.Append(e.Data); }; 
     DataReceivedEventHandler StoreOutput = (o, e) => { output.Append(e.Data); }; 
     try 
     { 
      Debug.WriteLine(command); 
      Debug.WriteLine(arguments); 
      p.StartInfo.FileName = command; 
      p.StartInfo.Arguments = arguments; 
      p.EnableRaisingEvents = true; 
      p.StartInfo.CreateNoWindow = true; 
      p.StartInfo.UseShellExecute = false; 
      p.StartInfo.RedirectStandardError = true; 
      p.StartInfo.RedirectStandardOutput = true; 
      p.OutputDataReceived += StoreOutput; 
      p.ErrorDataReceived += StoreError; 
      p.Start(); 
      p.BeginErrorReadLine(); 
      p.BeginOutputReadLine(); 
      if (!p.WaitForExit(mstimeout)) 
      { 
       p.Kill(); 
       timeout = true; 
       Debug.WriteLine("Process killed"); 

      } 
      else 
      { 
       p.WaitForExit(); 
      } 
     } 
     finally 
     { 
      report = output.ToString() + "\n" + error.ToString(); 
      Debug.WriteLine(report); 
      p.Dispose(); 
     } 
     if (timeout) 
     { 
      throw new TimeoutException("Timeout during call: " + command + " " + arguments); 
     } 
     return report; 
    } 
+1

その「最後」の最初の4行は何もせず、最後は*積極的に有害な*です。 GCは、作業をさせるときに最も効果的です。それはあなたよりもずっとよく知っています。 – Servy

1

私は事あなたが何かを行うことができません。私はマイクロソフトがあなたの出力(出力とエラー)を得たいと思っているプロセスを見逃していると思います。常に問題があります。最低限、あなたが持っている競争状態です。 Microsoftでバグが報告されました:https://connect.microsoft.com/VisualStudio/feedback/details/3119134/race-condition-in-process-asynchronous-output-stream-read

これは私が今使っているコードです(非同期モードで実行されているすべての実装で同じ競合状態の問題があります)。

using System; 
using System.Diagnostics; 
using System.IO; 
using System.Text; 
using System.Threading; 

namespace HQ.Util.General 
{ 
    public class ProcessExecutionWithOutputCapture 
    { 
     // ************************************************************************ 
     public class ProcessWithOutputCaptureResult 
     { 
      public string Error { get; internal set; } 

      public string Output { get; internal set; } 

      public string ExecutionError 
      { 
       get 
       { 
        if (String.IsNullOrEmpty(Error)) 
        { 
         return Error; 
        } 

        return Exception?.ToString(); 
       } 
      } 

      public bool HasTimeout { get; internal set; } 

      /// <summary> 
      /// Can be cancel through the eventCancel which will cancel the wait (and if set, will kill the process) 
      /// </summary> 
      public bool HasBeenCanceled { get; internal set; } 

      public int ExitCode { get; internal set; } 

      public Exception Exception { get; internal set; } 

      public bool HasSucceded => !HasTimeout && Exception == null; 
     } 

     // ************************************************************************ 
     private StringBuilder _sbOutput = new StringBuilder(); 
     private StringBuilder _sbError = new StringBuilder(); 

     private AutoResetEvent _outputWaitHandle = null; 
     private AutoResetEvent _errorWaitHandle = null; 

     // Could be usefull when user want to exit to not wait for process to end and kill it (if wanted) 
     public EventWaitHandle AdditionalConditionToStopWaitingProcess { get; set; } 
     public bool IsAdditionalConditionToStopWaitingProcessShouldAlsoKill { get; set; } 

     public ProcessWindowStyle ProcessWindowStyle { get; set; } = ProcessWindowStyle.Hidden; 
     public bool CreateWindow { get; set; } = false; 

     public static ProcessWithOutputCaptureResult ExecuteWith(string executablePath, string arguments, int timeout = Timeout.Infinite, ProcessWindowStyle processWindowStyle = ProcessWindowStyle.Hidden, bool createWindow = false) 
     { 
      var p = new ProcessExecutionWithOutputCapture(); 
      return p.Execute(executablePath, arguments, timeout); 
     } 

     // ************************************************************************ 
     /// <summary> 
     /// Only support existing exectuable (no association or dos command which have no executable like 'dir'). 
     /// But accept full path, partial path or no path where it will use regular system/user path. 
     /// </summary> 
     /// <param name="executablePath"></param> 
     /// <param name="arguments"></param> 
     /// <param name="timeout"></param> 
     /// <returns></returns> 
     private ProcessWithOutputCaptureResult Execute(string executablePath, string arguments = null, int timeout = Timeout.Infinite) 
     { 
      ProcessWithOutputCaptureResult processWithOutputCaptureResult = null; 

      using (Process process = new Process()) 
      { 
       process.StartInfo.FileName = executablePath; 
       process.StartInfo.Arguments = arguments; 
       process.StartInfo.UseShellExecute = false; // required to redirect output to appropriate (output or error) process stream 

       process.StartInfo.WindowStyle = ProcessWindowStyle; 
       process.StartInfo.CreateNoWindow = CreateWindow; 

       process.StartInfo.RedirectStandardOutput = true; 
       process.StartInfo.RedirectStandardError = true; 

       _outputWaitHandle = new AutoResetEvent(false); 
       _errorWaitHandle = new AutoResetEvent(false); 

       bool asyncReadStarted = false; 

       try 
       { 
        process.OutputDataReceived += ProcessOnOutputDataReceived; 
        process.ErrorDataReceived += ProcessOnErrorDataReceived; 

        process.Start(); 

        // Here there is a race condition. See: https://connect.microsoft.com/VisualStudio/feedback/details/3119134/race-condition-in-process-asynchronous-output-stream-read 

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

        asyncReadStarted = true; 

        // See: ProcessStartInfo.RedirectStandardOutput Property: 
        //  https://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k(System.Diagnostics.ProcessStartInfo.RedirectStandardOutput);k(TargetFrameworkMoniker-.NETFramework,Version%3Dv4.5.2);k(DevLang-csharp)&rd=true 
        // All 4 next lines should only be called when not using asynchronous read (process.BeginOutputReadLine() and process.BeginErrorReadLine()) 
        //_sbOutput.AppendLine(process.StandardOutput.ReadToEnd()); 
        //_sbError.AppendLine(process.StandardError.ReadToEnd()); 
        //_sbOutput.AppendLine(process.StandardOutput.ReadToEnd()); 
        //_sbError.AppendLine(process.StandardError.ReadToEnd()); 

        var waitHandles = new WaitHandle[1 + (AdditionalConditionToStopWaitingProcess == null ? 0 : 1)]; 

        waitHandles[0] = new ProcessWaitHandle(process); 
        if (AdditionalConditionToStopWaitingProcess != null) 
        { 
         waitHandles[1] = AdditionalConditionToStopWaitingProcess; 
        } 

        bool hasSucceded = false; 
        int waitResult = WaitHandle.WaitAny(waitHandles, timeout); 
        if (waitResult == 1) // The wait has been interrrupted by an external event 
        { 
         if (IsAdditionalConditionToStopWaitingProcessShouldAlsoKill) 
         { 
          process.Kill(); 
         } 
        } 
        else if (waitResult == 0) // Process has completed normally, no timeout or external event 
        { 
         // Ensure internal process code has completed like ensure to wait until stdout et stderr had been fully completed 
         hasSucceded = process.WaitForExit(timeout); 

         if (_outputWaitHandle.WaitOne(timeout) && _errorWaitHandle.WaitOne(timeout)) 
         { 
          processWithOutputCaptureResult = new ProcessWithOutputCaptureResult(); 
          processWithOutputCaptureResult.ExitCode = process.ExitCode; 
          processWithOutputCaptureResult.Output = _sbOutput.ToString(); 
          processWithOutputCaptureResult.Error = _sbError.ToString(); 
         } 
        } 
        else // Process timeout 
        { 
         processWithOutputCaptureResult = new ProcessWithOutputCaptureResult(); 
         processWithOutputCaptureResult.HasTimeout = true; 
        } 
       } 
       catch (Exception ex) 
       { 
        if (ex.HResult == -2147467259) 
        { 
         processWithOutputCaptureResult = new ProcessWithOutputCaptureResult(); 
         processWithOutputCaptureResult.Exception = new FileNotFoundException("File not found: " + executablePath, ex); 
        } 
        else 
        { 
         processWithOutputCaptureResult = new ProcessWithOutputCaptureResult(); 
         processWithOutputCaptureResult.Exception = ex; 
        } 
       } 
       finally 
       { 
        if (asyncReadStarted) 
        { 
         process.CancelOutputRead(); 
         process.CancelErrorRead(); 
        } 

        process.OutputDataReceived -= ProcessOnOutputDataReceived; 
        process.ErrorDataReceived -= ProcessOnOutputDataReceived; 

        _outputWaitHandle.Close(); 
        _outputWaitHandle.Dispose(); 

        _errorWaitHandle.Close(); 
        _errorWaitHandle.Dispose(); 
       } 
      } 

      return processWithOutputCaptureResult; 
     } 

     // ************************************************************************ 
     private void ProcessOnOutputDataReceived(object sender, DataReceivedEventArgs e) 
     { 
      if (e.Data == null) 
      { 
       _outputWaitHandle.Set(); 
      } 
      else 
      { 
       _sbOutput.AppendLine(e.Data); 
      } 
     } 

     // ************************************************************************ 
     private void ProcessOnErrorDataReceived(object sender, DataReceivedEventArgs e) 
     { 
      if (e.Data == null) 
      { 
       _errorWaitHandle.Set(); 
      } 
      else 
      { 
       _sbError.AppendLine(e.Data); 
      } 
     } 

     // ************************************************************************ 
    } 
} 

(実行を転送するアプリケーションとして)使用法:あなたは確かに出力/ ErrorDataReceivedイベントの競合状態を持っている

using System; 
using System.Diagnostics; 
using System.Linq; 
using System.Threading; 
using HQ.Util.General; 
using System.Reflection; 

namespace ExecutionForwarder 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      Stopwatch stopwatch = Stopwatch.StartNew(); 

      Console.WriteLine($"App: {Assembly.GetEntryAssembly().FullName}"); 
      Console.WriteLine($"Executing from folder: {Environment.CurrentDirectory}"); 
      Console.WriteLine($"at: {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}"); 
      Console.WriteLine($"With args: [{string.Join(" ", args.Skip(1))}]"); 

      if (args.Length == 1 && args[0].ToLower().StartsWith("-delay:")) 
      { 
       int millisec; 
       if (Int32.TryParse(args[0].Substring(args[0].IndexOf(":") + 1), out millisec)) 
       { 
        Console.WriteLine($"Sleeping for {millisec} milliseconds and will exit."); 
        Thread.Sleep(millisec); 
       } 
       else 
       { 
        Console.Error.WriteLine("Error while trying to read the delay."); 
        Environment.ExitCode = -99; 
       } 
      } 
      else 
      { 
       if (args.Length == 0) 
       { 
        Console.Error.WriteLine($"Can't forward execution. There is no argument (executable) provided."); 
        Environment.ExitCode = -99; 
       } 
       else 
       { 
        var result = ProcessExecutionWithOutputCapture.ExecuteWith(args[0], string.Join(" ", args.Skip(1))); 

        Console.Write(result.Output); 
        Console.Error.Write(result.Error); 

        Environment.ExitCode = result.ExitCode; 
       } 
      } 

      Console.WriteLine($"Done in {stopwatch.ElapsedMilliseconds} millisecs"); 
     } 
    } 
} 
+0

2つのwaitステートメントを組み合わせると実際に動作します。まず、タイムアウトを待つ。タイムアウトヒットがそれを殺すだけなら。タイムアウトがタイムアウトなしで待機した場合。タイムアウトなしで待機すると、ストリームを待つという副作用があります。しかし、実装はその呼び出しの副作用に依存するので、私はあなたが何を意味するかを見ていると思う。 – Johannes

+0

@Johannes、問題は二重です。 1 - 非同期と同期を混在させることができます(これは発生する競争により正常です)。 2 - 非同期と非同期を混在させることができないということは、1つのモードのみに依存できることを意味します。同期はそのサイズによって制限されます。大きなバッファが必要な場合は、非同期モードに依存する必要があります。しかし、競合状態を防ぐために、現在のインターフェースProcess/ProcessStartInfoが現在実装されていないプロセスが開始される前に、ストリームを非同期モードに設定する必要があります。 –

+0

私はうまく動作する実装があります。私はそれを(数分で)公開するだろうが、それはまだ私がバグを報告したのと同じ競争状態の問題を持っている。 –

関連する問題