Skip to content

Commit 2dc178b

Browse files
committed
address issues and suggestions in PR review
1 parent 7eea98d commit 2dc178b

File tree

4 files changed

+72
-62
lines changed

4 files changed

+72
-62
lines changed

tools/ReadConsoleInputStream/ConcurrentCircularQueue.cs renamed to tools/ReadConsoleInputStream/ConcurrentBoundedQueue.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
44

5-
namespace Nivot.Terminal
5+
namespace Samples.Terminal
66
{
77
/// <summary>
8-
/// Implements a circular buffer.
8+
/// Implements a bounded queue that won't block on overflow; instead the oldest item is discarded.
99
/// </summary>
1010
/// <typeparam name="T"></typeparam>
11-
public class ConcurrentCircularQueue<T> : ConcurrentQueue<T>
11+
public class ConcurrentBoundedQueue<T> : ConcurrentQueue<T>
1212
{
13-
public ConcurrentCircularQueue(int capacity)
13+
public ConcurrentBoundedQueue(int capacity)
1414
{
1515
Capacity = GetAlignedCapacity(capacity);
1616
}
@@ -20,7 +20,7 @@ public ConcurrentCircularQueue(int capacity)
2020
/// </summary>
2121
/// <param name="collection"></param>
2222
/// <param name="capacity"></param>
23-
public ConcurrentCircularQueue(IEnumerable<T> collection, int capacity) : base(collection)
23+
public ConcurrentBoundedQueue(IEnumerable<T> collection, int capacity) : base(collection)
2424
{
2525
Capacity = GetAlignedCapacity(capacity);
2626
}
@@ -40,6 +40,7 @@ private int GetAlignedCapacity(int n)
4040

4141
public new void Enqueue(T item)
4242
{
43+
// if we're about to overflow, dump oldest item
4344
if (Count >= Capacity)
4445
{
4546
lock (this)
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
using System;
2+
using System.Diagnostics;
23
using System.Runtime.InteropServices;
34

4-
namespace Nivot.Terminal
5+
namespace Samples.Terminal
56
{
67
internal static class NativeMethods
78
{
89
private static int MakeHRFromErrorCode(int errorCode)
910
{
1011
// Don't convert it if it is already an HRESULT
1112
if ((0xFFFF0000 & errorCode) != 0)
13+
{
14+
Debug.Assert(false, "errorCode is already HRESULT");
1215
return errorCode;
16+
}
1317

1418
return unchecked(((int)0x80070000) | errorCode);
1519
}
@@ -18,5 +22,10 @@ internal static Exception GetExceptionForWin32Error(int errorCode)
1822
{
1923
return Marshal.GetExceptionForHR(MakeHRFromErrorCode(errorCode));
2024
}
25+
26+
internal static Exception GetExceptionForLastWin32Error()
27+
{
28+
return GetExceptionForWin32Error(Marshal.GetLastWin32Error());
29+
}
2130
}
2231
}

tools/ReadConsoleInputStream/Program.cs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,18 @@
3939
using Pipelines.Sockets.Unofficial;
4040
using Vanara.PInvoke;
4141

42-
namespace Nivot.Terminal
42+
namespace Samples.Terminal
4343
{
4444
internal class Program
4545
{
4646
private static async Task Main(string[] args)
4747
{
4848
// run for 90 seconds
49-
const int timeout = 90000;
49+
var timeout = TimeSpan.FromSeconds(90);
50+
51+
// in reality this will likely never be reached, but it is useful to guard against
52+
// conditions where the queue isn't drained, or not drained fast enough.
53+
const int maxNonKeyEventRetention = 128;
5054

5155
var source = new CancellationTokenSource(timeout);
5256
var token = source.Token;
@@ -55,25 +59,17 @@ private static async Task Main(string[] args)
5559
if (!Kernel32.GetConsoleMode(handle, out Kernel32.CONSOLE_INPUT_MODE mode))
5660
throw NativeMethods.GetExceptionForWin32Error(Marshal.GetLastWin32Error());
5761

58-
// enable VT sequences so cursor movement etc is encapsulated in the stream
5962
mode |= Kernel32.CONSOLE_INPUT_MODE.ENABLE_WINDOW_INPUT;
6063
mode |= Kernel32.CONSOLE_INPUT_MODE.ENABLE_VIRTUAL_TERMINAL_INPUT;
6164
mode &= ~Kernel32.CONSOLE_INPUT_MODE.ENABLE_ECHO_INPUT;
6265
mode &= ~Kernel32.CONSOLE_INPUT_MODE.ENABLE_LINE_INPUT;
6366

6467
if (!Kernel32.SetConsoleMode(handle, mode))
65-
throw NativeMethods.GetExceptionForWin32Error(Marshal.GetLastWin32Error());
66-
67-
// set utf-8 cp
68-
if (!Kernel32.SetConsoleCP(65001))
69-
throw NativeMethods.GetExceptionForWin32Error(Marshal.GetLastWin32Error());
70-
71-
if (!Kernel32.SetConsoleOutputCP(65001))
72-
throw NativeMethods.GetExceptionForWin32Error(Marshal.GetLastWin32Error());
68+
throw NativeMethods.GetExceptionForLastWin32Error();
7369

74-
// base our provider/consumer on a circular buffer to keep memory usage under control
70+
// base our provider/consumer on a bounded queue to keep memory usage under control
7571
var events = new BlockingCollection<Kernel32.INPUT_RECORD>(
76-
new ConcurrentCircularQueue<Kernel32.INPUT_RECORD>(256));
72+
new ConcurrentBoundedQueue<Kernel32.INPUT_RECORD>(maxNonKeyEventRetention));
7773

7874
// Task that will consume non-key events asynchronously
7975
var consumeEvents = Task.Run(() =>
@@ -124,7 +120,7 @@ private static async Task Main(string[] args)
124120

125121
while (sequence.TryGet(ref segment, out var mem))
126122
{
127-
// decode back from unicode (2 bytes per char)
123+
// decode back from unicode
128124
var datum = Encoding.Unicode.GetString(mem.Span);
129125
Console.Write(datum);
130126
}

tools/ReadConsoleInputStream/ReadConsoleInputStream.cs

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
using Vanara.PInvoke;
77

8-
namespace Nivot.Terminal
8+
namespace Samples.Terminal
99
{
1010
/// <summary>
1111
/// Provides a Stream-oriented view over the console's input buffer key events
@@ -93,61 +93,65 @@ public override int Read(byte[] buffer, int offset, int count)
9393
var records = new Kernel32.INPUT_RECORD[BufferSize];
9494

9595
// begin input loop
96-
waitForInput:
97-
98-
var readSuccess = Kernel32.ReadConsoleInput(_handle, records, 256, out var recordsRead);
99-
Debug.WriteLine("Read {0} input record(s)", recordsRead);
100-
101-
if (readSuccess && recordsRead > 0)
96+
do
10297
{
103-
for (var index = 0; index < recordsRead; index++)
98+
var readSuccess = Kernel32.ReadConsoleInput(_handle, records, BufferSize, out var recordsRead);
99+
Debug.WriteLine("Read {0} input record(s)", recordsRead);
100+
101+
// some of the arithmetic here is deliberately more explicit than it needs to be
102+
// in order to show how 16-bit unicode WCHARs are packed into the buffer. The console
103+
// subsystem is one of the last bastions of UCS-2, so until UTF-16 is fully adopted
104+
// the two-byte character assumptions below will hold.
105+
if (readSuccess && recordsRead > 0)
104106
{
105-
var record = records[index];
106-
107-
if (record.EventType == Kernel32.EVENT_TYPE.KEY_EVENT)
107+
for (var index = 0; index < recordsRead; index++)
108108
{
109-
// skip key up events - if not, every key will be duped in the stream
110-
if (record.Event.KeyEvent.bKeyDown == false) continue;
109+
var record = records[index];
111110

112-
// pack ucs-2/utf-16le/unicode chars into position in our byte[] buffer.
113-
var glyph = (ushort) record.Event.KeyEvent.uChar;
111+
if (record.EventType == Kernel32.EVENT_TYPE.KEY_EVENT)
112+
{
113+
// skip key up events - if not, every key will be duped in the stream
114+
if (record.Event.KeyEvent.bKeyDown == false) continue;
114115

115-
var lsb = (byte) (glyph & 0xFFu);
116-
var msb = (byte) ((glyph >> 8) & 0xFFu);
116+
// pack ucs-2/utf-16le/unicode chars into position in our byte[] buffer.
117+
var glyph = (ushort) record.Event.KeyEvent.uChar;
117118

118-
// ensure we accommodate key repeat counts
119-
for (var n = 0; n < record.Event.KeyEvent.wRepeatCount; n++)
120-
{
121-
buffer[offset + charsRead * BytesPerWChar] = lsb;
122-
buffer[offset + charsRead * BytesPerWChar + 1] = msb;
119+
var lsb = (byte) (glyph & 0xFFu);
120+
var msb = (byte) ((glyph >> 8) & 0xFFu);
121+
122+
// ensure we accommodate key repeat counts
123+
for (var n = 0; n < record.Event.KeyEvent.wRepeatCount; n++)
124+
{
125+
buffer[offset + charsRead * BytesPerWChar] = lsb;
126+
buffer[offset + charsRead * BytesPerWChar + 1] = msb;
123127

124-
charsRead++;
128+
charsRead++;
129+
}
125130
}
126-
}
127-
else
128-
{
129-
// ignore focus events (not doing so makes debugging absolutely hilarious)
130-
if (record.EventType != Kernel32.EVENT_TYPE.FOCUS_EVENT)
131+
else
131132
{
132-
// I assume success adding records - this is not so critical
133-
// if it is critical to you, loop on this with a miniscule delay
134-
_nonKeyEvents.TryAdd(record);
133+
// ignore focus events; not doing so makes debugging absolutely hilarious
134+
// when breakpoints repeatedly cause focus events to occur as your view toggles
135+
// between IDE and console.
136+
if (record.EventType != Kernel32.EVENT_TYPE.FOCUS_EVENT)
137+
{
138+
// I assume success adding records - this is not so critical
139+
// if it is critical to you, loop on this with a miniscule delay
140+
_nonKeyEvents.TryAdd(record);
141+
}
135142
}
136143
}
144+
bytesRead = charsRead * BytesPerWChar;
145+
}
146+
else
147+
{
148+
Debug.Assert(bytesRead == 0, "bytesRead == 0");
137149
}
138150

139-
bytesRead = charsRead * BytesPerWChar;
140-
141-
// we should continue to block if no chars read (KEY_EVENT)
142-
// even though non-key events were dispatched
143-
if (bytesRead == 0) goto waitForInput;
144-
}
145-
else
146-
{
147-
Debug.Assert(bytesRead == 0, "bytesRead == 0");
148-
}
149-
151+
} while (bytesRead == 0);
152+
150153
Debug.WriteLine("Read {0} character(s)", charsRead);
154+
151155
ret = Win32Error.ERROR_SUCCESS;
152156
}
153157

0 commit comments

Comments
 (0)