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.