先日、イケメンせんせーから質問を受けて結局無理という結論に陥ったので、記事にしておきます。
質問
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:これって、でもちがう