19115
PowerShell
5.0にて`System.Diagnostics.Process`の`OutputDataReceived`イベントを使って標準出力をリダイレクトすると、出力順が乱れて本来の標準出力と異なる結果が返ってきてしまうことがあります。

プロセスに対して`StandardOutput.ReadToEnd`メソッドを使用すれば出力順は乱れませんが、標準出力のバイト数が大きいと[処理が固ま](http://www.atmarkit.co.jp/fdotnet/dotnettips/805pipeasync/pipeasync.html)ってしまいます。

リダイレクトを使って標準出力を出力順に受け取ったり、`StandardOutput.ReadToEnd`のように同期のとれた方法で安全に標準出力結果を取り出すには、どうすれば良いのでしょうか。

なおPowerShellから`cmd /C "ping
/?"`のように`cmd.exe`を経由で外部exeを実行することで上記の要件は強引に達成できるように見えますが、よりPowerShellらしい解法があればご教示願います。

サンプルコード:

```

    #pingのヘルプを呼び出し、標準出力を返すだけのサンプルコード
    function Invoke-Ping {
        $info = New-Object -TypeName System.Diagnostics.ProcessStartInfo
        $info.CreateNoWindow = $true
        $info.UseShellExecute = $false
        $info.RedirectStandardOutput = $true
        $info.FileName = 'ping'
        $info.Arguments = @('/?')
    
        $process = New-Object -TypeName System.Diagnostics.Process
        $process.StartInfo = $info
    
        $outBuilder = New-Object -TypeName System.Text.StringBuilder
        #非同期に標準出力を受け取る
        $outReceived = {
            if (! [String]::IsNullOrEmpty($EventArgs.Data)) {
                $Event.MessageData.AppendLine($EventArgs.Data)
            }
        }
        $outEvent = Register-ObjectEvent -InputObject $process `
            -Action $outReceived -EventName 'OutputDataReceived' `
            -MessageData $outBuilder
        [void]$process.Start()
        $process.BeginOutputReadLine()
        [void]$process.WaitForExit()
        Unregister-Event -SourceIdentifier $outEvent.Name
    
        $outBuilder.ToString()
    }
    
    $set = New-Object 'System.Collections.Generic.HashSet[string]'
    1..10 | % {
      #標準出力の戻り値がずれる
      ($outValue = Invoke-Ping) > $null
      $set.Add($outValue) > $null
    }
    $set -join "-----`r`n"
    "HashSet.Count = {0}" -f $set.Count
    
```

実行結果の抜粋例(出力結果の行が正しい結果と前後しています):

```

    -----
                [-r ホップ数] [-s ホップ数] [[-j ホスト一覧] | [-k ホスト一覧]]
    使用法: ping [-t] [-a] [-n 要求数] [-l サイズ] [-f] [-i TTL] [-v TOS]
                [-w タイムアウト] [-R] [-S ソースアドレス] [-c コンパートメント]
    オプション:
                [-p] [-4] [-6] ターゲット名
                       統計を表示して続行するには、Ctrl+Break を押してください。
                       停止するには、Ctrl+C を押してください。
        -a             アドレスをホスト名に解決します。
        -t             中断されるまで、指定されたホストを Ping します。
    
```
CC BY-SA 3.0
2015-11-21T03:25:17.340
19110
2016-02-20T21:52:17.220
2015-11-21T03:54:37.010
9820
question
windows powershell
PowerShellにて外部プロセスによる大量の標準出力を出力順に取得したい
シェルとして普通に持つリダイレクト機能ではダメなのでしょうか？

```

    $output = ping /?
    $output | Select-Object -First 10
    
```

で

```

    Usage: ping [-t] [-a] [-n count] [-l size] [-f] [-i TTL] [-v TOS]
                [-r count] [-s count] [[-j host-list] | [-k host-list]]
                [-w timeout] [-R] [-S srcaddr] [-c compartment] [-p]
                [-4] [-6] target_name
    
    Options:
        -t             Ping the specified host until stopped.
                       To see statistics and continue - type Control-Break;
                       To stop - type Control-C.
    
```

が得られました。
CC BY-SA 3.0
2015-11-21T04:04:11.523
19115
2015-11-21T04:04:11.523
4236
19110
answer
出力を得たいのが標準出力だけなら、ReadToEnd()を読んでからWaitForExit()してやればハングしなかったですよ。(Powershell3でですが)

標準エラー出力もとる場合、標準出力のReadToEnd()が終わらないと標準エラー出力のReadToEnd()が呼ばれない為にか、ハングしました。

この場合はRunspacePoolを使用して別スレッドで標準出力と標準エラー出力のReadToEnd()を呼んでやることで、ハングを回避することが出来ました。  
コードはまた時間のある時に書きます。

追記:
こんな感じです。(RunspacePoolについては[この記事](http://tech.guitarrapc.com/entry/2013/10/29/100946)を参考にしました。)

```

    function Invoke-Process {
    
      [CmdletBinding()]
      [OutputType([object])]
      param([string]$cmd, [string]$arg, [string]$dir=".", [int]$timeout=0, [string]$enc)
      trap { Write-Host "[Invoke-Process] Error $_"; throw $_ }
    
      try {
        $psi = New-Object System.Diagnostics.ProcessStartInfo
        $psi.CreateNoWindow = $true
        $psi.LoadUserProfile = $true
        $psi.UseShellExecute = $false
        $psi.RedirectStandardOutput = $true
        $psi.RedirectStandardError = $true
        $psi.FileName = $cmd
        $psi.Arguments = $arg
        $psi.WorkingDirectory = $dir
        if ($enc) {
          $psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding($enc)
          $psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding($enc)
        }
    
        $process = New-Object System.Diagnostics.Process
        $process.StartInfo = $psi
    
        $sessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
        $minPoolSize = $maxPoolSize = 10
        $runspacePool = [runspacefactory]::CreateRunspacePool($minPoolSize, $maxPoolSize,  $sessionState, $Host)
        $runspacePool.ApartmentState = "STA"
        $runspacePool.Open()
    
        [void]$process.Start()
    
        $script = {
          [CmdletBinding()]
          param([object]$std)
          $std.ReadToEnd()
        }
    
        $stdOutPs = [powershell]::Create().AddScript($script).AddArgument($process.StandardOutput)
        $stdOutPs.RunspacePool = $runspacePool
        $stdOutRun = [PSCustomObject]@{
          runspace = $stdOutPs.BeginInvoke()
          powershell = $stdOutPs
        }
        $stdErrPs = [powershell]::Create().AddScript($script).AddArgument($process.StandardError)
        $stdErrPs.RunspacePool = $runspacePool
        $stdErrRun = [PSCustomObject]@{
          runspace = $stdErrPs.BeginInvoke()
          powershell = $stdErrPs
        }
    
        while ((@($stdOutRun.runspace, $stdErrRun.runspace) | Sort-Object IsCompleted -Unique).IsCompleted -ne $true) { Start-Sleep -Milliseconds 5 }
    
        if ($timeout) {
          $isTimeout = $false
          if (!$process.WaitForExit($timeout.TotalMilliseconds)) {
            $isTimeout = $true
            $process.Kill()
          }
        }
    
        $process.WaitForExit()
    
        $exitCode = $process.ExitCode
    
        $stdOut = $stdOutRun.powershell.EndInvoke($stdOutRun.runspace)
        $stdErr = $stdErrRun.powershell.EndInvoke($stdErrRun.runspace)
    
        return [PSCustomObject]@{
          cmd = $cmd
          arg = $arg
          exitCode = $exitCode
          stdOut = $stdOut
          stdErr = $stdErr
          isTimeout = $isTimeout
        }
    
      } finally {
    
        if ($process) { $process.Dispose() }
        if ($stdOutRun) { $stdOutRun.powershell.Dispose() }
        if ($stdErrRun) { $stdErrRun.powershell.Dispose() }
        if ($runspacePool) { $runspacePool.Dispose() }
    
      }
    
    }
    
```
CC BY-SA 3.0
2016-02-20T13:13:41.710
22317
2016-02-20T21:52:17.220
2016-02-20T21:52:17.220
14483
14483
19110
answer