先日、イケメンせんせー から質問を受けて結局無理という結論に陥ったので、記事にしておきます。
Question
PowerShellで | (パイプ)を使うとき、 アプリ.exe | アプリ.exe と、普通のアプリの標準入出力をつなげた時PowerShellがバッファリングしてるっぽいんですけど何とかする方法ってあるんですかねー?
結論
ない
(正確には PowerShell 単体でどうにかできない。)
ということで、見ていきましょう。
目次
サンプル
パイプで、上流でファイルを読んで、下流で標準入力として受け取るパターンで考えましょう。
FileOutput.exe temp.log | ReadInput.exe
パイプの上流では、ファイルを読んで出力しているだけです。
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace FileOutput { class Program { static void Main(string[] args) { var inputStream = File.OpenRead(args[0]); var outputStream = Console.OpenStandardOutput(); var bytes = new byte[256]; var readLen = 0; while ((readLen = inputStream.Read(bytes, 0, bytes.Length)) != 0) { outputStream.Write(bytes, 0, readLen); } } } }
パイプの下流では、パイプを通ってきた標準入力を読み取っています。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ReadInput { class Program { static void Main(string[] args) { Console.WriteLine("Start"); var stream = Console.OpenStandardInput(); var bytes = new byte[256]; var readLen = 0; while ((readLen = stream.Read(bytes, 0, bytes.Length)) != 0) { Console.WriteLine(readLen); } Console.WriteLine("End"); } } }
どうなるのか
PowerShell で やってみましょう。
データが小さい時
読み取るtemp.log データを 1~10と小さくします。
1..10 | Out-File temp.log -Append
実行します。
.\FileOutput.exe temp.log | .\ReadInput.exe
一瞬ですね。cmdと違和感ありません。
Start 31 End
データが大きい時
読み取るtemp.log データをMBまで大きくします。
1..100000 | Out-File temp.log -Append
実行します。
.\FileOutput.exe temp.log | .\ReadInput.exe
終わらないですね。ほげー
Start
cmdでやってみる
cmdなら問題にならないのです。では、どうなるのかというと。
cmd /c "\FileOutput.exe temp.log | .\ReadInput.exe"
この通り、そのまま無加工の状態でパイプを渡しているんですね。 速度の低下もなくスムーズに完了まで進みます。
Start 256 256 256 256 256 256 256 256 256 256 256 256 256 256 256 256 256 256 256 256 256 256 256 256 256 中略 End
何が起こっているのか
PowerShellは、その仕様としてデータを文字列の配列に変換しようとします。また、PowerShellは、標準入力を Object[]型 の配列に変換してしまいます。
つまり、PowerShell で外部コマンドを実行すると、PowerShell自身でデータを読み取って変換保持、結果をパイプラインを通して下流に渡します。
このために、PowerShell で外部コマンドを利用して大きなデータの処理をパイプラインで行おうとするとメモリ爆発するのです。
一方で、cmdは出力に何も手を加えないで下流に渡します。ですので、 cmd.exe のメモリは変わらず、外部コマンドに委任されます。
問題1.メモリ爆発
結果として、PowerShell.exe のメモリがデータサイズに応じてどんどん増えます。データサイズによっては遂には落ちます。ほげー
PowerShell 起動直後
実行後 5sec
実行後 20sec
実行完了時
問題2.バイナリデータが壊れる
この件は Connectですでに上がっています。
つまり、PowerShell ではバイナリデータをながせない状況です。
まとめ
PowerShell で外部コマンドのパイプ渡しは禿げます。データが小さければいいのですが、もぅ、やだ。
回避するには、 PowerShell でも cmd 同様にデータを加工せずに 上流から下流に流す仕様追加が必要になります。
Connect にリクエストを挙げておいたので、ぜひVote していただけると..... まぁ cmd /c で回避できるので対応優先度が相当低いのでしょうが。
つまり PowerShel で、以下のように cmd を /c で呼び出せば、まぁはい。*1
cmd /c "\FileOutput.exe .\temp.log | .\ReadInput.exe"
*1:これって、でもちがう