代码之家  ›  专栏  ›  技术社区  ›  Barry Kelly

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

  •  11
  • Barry Kelly  · 技术社区  · 17 年前

    .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
    

    …可以观察到 生产 很长一段时间都在跑 消费 已终止。

    事实上,我的 消费 程序是Unix风格的 head 还有我的 生产 程序打印数据,计算成本很高。我想在管道的另一端关闭连接时终止输出。

    我如何在.NET中执行此操作?

    (我知道一个明显的替代方法是通过命令行参数来限制输出,这确实是我目前正在做的,但是我仍然想知道如何做,因为我希望能够对何时终止读取做出更可配置的判断;例如,通过管道传输 grep 之前 头 )

    更新: 它看起来非常像 System.IO.__ConsoleStream .NET中的实现硬编码以忽略错误0x6d( ERROR_BROKEN_PIPE )和0xE8( ERROR_NO_DATA )这可能意味着我需要重新实现控制台流。叹息…

    2 回复  |  直到 17 年前
        1
  •  7
  •   Barry Kelly    17 年前

    为了解决这个问题,我必须在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 在最外面的一层,我看起来很好去。

        2
  •  1
  •   EnocNRoll - AnandaGopal Pardue    17 年前

    我同意,如果没有错误报告,控制台流对您没有任何用处。我很好奇为什么他们选择不考虑这个。

    对于那些想跟随的人,请查看下面的链接,找到一个相当古老但仍然相关的uuconsolestream列表…

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