Обнаружение закрытой трубы в перенаправленном консольном выпуске в приложениях .NET

Класс .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

... можно заметить, что Produce продолжает работать после завершения Consume.

В действительности моя программа Consume - это стиль Unix head, а моя программа Produce печатает данные, которые дорого рассчитать. Я хотел бы завершить вывод, когда другой конец канала закрыл соединение.

Как это сделать в .NET?

(Я знаю, что очевидной альтернативой является передача аргумента командной строки для ограничения вывода и действительно того, что я сейчас делаю, но я все равно хотел бы знать, как это сделать, поскольку я хочу быть в состоянии чтобы сделать более настраиваемые суждения о том, когда прекратить чтение, например, проложить через grep до head.)

UPDATE:. Это выглядит ужасно, как реализация System.IO.__ConsoleStream в .NET жестко запрограммирована для игнорирования ошибок 0x6D (ERROR_BROKEN_PIPE) и 0xE8 (ERROR_NO_DATA). Вероятно, это означает, что мне нужно повторно реализовать консольный поток. Вздох...)

Ответ 1

Чтобы решить эту проблему, мне пришлось написать собственную реализацию базового потока поверх дескрипторов 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 на самом внешнем уровне, похоже, что мне хорошо идти.

Ответ 2

Я согласен с тем, что без сообщений об ошибках ERROR_BROKEN_PIPE и ERROR_NO_DATA __ConsoleStream бесполезен для вас. Мне любопытно, почему они решили оставить это.

Для тех, кто хочет следовать, посмотрите следующую ссылку для довольно старого, но тем не менее релевантного списка __ConsoleStream...

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