address issues and suggestions in PR review
This commit is contained in:
parent
7eea98d4ea
commit
2dc178b852
|
@ -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)
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue