.NETアプリケーションのリダイレクトされたコンソール出力で閉じたパイプを検出する

StackOverflow https://stackoverflow.com/questions/469860

質問

.NET ConsoleクラスおよびそのデフォルトのTextWriter実装(Console.Outとして暗黙的に、たとえばConsole.WriteLine()で暗黙的に使用可能)は、アプリケーションの出力が別のプログラムにパイプされるときにエラーを通知しません。他のプログラムは、アプリケーションが終了する前にパイプを終了または閉じます。これは、アプリケーションが必要以上に長く実行され、出力がブラックホールに書き込まれる可能性があることを意味します。

リダイレクトパイプのもう一方の端が閉じたことを検出するにはどうすればよいですか

詳細な説明は次のとおりです。

問題を実証するサンプルプログラムのペアを次に示します。 Produce計算の効果をシミュレートするために、多数の整数をかなりゆっくりと出力します。

using System;
class Produce
{
    static void Main()
    {
        for (int i = 0; i < 10000; ++i)
        {
            System.Threading.Thread.Sleep(100); // added for effect
            Console.WriteLine(i);
        }
    }
}

Consumeは、入力の最初の10行のみを読み取ってから終了します。

using System;
class Consume
{
    static void Main()
    {
        for (int i = 0; i < 10; ++i)
            Console.ReadLine();
    }
}

これらの2つのプログラムがコンパイルされ、最初の出力が2番目にパイプされる場合、次のようになります。

Produce | Consume

... headが終了した後もgrepは長時間実行し続けることがわかります。

実際には、私のSystem.IO.__ConsoleStreamプログラムはUnixスタイルのERROR_BROKEN_PIPEであり、私のERROR_NO_DATAプログラムは計算にコストのかかるデータを出力します。パイプのもう一方の端が接続を閉じたときに出力を終了したいです。

.NETでこれを行うにはどうすればよいですか

(明白な代替手段は、出力を制限するコマンドライン引数を渡すことであることを知っており、実際それは私が現在していることですが、できるようにしたいので、私はまだこれを行う方法を知りたいです読み取りを終了するタイミングについて、より構成可能な判断を下すため、たとえば<=>の前に<=>をパイピングする。)

UPDATE: .NETの<=>実装は、エラー0x6D(<=>)および0xE8(<=>)を無視するようにハードコードされているように見えます。それはおそらく、コンソールストリームを再実装する必要があることを意味します。ため息...)

役に立ちましたか?

解決

これを解決するには、Win32ファイルハンドル上に独自の基本的なストリーム実装を記述する必要がありました。非同期サポート、バッファリング、シークを実装する必要がなかったので、これはそれほど難しくありませんでした。

残念ながら、安全でないコードを使用する必要がありますが、通常、ローカルで完全に信頼されて実行されるコンソールアプリケーションでは問題になりません。

コアストリームは次のとおりです。

class HandleStream : Stream
{
    SafeHandle _handle;
    FileAccess _access;
    bool _eof;

    public HandleStream(SafeHandle handle, FileAccess access)
    {
        _handle = handle;
        _access = access;
    }

    public override bool CanRead
    {
        get { return (_access & FileAccess.Read) != 0; }
    }

    public override bool CanSeek
    {
        get { return false; }
    }

    public override bool CanWrite
    {
        get { return (_access & FileAccess.Write) != 0; }
    }

    public override void Flush()
    {
        // use external buffering if you need it.
    }

    public override long Length
    {
        get { throw new NotSupportedException(); }
    }

    public override long Position
    {
        get { throw new NotSupportedException(); }
        set { throw new NotSupportedException(); }
    }

    static void CheckRange(byte[] buffer, int offset, int count)
    {
        if (offset < 0 || count < 0 || (offset + count) < 0
            || (offset + count) > buffer.Length)
            throw new ArgumentOutOfRangeException();
    }

    public bool EndOfStream
    {
        get { return _eof; }
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        CheckRange(buffer, offset, count);
        int result = ReadFileNative(_handle, buffer, offset, count);
        _eof |= result == 0;
        return result;
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        int notUsed;
        Write(buffer, offset, count, out notUsed);
    }

    public void Write(byte[] buffer, int offset, int count, out int written)
    {
        CheckRange(buffer, offset, count);
        int result = WriteFileNative(_handle, buffer, offset, count);
        _eof |= result == 0;
        written = result;
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        throw new NotSupportedException();
    }

    public override void SetLength(long value)
    {
        throw new NotSupportedException();
    }

