Handling Console Input in .NET 4.0 for PipeExec


When using the PipeExec component to send data to a .NET 4.0 or later console application, the input may not be recognized by the application. This behavior differs from earlier versions of .NET, where console applications can read input from a pipe without issues.

This limitation is due to changes in input processing introduced in .NET 4.0, which restrict the ability of console applications to read input from pipes. As a result, standard input methods may not function as expected in these environments.

To work around this limitation, you can implement a custom class that reads from the input stream using native methods, which are not subject to this restriction.

In your .NET 4.0 console application, create a MyConsole class as defined in the example below. The usage of this class is similar to the standard Console class. For example:

string input = "";
while ((input = MyIO.MyConsole.In.ReadLine()) != null)
{
  Console.WriteLine("Echo back: " + input);
}

Note The MyConsole class uses P/Invoke and the Allow unsafe code setting option must be checked in the Build section of the project options.

MyConsole Class:

using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
namespace MyIO
{
  public static class MyConsole
  {
    internal sealed class MyConsoleStream : Stream
    {
      internal const int DefaultBufferSize = 128;
      // From winerror.h
      private const int ERROR_BROKEN_PIPE = 109;
      // ERROR_NO_DATA ("The pipe is being closed") is returned when we write to
      // a console that is closing.
      private const int ERROR_NO_DATA = 232;
      private SafeFileHandle _handle;
      private bool _canRead;
      private bool _canWrite;
      internal MyConsoleStream(SafeFileHandle handle, FileAccess access)
      {
        _handle = handle;
        _canRead = access == FileAccess.Read;
        _canWrite = access == FileAccess.Write;
      }
      public override bool CanRead
      {
        get { return _canRead; }
      }
      public override bool CanWrite
      {
        get { return _canWrite; }
      }
      public override bool CanSeek
      {
        get { return false; }
      }
      public override long Length
      {
        get
        {
          throw new NotImplementedException();
        }
      }
      public override long Position
      {
        get
        {
          throw new NotImplementedException();
        }
        set
        {
          throw new NotImplementedException();
        }
      }
      public override void Flush()
      {
      }
      public override long Seek(long offset, SeekOrigin origin)
      {
        return 0;
      }
      public override void SetLength(long value)
      {
      }
      protected override void Dispose(bool disposing)
      {
        // We're probably better off not closing the OS handle here. First,
        // we allow a program to get multiple instances of __ConsoleStreams
        // around the same OS handle, so closing one handle would invalidate
        // them all. Additionally, we want a second AppDomain to be able to
        // write to stdout if a second AppDomain quits.
        if (_handle != null)
        {
          _handle = null;
        }
        _canRead = false;
        _canWrite = false;
        base.Dispose(disposing);
      }
      public override int Read(byte[] buffer, int offset, int count)
      {
        if (buffer == null)
          throw new ArgumentNullException("buffer");
        if (offset < 0 || count < 0)
          throw new ArgumentOutOfRangeException((offset < 0 ? "offset" : "count"), "ArgumentOutOfRange_NeedNonNegNum");
        if (buffer.Length - offset < count)
          throw new ArgumentException("Argument_InvalidOffLen");
        if (!_canRead)
        {
          throw new InvalidOperationException("Can not read.");
        }
        int errorCode = 0;
        int result = ReadFileNative(_handle, buffer, offset, count, 0, out errorCode);
        if (result == -1)
        {
          throw new IOException("read error", errorCode);
        }
        return result;
      }
      public override void Write(byte[] buffer, int offset, int count)
      {
        if (buffer == null)
          throw new ArgumentNullException("buffer");
        if (offset < 0 || count < 0)
          throw new ArgumentOutOfRangeException((offset < 0 ? "offset" : "count"), "ArgumentOutOfRange_NeedNonNegNum");
        if (buffer.Length - offset < count)
          throw new ArgumentException("Argument_InvalidOffLen");
        if (!_canWrite)
        {
          throw new InvalidOperationException("Can not write.");
        }
        int errorCode = 0;
        int result = WriteFileNative(_handle, buffer, offset, count, 0, out errorCode);
        if (result == -1)
        {
          // BCLDebug.ConsoleError("__ConsoleStream::Write: throwing on error. Error code: "+errorCode+" 0x"+errorCode.ToString("x")+" handle: "+_handle.ToString());
          throw new IOException("write error", errorCode);
        }
        return;
      }
      // P/Invoke wrappers for writing to and from a file, nearly identical
      // to the ones on FileStream. These are duplicated to save startup/hello
      // world working set.
      unsafe private static int ReadFileNative(SafeFileHandle hFile, byte[] bytes, int offset, int count, int mustBeZero, out int errorCode)
      {
        // Don't corrupt memory when multiple threads are erroneously writing
        // to this stream simultaneously.
        if (bytes.Length - offset < count)
          throw new IndexOutOfRangeException("IndexOutOfRange_IORaceCondition");
        // You can't use the fixed statement on an array of length 0.
        if (bytes.Length == 0)
        {
          errorCode = 0;
          return 0;
        }
        int r;
        int numBytesRead;
        fixed (byte* p = bytes)
        {
          r = ReadFile(hFile, p + offset, count, out numBytesRead, IntPtr.Zero);
        }
        if (r == 0)
        {
          errorCode = Marshal.GetLastWin32Error();
          if (errorCode == ERROR_BROKEN_PIPE)
          {
            // A pipe into stdin was closed. Not an error, but EOF.
            return 0;
          }
          return -1;
        }
        else
          errorCode = 0;
        return numBytesRead;
      }
      unsafe private static int WriteFileNative(SafeFileHandle hFile, byte[] bytes, int offset, int count, int mustBeZero, out int errorCode)
      {
        // You can't use the fixed statement on an array of length 0.
        if (bytes.Length == 0)
        {
          errorCode = 0;
          return 0;
        }
        int numBytesWritten = 0;
        int r;
        fixed (byte* p = bytes)
        {
          r = WriteFile(hFile, p + offset, count, out numBytesWritten, IntPtr.Zero);
        }
        if (r == 0)
        {
          errorCode = Marshal.GetLastWin32Error();
          if (errorCode == ERROR_NO_DATA || errorCode == ERROR_BROKEN_PIPE)
            return 0;
          return -1;
        }
        else
        {
          errorCode = 0;
        }
        return numBytesWritten;
      }
      // The P/Invoke declarations for ReadFile and WriteFile are here for a reason! This prevents us from loading several classes
      // in the trivial hello world case.
      [DllImport("kernel32.dll", SetLastError = true)]
      unsafe private static extern int ReadFile(SafeFileHandle handle, byte* bytes, int numBytesToRead, out int numBytesRead, IntPtr mustBeZero);
      [DllImport("kernel32.dll", SetLastError = true)]
      unsafe static internal extern int WriteFile(SafeFileHandle handle, byte* bytes, int numBytesToWrite, out int numBytesWritten, IntPtr mustBeZero);
    }
    private const int _DefaultConsoleBufferSize = 256;
    private static TextReader _in;
    private static TextWriter _out;
    private static TextWriter _error;
    // Private object for locking instead of locking on a public type for SQL reliability work.
    private static object s_InternalSyncObject;
    private static object InternalSyncObject
    {
      get
      {
        if (s_InternalSyncObject == null)
        {
          object o = new object();
          Interlocked.CompareExchange(ref s_InternalSyncObject, o, null);
        }
        return s_InternalSyncObject;
      }
    }
    // About reliability: I'm not using SafeHandle here. We don't
    // need to close these handles, and we don't allow the user to close
    // them so we don't have many of the security problems inherent in
    // something like file handles. Additionally, in a host like SQL
    // Server, we won't have a console.
    private static IntPtr _consoleInputHandle;
    private static IntPtr _consoleOutputHandle;
    const int STD_OUTPUT_HANDLE = -11;
    const int STD_INPUT_HANDLE = -10;
    const int STD_ERROR_HANDLE = -12;
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr GetStdHandle(int nStdHandle);
    [DllImport("kernel32.dll", SetLastError = true)]
    internal static extern uint GetConsoleCP();
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern uint GetConsoleOutputCP();
    private static IntPtr ConsoleInputHandle
    {
      get
      {
        if (_consoleInputHandle == IntPtr.Zero)
        {
          _consoleInputHandle = GetStdHandle(STD_INPUT_HANDLE);
        }
        return _consoleInputHandle;
      }
    }
    private static IntPtr ConsoleOutputHandle
    {
      get
      {
        if (_consoleOutputHandle == IntPtr.Zero)
        {
          _consoleOutputHandle = GetStdHandle(STD_OUTPUT_HANDLE);
        }
        return _consoleOutputHandle;
      }
    }
    public static TextWriter Error
    {
      get
      {
        // Hopefully this is inlineable.
        if (_error == null)
          InitializeStdOutError(false);
        return _error;
      }
    }
    public static TextReader In
    {
      get
      {
        // Because most applications don't use stdin, we can delay
        // initialize it slightly better startup performance.
        if (_in == null)
        {
          lock (InternalSyncObject)
          {
            if (_in == null)
            {
              // Set up Console.In
              Stream s = OpenStandardInput(_DefaultConsoleBufferSize);
              TextReader tr;
              if (s == Stream.Null)
                tr = StreamReader.Null;
              else
              {
                // Hopefully Encoding.GetEncoding doesn't load as many classes now.
                Encoding enc = Encoding.GetEncoding((int)GetConsoleCP());
                tr = TextReader.Synchronized(new StreamReader(s, enc, false, _DefaultConsoleBufferSize));
              }
              System.Threading.Thread.MemoryBarrier();
              _in = tr;
            }
          }
        }
        return _in;
      }
    }
    public static TextWriter Out
    {
      get
      {
        // Hopefully this is inlineable.
        if (_out == null)
          InitializeStdOutError(true);
        return _out;
      }
    }
    private static void InitializeStdOutError(bool stdout)
    {
      // Set up Console.Out or Console.Error.
      lock (InternalSyncObject)
      {
        if (stdout && _out != null)
          return;
        else if (!stdout && _error != null)
          return;
        TextWriter writer = null;
        Stream s;
        if (stdout)
          s = OpenStandardOutput(_DefaultConsoleBufferSize);
        else
          s = OpenStandardError(_DefaultConsoleBufferSize);
        if (s == Stream.Null)
        {
#if _DEBUG
                    if (CheckOutputDebug())
                        writer = MakeDebugOutputTextWriter((stdout) ? "Console.Out: " : "Console.Error: ");
                    else
#endif // _DEBUG
          writer = TextWriter.Synchronized(StreamWriter.Null);
        }
        else
        {
          int codePage = (int)GetConsoleOutputCP();
          Encoding encoding = Encoding.GetEncoding(codePage);
          StreamWriter stdxxx = new StreamWriter(s, encoding, _DefaultConsoleBufferSize);
          // stdxxx.HaveWrittenPreamble = true;
          stdxxx.AutoFlush = true;
          writer = TextWriter.Synchronized(stdxxx);
        }
        if (stdout)
          _out = writer;
        else
          _error = writer;
      }
    }
    // This method is only exposed via methods to get at the console.
    // We won't use any security checks here.
    private static Stream GetStandardFile(int stdHandleName, FileAccess access, int bufferSize)
    {
      // We shouldn't close the handle for stdout, etc, or we'll break
      // unmanaged code in the process that will print to console.
      // We should have a better way of marking this on SafeHandle.
      IntPtr handle = GetStdHandle(stdHandleName);
      SafeFileHandle sh = new SafeFileHandle(handle, false);
      // If someone launches a managed process via CreateProcess, stdout
      // stderr, & stdin could independently be set to INVALID_HANDLE_VALUE.
      // Additionally they might use 0 as an invalid handle.
      if (sh.IsInvalid)
      {
        // Minor perf optimization - get it out of the finalizer queue.
        sh.SetHandleAsInvalid();
        return Stream.Null;
      }
      // BCLDebug.ConsoleError("Console::GetStandardFile for std handle "+stdHandleName+" succeeded, returning handle number "+handle.ToString());
      Stream console = new MyConsoleStream(sh, access);
      // Do not buffer console streams, or we can get into situations where
      // we end up blocking waiting for you to hit enter twice. It was
      // redundant.
      return console;
    }
    public static Stream OpenStandardError()
    {
      return OpenStandardError(_DefaultConsoleBufferSize);
    }
    public static Stream OpenStandardError(int bufferSize)
    {
      if (bufferSize < 0)
        throw new ArgumentOutOfRangeException("bufferSize", "ArgumentOutOfRange_NeedNonNegNum");
      return GetStandardFile(STD_ERROR_HANDLE, FileAccess.Write, bufferSize);
    }
    public static Stream OpenStandardInput()
    {
      return OpenStandardInput(_DefaultConsoleBufferSize);
    }
    public static Stream OpenStandardInput(int bufferSize)
    {
      if (bufferSize < 0)
        throw new ArgumentOutOfRangeException("bufferSize", "ArgumentOutOfRange_NeedNonNegNum");
      return GetStandardFile(STD_INPUT_HANDLE, FileAccess.Read, bufferSize);
    }
    public static Stream OpenStandardOutput()
    {
      return OpenStandardOutput(_DefaultConsoleBufferSize);
    }
    public static Stream OpenStandardOutput(int bufferSize)
    {
      if (bufferSize < 0)
        throw new ArgumentOutOfRangeException("bufferSize", "ArgumentOutOfRange_NeedNonNegNum");
      return GetStandardFile(STD_OUTPUT_HANDLE, FileAccess.Write, bufferSize);
    }
  }
}

We appreciate your feedback. If you have any questions, comments, or suggestions about this article please contact our support team at support@nsoftware.com.