tech.guitarrapc.cóm

Technical updates

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

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

質問

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

image

実行後 5sec

image

実行後 20sec

image

実行完了時

image

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

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

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

まとめ

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

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

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

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

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

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