    [return: MarshalAs(UnmanagedType.Bool)]
    [DllImport("kernel32", SetLastError=true)]
    static extern unsafe bool ReadFile(
        SafeHandle hFile, byte* lpBuffer, int nNumberOfBytesToRead,
        out int lpNumberOfBytesRead, IntPtr lpOverlapped);

    [return: MarshalAs(UnmanagedType.Bool)]
    [DllImport("kernel32.dll", SetLastError=true)]
    static extern unsafe bool WriteFile(
        SafeHandle hFile, byte* lpBuffer, int nNumberOfBytesToWrite, 
        out int lpNumberOfBytesWritten, IntPtr lpOverlapped);

    unsafe static int WriteFileNative(SafeHandle hFile, byte[] buffer, int offset, int count)
    {
        if (buffer.Length == 0)
            return 0;

        fixed (byte* bufAddr = &buffer[0])
        {
            int result;
            if (!WriteFile(hFile, bufAddr + offset, count, out result, IntPtr.Zero))
            {
                // Using Win32Exception just to get message resource from OS.
                Win32Exception ex = new Win32Exception(Marshal.GetLastWin32Error());
                int hr = ex.NativeErrorCode | unchecked((int) 0x80000000);
                throw new IOException(ex.Message, hr);
            }

            return result;
        }
    }

    unsafe static int ReadFileNative(SafeHandle hFile, byte[] buffer, int offset, int count)
    {
        if (buffer.Length == 0)
            return 0;

        fixed (byte* bufAddr = &buffer[0])
        {
            int result;
            if (!ReadFile(hFile, bufAddr + offset, count, out result, IntPtr.Zero))
            {
                Win32Exception ex = new Win32Exception(Marshal.GetLastWin32Error());
                int hr = ex.NativeErrorCode | unchecked((int) 0x80000000);
                throw new IOException(ex.Message, hr);
            }
            return result;
        }
    }
}
必要に応じて

BufferedStreamをバッファリング用にラップできますが、コンソール出力では、TextWriterはとにかく文字レベルのバッファリングを行い、改行でのみフラッシュします。

ストリームは、Win32Exception自体を呼び出すのではなく、FormatMessageを悪用してエラーメッセージを抽出します。

このストリームに基づいて、コンソールI / Oの簡単なラッパーを作成できました。

static class ConsoleStreams
{
    enum StdHandle
    {
        Input = -10,
        Output = -11,
        Error = -12,
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr GetStdHandle(int nStdHandle);  

    static SafeHandle GetStdHandle(StdHandle h)
    {
        return new SafeFileHandle(GetStdHandle((int) h), true);
    }

    public static HandleStream OpenStandardInput()
    {
        return new HandleStream(GetStdHandle(StdHandle.Input), FileAccess.Read);
    }

    public static HandleStream OpenStandardOutput()
    {
        return new HandleStream(GetStdHandle(StdHandle.Output), FileAccess.Write);
    }

    public static HandleStream OpenStandardError()
    {
        return new HandleStream(GetStdHandle(StdHandle.Error), FileAccess.Write);
    }

    static TextReader _in;
    static StreamWriter _out;
    static StreamWriter _error;

    public static TextWriter Out
    {
        get
        {
            if (_out == null)
            {
                _out = new StreamWriter(OpenStandardOutput());
                _out.AutoFlush = true;
            }
            return _out;
        }
    }

    public static TextWriter Error
    {
        get
        {
            if (_error == null)
            {
                _error = new StreamWriter(OpenStandardError());
                _error.AutoFlush = true;
            }
            return _error;
        }
    }

    public static TextReader In
    {
        get
        {
            if (_in == null)
                _in = new StreamReader(OpenStandardInput());
            return _in;
        }
    }
}

最終結果は、パイプのもう一方の端が接続を終了した後にコンソール出力に書き込むと、メッセージで素晴らしい例外が発生することです:

  

パイプが閉じられています

最も外側のレベルでIOExceptionをキャッチして無視することで、私が行ってもいいように見えます。

他のヒント

ERROR_BROKEN_PIPEおよびERROR_NO_DATAエラーのレポートがなければ、__ ConsoleStreamは役に立ちません。なぜ彼らがこれを省いたのか興味があります。

フォローしたい人のために、次のリンクをチェックしてください。かなり古いですが、関連する__ConsoleStreamのリスト...

http://www.123aspx.com/Rotor/RotorSrc.aspx ?rot = 42958

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top