Обнаружение закрытого канала в перенаправленном выводе консоли в приложениях .NET - PullRequest
11 голосов
/ 22 января 2009

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

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

Ответы [ 2 ]

7 голосов
/ 23 января 2009

Чтобы решить эту проблему, мне пришлось написать собственную базовую реализацию потока через дескрипторы файлов 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 голос
/ 22 января 2009

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

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

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

...