在.NET应用程序中检测重定向控制台输出中的关闭管道

12

.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();
    }
}
如果这两个程序被编译,并且第一个程序的输出通过管道传送到第二个程序中,像这样:
Produce | Consume

观察可知,即使Consume已经结束运行,Produce仍在继续运行。

实际上,我的Consume程序类似于Unix的head命令,而我的Produce程序打印的数据计算成本很高。我希望在管道的另一端关闭连接时终止输出。

在.NET中,我该怎么做呢?

(我知道一个明显的替代方法是传递命令行参数来限制输出,事实上这也是我目前正在做的,但我仍然想知道如何实现这一点,因为我希望能够对何时终止读取进行更多配置;例如,在head之前通过grep进行筛选。)

更新: 看起来.NET中的System.IO.__ConsoleStream实现是硬编码为忽略错误0x6D (ERROR_BROKEN_PIPE)和0xE8 (ERROR_NO_DATA),这可能意味着我需要重新实现控制台流。哎...


如果您决定继续追求,我会非常感兴趣了解您的解决方案。 - EnocNRoll - AnandaGopal Pardue
2个回答

8
为了解决这个问题,我不得不在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

基于此流,我能够编写一个简单的控制台输入输出的包装器:

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,看起来我可以继续进行。

1

我同意,如果不报告ERROR_BROKEN_PIPE和ERROR_NO_DATA错误,__ConsoleStream对你来说没有用处。我很好奇他们为什么选择把它留出来。

对于那些想要跟进的人,请查看以下链接,以获取相当老旧但仍然相关的__ConsoleStream清单...

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接