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