using System; using System.Collections.Concurrent; using System.Diagnostics; using System.IO; using Vanara.PInvoke; namespace Samples.Terminal { /// /// Provides a Stream-oriented view over the console's input buffer key events /// while also collecting out of band events like buffer resize, menu etc in /// a caller-provided BlockingCollection. /// /// The buffer contains unicode chars, not 8 bit CP encoded chars as we rely on ReadConsoleInputW. public sealed class ReadConsoleInputStream : Stream { private const int BufferSize = 256; private const int BytesPerWChar = 2; private readonly BlockingCollection _nonKeyEvents; private IntPtr _handle; /// /// Creates an instance of ReadConsoleInputStream over the standard handle for StdIn. /// /// A BlockingCollection provider/consumer collection for collecting non key events. public ReadConsoleInputStream(BlockingCollection nonKeyEvents) : this(Kernel32.GetStdHandle(Kernel32.StdHandleType.STD_INPUT_HANDLE), nonKeyEvents) { } /// /// Creates an instance of ReadConsoleInputStream over a caller-provided standard handle for stdin. /// /// A HFILE handle representing StdIn /// A BlockingCollection provider/consumer collection for collecting non key events. internal ReadConsoleInputStream(HFILE handle, BlockingCollection nonKeyEvents) { Debug.Assert(!handle.IsInvalid, "handle.IsInvalid == false"); _handle = handle.DangerousGetHandle(); _nonKeyEvents = nonKeyEvents; } public override bool CanRead { get; } = true; public override bool CanWrite => false; public override bool CanSeek => false; public override long Length => throw new NotSupportedException("Seek not supported."); public override long Position { get => throw new NotSupportedException("Seek not supported."); set => throw new NotSupportedException("Seek not supported."); } protected override void Dispose(bool disposing) { _handle = IntPtr.Zero; base.Dispose(disposing); } public override int Read(byte[] buffer, int offset, int count) { ValidateRead(buffer, offset, count); Debug.Assert(offset >= 0, "offset >= 0"); Debug.Assert(count >= 0, "count >= 0"); Debug.Assert(buffer != null, "bytes != null"); // Don't corrupt memory when multiple threads are erroneously writing // to this stream simultaneously. if (buffer.Length - offset < count) throw new IndexOutOfRangeException("IndexOutOfRange_IORaceCondition"); int bytesRead; int ret; if (buffer.Length == 0) { bytesRead = 0; ret = Win32Error.ERROR_SUCCESS; } else { var charsRead = 0; bytesRead = 0; var records = new Kernel32.INPUT_RECORD[BufferSize]; // begin input loop do { var readSuccess = Kernel32.ReadConsoleInput(_handle, records, BufferSize, out var recordsRead); Debug.WriteLine("Read {0} input record(s)", recordsRead); // some of the arithmetic here is deliberately more explicit than it needs to be // in order to show how 16-bit unicode WCHARs are packed into the buffer. The console // subsystem is one of the last bastions of UCS-2, so until UTF-16 is fully adopted // the two-byte character assumptions below will hold. if (readSuccess && recordsRead > 0) { for (var index = 0; index < recordsRead; index++) { var record = records[index]; if (record.EventType == Kernel32.EVENT_TYPE.KEY_EVENT) { // skip key up events - if not, every key will be duped in the stream if (!record.Event.KeyEvent.bKeyDown) continue; // pack ucs-2/utf-16le/unicode chars into position in our byte[] buffer. var glyph = (ushort) record.Event.KeyEvent.uChar; var lsb = (byte) (glyph & 0xFFu); var msb = (byte) ((glyph >> 8) & 0xFFu); // ensure we accommodate key repeat counts for (var n = 0; n < record.Event.KeyEvent.wRepeatCount; n++) { buffer[offset + charsRead * BytesPerWChar] = lsb; buffer[offset + charsRead * BytesPerWChar + 1] = msb; charsRead++; } } else { // ignore focus events; not doing so makes debugging absolutely hilarious // when breakpoints repeatedly cause focus events to occur as your view toggles // between IDE and console. if (record.EventType != Kernel32.EVENT_TYPE.FOCUS_EVENT) { // I assume success adding records - this is not so critical // if it is critical to you, loop on this with a minuscule delay _nonKeyEvents.TryAdd(record); } } } bytesRead = charsRead * BytesPerWChar; } else { Debug.Assert(bytesRead == 0, "bytesRead == 0"); } } while (bytesRead == 0); Debug.WriteLine("Read {0} character(s)", charsRead); ret = Win32Error.ERROR_SUCCESS; } var errCode = ret; if (Win32Error.ERROR_SUCCESS != errCode) throw NativeMethods.GetExceptionForWin32Error(errCode); return bytesRead; } public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException("Write operations not implemented."); } public override void Flush() { throw new NotSupportedException("Flush/Write not supported."); } public override void SetLength(long value) { throw new NotSupportedException("Seek not supported."); } public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException("Seek not supported."); } private void ValidateRead(byte[] buffer, int offset, int count) { if (buffer == null) throw new ArgumentNullException(nameof(buffer)); if (offset < 0 || count < 0) throw new ArgumentOutOfRangeException(offset < 0 ? nameof(offset) : nameof(count), "offset or count cannot be negative numbers."); if (buffer.Length - offset < count) throw new ArgumentException("invalid offset length."); if (!CanRead) throw new NotSupportedException("Get read not supported."); } } }