tech.guitarrapc.cóm

Technical updates

PowerShell で 外部コマンドをパイプで渡す時の問題について

先日、イケメンせんせー から質問を受けて結局無理という結論に陥ったので、記事にしておきます。

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 起動直後

f:id:guitarrapc_tech:20140210220334p:plain

実行後 5sec

f:id:guitarrapc_tech:20140210220411p:plain

実行後 20sec

f:id:guitarrapc_tech:20140210220457p:plain

実行完了時

f:id:guitarrapc_tech:20140210220528p:plain

問題2.バイナリデータが壊れる

この件は Connectですでに上がっています。

つまり、PowerShell ではバイナリデータをながせない状況です。

まとめ

PowerShell で外部コマンドのパイプ渡しは禿げます。データが小さければいいのですが、もぅ、やだ。

回避するには、 PowerShell でも cmd 同様にデータを加工せずに 上流から下流に流す仕様追加が必要になります。

Connect にリクエストを挙げておいたので、ぜひVote していただけると..... まぁ cmd /c で回避できるので対応優先度が相当低いのでしょうが。

つまり PowerShel で、以下のように cmd を /c で呼び出せば、まぁはい。*1

cmd /c "\FileOutput.exe .\temp.log | .\ReadInput.exe"

*1:これって、でもちがう