address issues and suggestions in PR review

This commit is contained in:
oising 2019-04-23 17:38:35 -04:00
parent 7eea98d4ea
commit 2dc178b852
4 changed files with 75 additions and 65 deletions

View file

@ -2,15 +2,15 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace Nivot.Terminal
namespace Samples.Terminal
{
/// <summary>
/// Implements a circular buffer.
/// Implements a bounded queue that won't block on overflow; instead the oldest item is discarded.
/// </summary>
/// <typeparam name="T"></typeparam>
public class ConcurrentCircularQueue<T> : ConcurrentQueue<T>
public class ConcurrentBoundedQueue<T> : ConcurrentQueue<T>
{
public ConcurrentCircularQueue(int capacity)
public ConcurrentBoundedQueue(int capacity)
{
Capacity = GetAlignedCapacity(capacity);
}
@ -20,7 +20,7 @@ namespace Nivot.Terminal
/// </summary>
/// <param name="collection"></param>
/// <param name="capacity"></param>
public ConcurrentCircularQueue(IEnumerable<T> collection, int capacity) : base(collection)
public ConcurrentBoundedQueue(IEnumerable<T> collection, int capacity) : base(collection)
{
Capacity = GetAlignedCapacity(capacity);
}
@ -40,6 +40,7 @@ namespace Nivot.Terminal
public new void Enqueue(T item)
{
// if we're about to overflow, dump oldest item
if (Count >= Capacity)
{
lock (this)

View file

@ -1,7 +1,8 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Nivot.Terminal
namespace Samples.Terminal
{
internal static class NativeMethods
{
@ -9,7 +10,10 @@ namespace Nivot.Terminal
{
// Don't convert it if it is already an HRESULT
if ((0xFFFF0000 & errorCode) != 0)
{
Debug.Assert(false, "errorCode is already HRESULT");
return errorCode;
}
return unchecked(((int)0x80070000) | errorCode);
}
@ -18,5 +22,10 @@ namespace Nivot.Terminal
{
return Marshal.GetExceptionForHR(MakeHRFromErrorCode(errorCode));
}
internal static Exception GetExceptionForLastWin32Error()
{
return GetExceptionForWin32Error(Marshal.GetLastWin32Error());
}
}
}

View file

@ -39,14 +39,18 @@ using System.Threading.Tasks;
using Pipelines.Sockets.Unofficial;
using Vanara.PInvoke;
namespace Nivot.Terminal
namespace Samples.Terminal
{
internal class Program
{
private static async Task Main(string[] args)
{
// run for 90 seconds
const int timeout = 90000;
var timeout = TimeSpan.FromSeconds(90);
// in reality this will likely never be reached, but it is useful to guard against
// conditions where the queue isn't drained, or not drained fast enough.
const int maxNonKeyEventRetention = 128;
var source = new CancellationTokenSource(timeout);
var token = source.Token;
@ -55,25 +59,17 @@ namespace Nivot.Terminal
if (!Kernel32.GetConsoleMode(handle, out Kernel32.CONSOLE_INPUT_MODE mode))
throw NativeMethods.GetExceptionForWin32Error(Marshal.GetLastWin32Error());
// enable VT sequences so cursor movement etc is encapsulated in the stream
mode |= Kernel32.CONSOLE_INPUT_MODE.ENABLE_WINDOW_INPUT;
mode |= Kernel32.CONSOLE_INPUT_MODE.ENABLE_VIRTUAL_TERMINAL_INPUT;
mode &= ~Kernel32.CONSOLE_INPUT_MODE.ENABLE_ECHO_INPUT;
mode &= ~Kernel32.CONSOLE_INPUT_MODE.ENABLE_LINE_INPUT;
if (!Kernel32.SetConsoleMode(handle, mode))
throw NativeMethods.GetExceptionForWin32Error(Marshal.GetLastWin32Error());
throw NativeMethods.GetExceptionForLastWin32Error();
// set utf-8 cp
if (!Kernel32.SetConsoleCP(65001))
throw NativeMethods.GetExceptionForWin32Error(Marshal.GetLastWin32Error());
if (!Kernel32.SetConsoleOutputCP(65001))
throw NativeMethods.GetExceptionForWin32Error(Marshal.GetLastWin32Error());
// base our provider/consumer on a circular buffer to keep memory usage under control
// base our provider/consumer on a bounded queue to keep memory usage under control
var events = new BlockingCollection<Kernel32.INPUT_RECORD>(
new ConcurrentCircularQueue<Kernel32.INPUT_RECORD>(256));
new ConcurrentBoundedQueue<Kernel32.INPUT_RECORD>(maxNonKeyEventRetention));
// Task that will consume non-key events asynchronously
var consumeEvents = Task.Run(() =>
@ -124,7 +120,7 @@ namespace Nivot.Terminal
while (sequence.TryGet(ref segment, out var mem))
{
// decode back from unicode (2 bytes per char)
// decode back from unicode
var datum = Encoding.Unicode.GetString(mem.Span);
Console.Write(datum);
}

View file

@ -5,7 +5,7 @@ using System.IO;
using Vanara.PInvoke;
namespace Nivot.Terminal
namespace Samples.Terminal
{
/// <summary>
/// Provides a Stream-oriented view over the console's input buffer key events
@ -93,61 +93,65 @@ namespace Nivot.Terminal
var records = new Kernel32.INPUT_RECORD[BufferSize];
// begin input loop
waitForInput:
var readSuccess = Kernel32.ReadConsoleInput(_handle, records, 256, out var recordsRead);
Debug.WriteLine("Read {0} input record(s)", recordsRead);
if (readSuccess && recordsRead > 0)
do
{
for (var index = 0; index < recordsRead; index++)
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)
{
var record = records[index];
if (record.EventType == Kernel32.EVENT_TYPE.KEY_EVENT)
for (var index = 0; index < recordsRead; index++)
{
// skip key up events - if not, every key will be duped in the stream
if (record.Event.KeyEvent.bKeyDown == false) continue;
var record = records[index];
// 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++)
if (record.EventType == Kernel32.EVENT_TYPE.KEY_EVENT)
{
buffer[offset + charsRead * BytesPerWChar] = lsb;
buffer[offset + charsRead * BytesPerWChar + 1] = msb;
// skip key up events - if not, every key will be duped in the stream
if (record.Event.KeyEvent.bKeyDown == false) continue;
charsRead++;
}
}
else
{
// ignore focus events (not doing so makes debugging absolutely hilarious)
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 miniscule delay
_nonKeyEvents.TryAdd(record);
// 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 miniscule delay
_nonKeyEvents.TryAdd(record);
}
}
}
bytesRead = charsRead * BytesPerWChar;
}
else
{
Debug.Assert(bytesRead == 0, "bytesRead == 0");
}
bytesRead = charsRead * BytesPerWChar;
// we should continue to block if no chars read (KEY_EVENT)
// even though non-key events were dispatched
if (bytesRead == 0) goto waitForInput;
}
else
{
Debug.Assert(bytesRead == 0, "bytesRead == 0");
}
} while (bytesRead == 0);
Debug.WriteLine("Read {0} character(s)", charsRead);
ret = Win32Error.ERROR_SUCCESS;
}