Use overlapped IO when reading from the console (#40814)
* Use overlapped IO when reading from the console * Apply PR feedback
Blue committed
Jun 16, 2026 at 16:55 UTC
5db2759f178b0be678853cbc909d5f945d5c9279
5 files changed
+488
-407
src/windows/common/HandleIO.cpp
+377
@@ -73,6 +73,69 @@ inline void UnregisterWait(HANDLE waitHandle) noexcept
73
74
using unique_registered_wait = wil::unique_any_handle_null<decltype(&UnregisterWait), &UnregisterWait>;
75
76
+#define TTY_ALT_NUMPAD_VK_MENU (0x12)
77
+#define TTY_ESCAPE_CHARACTER (L'\x1b')
78
+#define TTY_INPUT_EVENT_BUFFER_SIZE (16)
79
+#define TTY_UTF8_TRANSLATION_BUFFER_SIZE (4 * TTY_INPUT_EVENT_BUFFER_SIZE)
80
+
81
+BOOL IsActionableKey(_In_ PKEY_EVENT_RECORD KeyEvent)
82
+{
83
+ //
84
+ // This is a bit complicated to discern.
85
+ //
86
+ // 1. Our first check is that we only want structures that
87
+ // represent at least one key press. If we have 0, then we don't
88
+ // need to bother. If we have >1, we'll send the key through
89
+ // that many times into the pipe.
90
+ // 2. Our second check is where it gets confusing.
91
+ // a. Characters that are non-null get an automatic pass. Copy
92
+ // them through to the pipe.
93
+ // b. Null characters need further scrutiny. We generally do not
94
+ // pass nulls through EXCEPT if they're sourced from the
95
+ // virtual terminal engine (or another application living
96
+ // above our layer). If they're sourced by a non-keyboard
97
+ // source, they'll have no scan code (since they didn't come
98
+ // from a keyboard). But that rule has an exception too:
99
+ // "Enhanced keys" from above the standard range of scan
100
+ // codes will return 0 also with a special flag set that says
101
+ // they're an enhanced key. That means the desired behavior
102
+ // is:
103
+ // Scan Code = 0, ENHANCED_KEY = 0
104
+ // -> This came from the VT engine or another app
105
+ // above our layer.
106
+ // Scan Code = 0, ENHANCED_KEY = 1
107
+ // -> This came from the keyboard, but is a special
108
+ // key like 'Volume Up' that wasn't generally a
109
+ // part of historic (pre-1990s) keyboards.
110
+ // Scan Code = <anything else>
111
+ // -> This came from a keyboard directly.
112
+ //
113
+
114
+ if ((KeyEvent->wRepeatCount == 0) || ((KeyEvent->uChar.UnicodeChar == UNICODE_NULL) &&
115
+ ((KeyEvent->wVirtualScanCode != 0) || (WI_IsFlagSet(KeyEvent->dwControlKeyState, ENHANCED_KEY)))))
116
+ {
117
+ return FALSE;
118
+ }
119
+
120
+ return TRUE;
121
+}
122
+
123
+BOOL GetNextCharacter(_In_ INPUT_RECORD* InputRecord, _Out_ PWCHAR NextCharacter)
124
+{
125
+ BOOL IsNextCharacterValid = FALSE;
126
+ if (InputRecord->EventType == KEY_EVENT)
127
+ {
128
+ const auto KeyEvent = &InputRecord->Event.KeyEvent;
129
+ if ((IsActionableKey(KeyEvent) != FALSE) && ((KeyEvent->bKeyDown != FALSE) || (KeyEvent->wVirtualKeyCode == TTY_ALT_NUMPAD_VK_MENU)))
130
+ {
131
+ *NextCharacter = KeyEvent->uChar.UnicodeChar;
132
+ IsNextCharacterValid = TRUE;
133
+ }
134
+ }
135
+
136
+ return IsNextCharacterValid;
137
+}
138
+
139
} // namespace
140
141
// HandleWrapper
@@ -808,6 +871,320 @@ HANDLE ReadSocketMessageHandle::GetHandle() const
871
return Event.get();
872
}
873
874
+// ReadConsoleHandle
875
+
876
+wsl::windows::common::io::ReadConsoleHandle::ReadConsoleHandle(
877
+ HandleWrapper&& Console,
878
+ std::function<void(const gsl::span<char>& Buffer)>&& OnRead,
879
+ std::function<void()>&& UpdateTerminalSize,
880
+ std::vector<char> DetachSequence,
881
+ std::function<void()>&& OnDetach) :
882
+ Console(std::move(Console)),
883
+ OnRead(std::move(OnRead)),
884
+ UpdateTerminalSize(std::move(UpdateTerminalSize)),
885
+ DetachSequence(std::move(DetachSequence)),
886
+ OnDetach(std::move(OnDetach))
887
+{
888
+}
889
+
890
+void wsl::windows::common::io::ReadConsoleHandle::Schedule()
891
+{
892
+ WI_ASSERT(State == IOHandleStatus::Standby);
893
+
894
+ //
895
+ // Use the console handle as the signal event.
896
+ // N.B. This behavior is documented here: https://learn.microsoft.com/en-us/windows/console/readconsoleinput
897
+ //
898
+
899
+ State = IOHandleStatus::Pending;
900
+}
901
+
902
+HANDLE wsl::windows::common::io::ReadConsoleHandle::GetHandle() const
903
+{
904
+ return Console.Get();
905
+}
906
+
907
+void wsl::windows::common::io::ReadConsoleHandle::Collect()
908
+{
909
+ WI_ASSERT(State == IOHandleStatus::Pending);
910
+
911
+ //
912
+ // Re-arm by default; a detected detach sequence overrides this to Completed below.
913
+ //
914
+
915
+ State = IOHandleStatus::Standby;
916
+
917
+ //
918
+ // N.B. ReadConsoleInputEx has no associated import library.
919
+ //
920
+
921
+ static LxssDynamicFunction<decltype(ReadConsoleInputExW)> readConsoleInput(L"Kernel32.dll", "ReadConsoleInputExW");
922
+
923
+ INPUT_RECORD InputRecordBuffer[TTY_INPUT_EVENT_BUFFER_SIZE];
924
+ INPUT_RECORD* InputRecordPeek = &(InputRecordBuffer[1]);
925
+ KEY_EVENT_RECORD* KeyEvent;
926
+ DWORD RecordsRead;
927
+
928
+ //
929
+ // The console handle stays signaled while input is available, so drain all currently available
930
+ // input here and return to Standby once none remains (the handle is waited on again by the IO loop).
931
+ //
932
+
933
+ for (;;)
934
+ {
935
+ // Detach if the escape sequence was detected.
936
+ // N.B. This needs to be done at the beginning of the loop so the escape sequence is also delivered.
937
+ if (!CurrentSequence.empty() && std::ranges::equal(CurrentSequence, DetachSequence))
938
+ {
939
+ OnDetach();
940
+ State = IOHandleStatus::Completed;
941
+ return;
942
+ }
943
+
944
+ //
945
+ // Because some input events generated by the console are encoded with
946
+ // more than one input event, we have to be smart about reading the
947
+ // events.
948
+ //
949
+ // First, we peek at the next input event.
950
+ // If it's an escape (wch == L'\x1b') event, then the characters that
951
+ // follow are part of an input sequence. We can't know for sure
952
+ // how long that sequence is, but we can assume it's all sent to
953
+ // the input queue at once, and it's less that 16 events.
954
+ // Furthermore, we can assume that if there's an Escape in those
955
+ // 16 events, that the escape marks the start of a new sequence.
956
+ // So, we'll peek at another 15 events looking for escapes.
957
+ // If we see an escape, then we'll read one less than that,
958
+ // such that the escape remains the next event in the input.
959
+ // From those read events, we'll aggregate chars into a single
960
+ // string to send to the subsystem.
961
+ // If it's not an escape, send the event through one at a time.
962
+ //
963
+
964
+ //
965
+ // Read one input event without blocking. If none is available, all input has been drained.
966
+ //
967
+
968
+ THROW_IF_WIN32_BOOL_FALSE(readConsoleInput(Console.Get(), InputRecordBuffer, 1, &RecordsRead, CONSOLE_READ_NOWAIT));
969
+ if (RecordsRead == 0)
970
+ {
971
+ return;
972
+ }
973
+
974
+ //
975
+ // Don't read additional records if the first entry is a window size
976
+ // event, or a repeated character. Handle those events on their own.
977
+ //
978
+
979
+ DWORD RecordsPeeked = 0;
980
+ if ((InputRecordBuffer[0].EventType != WINDOW_BUFFER_SIZE_EVENT) &&
981
+ ((InputRecordBuffer[0].EventType != KEY_EVENT) || (InputRecordBuffer[0].Event.KeyEvent.wRepeatCount < 2)))
982
+ {
983
+ //
984
+ // Read additional input records into the buffer if available.
985
+ //
986
+
987
+ THROW_IF_WIN32_BOOL_FALSE(PeekConsoleInputW(Console.Get(), InputRecordPeek, (RTL_NUMBER_OF(InputRecordBuffer) - 1), &RecordsPeeked));
988
+ }
989
+
990
+ //
991
+ // Iterate over peeked records [1, RecordsPeeked].
992
+ //
993
+
994
+ DWORD AdditionalRecordsToRead = 0;
995
+ WCHAR NextCharacter;
996
+ for (DWORD RecordIndex = 1; RecordIndex <= RecordsPeeked; RecordIndex++)
997
+ {
998
+ if (GetNextCharacter(&InputRecordBuffer[RecordIndex], &NextCharacter) != FALSE)
999
+ {
1000
+ KeyEvent = &InputRecordBuffer[RecordIndex].Event.KeyEvent;
1001
+ if (NextCharacter == TTY_ESCAPE_CHARACTER)
1002
+ {
1003
+ //
1004
+ // CurrentRecord is an escape event. We will start here
1005
+ // on the next input loop.
1006
+ //
1007
+
1008
+ break;
1009
+ }
1010
+ else if (KeyEvent->wRepeatCount > 1)
1011
+ {
1012
+ //
1013
+ // Repeated keys are handled on their own. Start with this
1014
+ // key on the next input loop.
1015
+ //
1016
+
1017
+ break;
1018
+ }
1019
+ else if (IS_HIGH_SURROGATE(NextCharacter) && (RecordIndex >= (RecordsPeeked - 1)))
1020
+ {
1021
+ //
1022
+ // If there is not enough room for the second character of
1023
+ // a surrogate pair, start with this character on the next
1024
+ // input loop.
1025
+ //
1026
+ // N.B. The test is for at least two remaining records
1027
+ // because typically a surrogate pair will be entered
1028
+ // via copy/paste, which will appear as an input
1029
+ // record with alt-down, alt-up and character. So to
1030
+ // include the next character of the surrogate pair it
1031
+ // is likely that the alt-up record will need to be
1032
+ // read first.
1033
+ //
1034
+
1035
+ break;
1036
+ }
1037
+ }
1038
+ else if (InputRecordBuffer[RecordIndex].EventType == WINDOW_BUFFER_SIZE_EVENT)
1039
+ {
1040
+ //
1041
+ // A window size event is handled on its own.
1042
+ //
1043
+
1044
+ break;
1045
+ }
1046
+
1047
+ //
1048
+ // Process the additional input record.
1049
+ //
1050
+
1051
+ AdditionalRecordsToRead += 1;
1052
+ }
1053
+
1054
+ if (AdditionalRecordsToRead > 0)
1055
+ {
1056
+ THROW_IF_WIN32_BOOL_FALSE(readConsoleInput(Console.Get(), InputRecordPeek, AdditionalRecordsToRead, &RecordsRead, CONSOLE_READ_NOWAIT));
1057
+
1058
+ if (RecordsRead == 0)
1059
+ {
1060
+ //
1061
+ // This would be an unexpected case. We've already peeked to see
1062
+ // that there are AdditionalRecordsToRead # of records in the
1063
+ // input that need reading, yet we didn't get them when we read.
1064
+ // In this case, stop draining and wait to be signaled again.
1065
+ //
1066
+
1067
+ return;
1068
+ }
1069
+
1070
+ //
1071
+ // We already had one input record in the buffer before reading
1072
+ // additional, So account for that one too
1073
+ //
1074
+
1075
+ RecordsRead += 1;
1076
+ }
1077
+
1078
+ //
1079
+ // Process each input event. Keydowns will get aggregated into
1080
+ // Utf8String before getting injected into the subsystem.
1081
+ //
1082
+
1083
+ WCHAR Utf16String[TTY_INPUT_EVENT_BUFFER_SIZE];
1084
+ ULONG Utf16StringSize = 0;
1085
+ for (DWORD RecordIndex = 0; RecordIndex < RecordsRead; RecordIndex++)
1086
+ {
1087
+ INPUT_RECORD* CurrentInputRecord = &(InputRecordBuffer[RecordIndex]);
1088
+ switch (CurrentInputRecord->EventType)
1089
+ {
1090
+ case KEY_EVENT:
1091
+
1092
+ KeyEvent = &CurrentInputRecord->Event.KeyEvent;
1093
+
1094
+ if (KeyEvent->bKeyDown && IsActionableKey(KeyEvent) && !DetachSequence.empty())
1095
+ {
1096
+ if (CurrentSequence.size() >= DetachSequence.size())
1097
+ {
1098
+ CurrentSequence.pop_front();
1099
+ }
1100
+
1101
+ CurrentSequence.push_back(CurrentInputRecord->Event.KeyEvent.uChar.AsciiChar);
1102
+ }
1103
+
1104
+ //
1105
+ // Filter out key up events unless they are from an <Alt> key.
1106
+ // Key up with an <Alt> key could contain a Unicode character
1107
+ // pasted from the clipboard and converted to an <Alt>+<Numpad> sequence.
1108
+ //
1109
+
1110
+ if ((KeyEvent->bKeyDown == FALSE) && (KeyEvent->wVirtualKeyCode != TTY_ALT_NUMPAD_VK_MENU))
1111
+ {
1112
+ break;
1113
+ }
1114
+
1115
+ //
1116
+ // Filter out key presses that are not actionable, such as just
1117
+ // pressing <Ctrl>, <Alt>, <Shift> etc. These key presses return
1118
+ // the character of null but will have a valid scan code off the
1119
+ // keyboard. Certain other key sequences such as Ctrl+A,
1120
+ // Ctrl+<space>, and Ctrl+@ will also return the character null
1121
+ // but have no scan code.
1122
+ // <Alt> + <NumPad> sequences will show an <Alt> but will have
1123
+ // a scancode and character specified, so they should be actionable.
1124
+ //
1125
+
1126
+ if (IsActionableKey(KeyEvent) == FALSE)
1127
+ {
1128
+ break;
1129
+ }
1130
+
1131
+ Utf16String[Utf16StringSize] = KeyEvent->uChar.UnicodeChar;
1132
+ Utf16StringSize += 1;
1133
+ break;
1134
+
1135
+ case WINDOW_BUFFER_SIZE_EVENT:
1136
+
1137
+ //
1138
+ // Query the window size and send an update message via the
1139
+ // control channel.
1140
+ //
1141
+
1142
+ UpdateTerminalSize();
1143
+ break;
1144
+ }
1145
+ }
1146
+
1147
+ CHAR Utf8String[TTY_UTF8_TRANSLATION_BUFFER_SIZE];
1148
+ DWORD Utf8StringSize = 0;
1149
+ if (Utf16StringSize > 0)
1150
+ {
1151
+ //
1152
+ // Windows uses UTF-16LE encoding, Linux uses UTF-8 by default.
1153
+ // Convert each UTF-16LE character into the proper UTF-8 byte
1154
+ // sequence equivalent.
1155
+ //
1156
+
1157
+ THROW_LAST_ERROR_IF(
1158
+ (Utf8StringSize = WideCharToMultiByte(
1159
+ CP_UTF8, 0, Utf16String, Utf16StringSize, Utf8String, sizeof(Utf8String), nullptr, nullptr)) == 0);
1160
+ }
1161
+
1162
+ //
1163
+ // Deliver the translated input bytes.
1164
+ //
1165
+
1166
+ const auto Utf8Span = gsl::make_span(Utf8String, static_cast<size_t>(Utf8StringSize));
1167
+ if ((RecordsRead == 1) && (InputRecordBuffer[0].EventType == KEY_EVENT) && (InputRecordBuffer[0].Event.KeyEvent.wRepeatCount > 1))
1168
+ {
1169
+ WI_ASSERT(Utf16StringSize == 1);
1170
+
1171
+ //
1172
+ // Handle repeated characters. They aren't part of an input
1173
+ // sequence, so there's only one event that's generating characters.
1174
+ //
1175
+
1176
+ for (WORD RepeatIndex = 0; RepeatIndex < InputRecordBuffer[0].Event.KeyEvent.wRepeatCount; RepeatIndex += 1)
1177
+ {
1178
+ OnRead(Utf8Span);
1179
+ }
1180
+ }
1181
+ else if (Utf8StringSize > 0)
1182
+ {
1183
+ OnRead(Utf8Span);
1184
+ }
1185
+ }
1186
+}
1187
+
1188
// WriteHandle
1189
1190
WriteHandle::WriteHandle(HandleWrapper&& MovedHandle, const std::vector<char>& Source, bool CompleteOnDrained) :
src/windows/common/HandleIO.h
+32
-2
@@ -3,6 +3,7 @@
3
#pragma once
4
5
#include <concurrent_queue.h>
6
+#include <deque>
7
#include <list>
8
9
#define LX_RELAY_BUFFER_SIZE 0x1000
@@ -236,6 +237,32 @@ private:
237
size_t CurrentOffset = 0;
238
};
239
240
+class ReadConsoleHandle : public OverlappedIOHandle
241
+{
242
+public:
243
+ NON_COPYABLE(ReadConsoleHandle);
244
+ NON_MOVABLE(ReadConsoleHandle);
245
+
246
+ ReadConsoleHandle(
247
+ HandleWrapper&& Console,
248
+ std::function<void(const gsl::span<char>& Buffer)>&& OnRead,
249
+ std::function<void()>&& UpdateTerminalSize = []() {},
250
+ std::vector<char> DetachSequence = {},
251
+ std::function<void()>&& OnDetach = []() {});
252
+
253
+ void Schedule() override;
254
+ void Collect() override;
255
+ HANDLE GetHandle() const override;
256
+
257
+private:
258
+ HandleWrapper Console;
259
+ std::function<void(const gsl::span<char>& Buffer)> OnRead;
260
+ std::function<void()> UpdateTerminalSize;
261
+ std::vector<char> DetachSequence;
262
+ std::function<void()> OnDetach;
263
+ std::deque<char> CurrentSequence;
264
+};
265
+
266
class WriteHandle : public OverlappedIOHandle
267
{
268
public:
@@ -309,8 +336,11 @@ public:
336
NON_COPYABLE(RelayHandle);
337
NON_MOVABLE(RelayHandle);
338
312
- RelayHandle(HandleWrapper&& Input, HandleWrapper&& Output) :
313
- Read(std::move(Input), [this](const gsl::span<char>& Buffer) { return OnRead(Buffer); }), Write(std::move(Output), {}, false)
339
+ template <typename... TArgs>
340
+ RelayHandle(HandleWrapper&& Input, HandleWrapper&& Output, TArgs&&... InputArgs) :
341
+ Read(
342
+ std::move(Input), [this](const gsl::span<char>& Buffer) { return OnRead(Buffer); }, std::forward<TArgs>(InputArgs)...),
343
+ Write(std::move(Output), {}, false)
344
{
345
}
346
src/windows/common/relay.cpp
+7
-361
@@ -407,71 +407,7 @@ void wsl::windows::common::relay::BidirectionalRelay(_In_ HANDLE LeftHandle, _In
407
}
408
}
409
410
-#define TTY_ALT_NUMPAD_VK_MENU (0x12)
411
-#define TTY_ESCAPE_CHARACTER (L'\x1b')
412
-#define TTY_INPUT_EVENT_BUFFER_SIZE (16)
413
-#define TTY_UTF8_TRANSLATION_BUFFER_SIZE (4 * TTY_INPUT_EVENT_BUFFER_SIZE)
414
-
415
-BOOL IsActionableKey(_In_ PKEY_EVENT_RECORD KeyEvent)
416
-{
417
- //
418
- // This is a bit complicated to discern.
419
- //
420
- // 1. Our first check is that we only want structures that
421
- // represent at least one key press. If we have 0, then we don't
422
- // need to bother. If we have >1, we'll send the key through
423
- // that many times into the pipe.
424
- // 2. Our second check is where it gets confusing.
425
- // a. Characters that are non-null get an automatic pass. Copy
426
- // them through to the pipe.
427
- // b. Null characters need further scrutiny. We generally do not
428
- // pass nulls through EXCEPT if they're sourced from the
429
- // virtual terminal engine (or another application living
430
- // above our layer). If they're sourced by a non-keyboard
431
- // source, they'll have no scan code (since they didn't come
432
- // from a keyboard). But that rule has an exception too:
433
- // "Enhanced keys" from above the standard range of scan
434
- // codes will return 0 also with a special flag set that says
435
- // they're an enhanced key. That means the desired behavior
436
- // is:
437
- // Scan Code = 0, ENHANCED_KEY = 0
438
- // -> This came from the VT engine or another app
439
- // above our layer.
440
- // Scan Code = 0, ENHANCED_KEY = 1
441
- // -> This came from the keyboard, but is a special
442
- // key like 'Volume Up' that wasn't generally a
443
- // part of historic (pre-1990s) keyboards.
444
- // Scan Code = <anything else>
445
- // -> This came from a keyboard directly.
446
- //
447
-
448
- if ((KeyEvent->wRepeatCount == 0) || ((KeyEvent->uChar.UnicodeChar == UNICODE_NULL) &&
449
- ((KeyEvent->wVirtualScanCode != 0) || (WI_IsFlagSet(KeyEvent->dwControlKeyState, ENHANCED_KEY)))))
450
- {
451
- return FALSE;
452
- }
453
-
454
- return TRUE;
455
-}
456
-
457
-BOOL GetNextCharacter(_In_ INPUT_RECORD* InputRecord, _Out_ PWCHAR NextCharacter)
458
-{
459
- BOOL IsNextCharacterValid = FALSE;
460
- if (InputRecord->EventType == KEY_EVENT)
461
- {
462
- const auto KeyEvent = &InputRecord->Event.KeyEvent;
463
- if ((IsActionableKey(KeyEvent) != FALSE) && ((KeyEvent->bKeyDown != FALSE) || (KeyEvent->wVirtualKeyCode == TTY_ALT_NUMPAD_VK_MENU)))
464
- {
465
- *NextCharacter = KeyEvent->uChar.UnicodeChar;
466
- IsNextCharacterValid = TRUE;
467
- }
468
- }
469
-
470
- return IsNextCharacterValid;
471
-}
472
-
473
-bool wsl::windows::common::relay::StandardInputRelay(
474
- HANDLE ConsoleHandle, HANDLE OutputHandle, const std::function<void()>& UpdateTerminalSize, HANDLE ExitEvent, const std::vector<char>& DetachSequence)
410
+bool wsl::windows::common::relay::StandardInputRelay(HANDLE ConsoleHandle, HANDLE OutputHandle, std::function<void()>&& UpdateTerminalSize, HANDLE ExitEvent)
411
{
412
try
413
{
@@ -481,308 +417,18 @@ bool wsl::windows::common::relay::StandardInputRelay(
417
return true;
418
}
419
484
- //
485
- // N.B. ReadConsoleInputEx has no associated import library.
486
- //
487
-
488
- static LxssDynamicFunction<decltype(ReadConsoleInputExW)> readConsoleInput(L"Kernel32.dll", "ReadConsoleInputExW");
489
-
490
- INPUT_RECORD InputRecordBuffer[TTY_INPUT_EVENT_BUFFER_SIZE];
491
- INPUT_RECORD* InputRecordPeek = &(InputRecordBuffer[1]);
492
- KEY_EVENT_RECORD* KeyEvent;
493
- DWORD RecordsRead;
494
- OVERLAPPED Overlapped = {0};
495
- const wil::unique_event OverlappedEvent(wil::EventOptions::ManualReset);
496
- Overlapped.hEvent = OverlappedEvent.get();
497
- const HANDLE WaitHandles[] = {ExitEvent, ConsoleHandle};
498
- const std::vector<HANDLE> ExitHandles = {ExitEvent};
499
- std::deque<char> CurrentSequence;
500
-
501
- for (;;)
502
- {
503
- // Detach if the escape sequence was detected.
504
- // N.B. This needs to done at the beginning of the loop so the escape sequence is also sent to docker.
505
- if (!CurrentSequence.empty() && std::ranges::equal(CurrentSequence, DetachSequence))
506
- {
507
- return false;
508
- }
509
-
510
- //
511
- // Because some input events generated by the console are encoded with
512
- // more than one input event, we have to be smart about reading the
513
- // events.
514
- //
515
- // First, we peek at the next input event.
516
- // If it's an escape (wch == L'\x1b') event, then the characters that
517
- // follow are part of an input sequence. We can't know for sure
518
- // how long that sequence is, but we can assume it's all sent to
519
- // the input queue at once, and it's less that 16 events.
520
- // Furthermore, we can assume that if there's an Escape in those
521
- // 16 events, that the escape marks the start of a new sequence.
522
- // So, we'll peek at another 15 events looking for escapes.
523
- // If we see an escape, then we'll read one less than that,
524
- // such that the escape remains the next event in the input.
525
- // From those read events, we'll aggregate chars into a single
526
- // string to send to the subsystem.
527
- // If it's not an escape, send the event through one at a time.
528
- //
529
-
530
- //
531
- // Read one input event.
532
- //
533
-
534
- DWORD WaitStatus = (WAIT_OBJECT_0 + 1);
535
- do
536
- {
537
- THROW_IF_WIN32_BOOL_FALSE(readConsoleInput(ConsoleHandle, InputRecordBuffer, 1, &RecordsRead, CONSOLE_READ_NOWAIT));
538
-
539
- if (RecordsRead == 0)
540
- {
541
- WaitStatus = WaitForMultipleObjects(RTL_NUMBER_OF(WaitHandles), WaitHandles, false, INFINITE);
542
- }
543
- } while ((WaitStatus == (WAIT_OBJECT_0 + 1)) && (RecordsRead == 0));
544
-
545
- //
546
- // Stop processing if the exit event has been signaled.
547
- //
548
-
549
- if (WaitStatus != (WAIT_OBJECT_0 + 1))
550
- {
551
- WI_ASSERT(WaitStatus == WAIT_OBJECT_0);
552
-
553
- break;
554
- }
555
-
556
- WI_ASSERT(RecordsRead == 1);
557
-
558
- //
559
- // Don't read additional records if the first entry is a window size
560
- // event, or a repeated character. Handle those events on their own.
561
- //
562
-
563
- DWORD RecordsPeeked = 0;
564
- if ((InputRecordBuffer[0].EventType != WINDOW_BUFFER_SIZE_EVENT) &&
565
- ((InputRecordBuffer[0].EventType != KEY_EVENT) || (InputRecordBuffer[0].Event.KeyEvent.wRepeatCount < 2)))
566
- {
567
- //
568
- // Read additional input records into the buffer if available.
569
- //
570
-
571
- THROW_IF_WIN32_BOOL_FALSE(PeekConsoleInputW(ConsoleHandle, InputRecordPeek, (RTL_NUMBER_OF(InputRecordBuffer) - 1), &RecordsPeeked));
572
- }
573
-
574
- //
575
- // Iterate over peeked records [1, RecordsPeeked].
576
- //
577
-
578
- DWORD AdditionalRecordsToRead = 0;
579
- WCHAR NextCharacter;
580
- for (DWORD RecordIndex = 1; RecordIndex <= RecordsPeeked; RecordIndex++)
581
- {
582
- if (GetNextCharacter(&InputRecordBuffer[RecordIndex], &NextCharacter) != FALSE)
583
- {
584
- KeyEvent = &InputRecordBuffer[RecordIndex].Event.KeyEvent;
585
- if (NextCharacter == TTY_ESCAPE_CHARACTER)
586
- {
587
- //
588
- // CurrentRecord is an escape event. We will start here
589
- // on the next input loop.
590
- //
591
-
592
- break;
593
- }
594
- else if (KeyEvent->wRepeatCount > 1)
595
- {
596
- //
597
- // Repeated keys are handled on their own. Start with this
598
- // key on the next input loop.
599
- //
600
-
601
- break;
602
- }
603
- else if (IS_HIGH_SURROGATE(NextCharacter) && (RecordIndex >= (RecordsPeeked - 1)))
604
- {
605
- //
606
- // If there is not enough room for the second character of
607
- // a surrogate pair, start with this character on the next
608
- // input loop.
609
- //
610
- // N.B. The test is for at least two remaining records
611
- // because typically a surrogate pair will be entered
612
- // via copy/paste, which will appear as an input
613
- // record with alt-down, alt-up and character. So to
614
- // include the next character of the surrogate pair it
615
- // is likely that the alt-up record will need to be
616
- // read first.
617
- //
618
-
619
- break;
620
- }
621
- }
622
- else if (InputRecordBuffer[RecordIndex].EventType == WINDOW_BUFFER_SIZE_EVENT)
623
- {
624
- //
625
- // A window size event is handled on its own.
626
- //
420
+ MultiHandleWait io;
421
628
- break;
629
- }
630
-
631
- //
632
- // Process the additional input record.
633
- //
422
+ io.AddHandle(std::make_unique<io::RelayHandle<io::ReadConsoleHandle>>(ConsoleHandle, OutputHandle, std::move(UpdateTerminalSize)));
423
635
- AdditionalRecordsToRead += 1;
636
- }
424
+ io.AddHandle(std::make_unique<io::EventHandle>(ExitEvent), MultiHandleWait::CancelOnCompleted | MultiHandleWait::NeedNotComplete);
425
+ io.Run({});
426
638
- if (AdditionalRecordsToRead > 0)
639
- {
640
- THROW_IF_WIN32_BOOL_FALSE(
641
- readConsoleInput(ConsoleHandle, InputRecordPeek, AdditionalRecordsToRead, &RecordsRead, CONSOLE_READ_NOWAIT));
642
-
643
- if (RecordsRead == 0)
644
- {
645
- //
646
- // This would be an unexpected case. We've already peeked to see
647
- // that there are AdditionalRecordsToRead # of records in the
648
- // input that need reading, yet we didn't get them when we read.
649
- // In this case, move along and finish this input event.
650
- //
651
-
652
- break;
653
- }
654
-
655
- //
656
- // We already had one input record in the buffer before reading
657
- // additional, So account for that one too
658
- //
659
-
660
- RecordsRead += 1;
661
- }
662
-
663
- //
664
- // Process each input event. Keydowns will get aggregated into
665
- // Utf8String before getting injected into the subsystem.
666
- //
667
-
668
- WCHAR Utf16String[TTY_INPUT_EVENT_BUFFER_SIZE];
669
- ULONG Utf16StringSize = 0;
670
- COORD WindowSize{};
671
- for (DWORD RecordIndex = 0; RecordIndex < RecordsRead; RecordIndex++)
672
- {
673
- INPUT_RECORD* CurrentInputRecord = &(InputRecordBuffer[RecordIndex]);
674
- switch (CurrentInputRecord->EventType)
675
- {
676
- case KEY_EVENT:
677
-
678
- KeyEvent = &CurrentInputRecord->Event.KeyEvent;
679
-
680
- if (KeyEvent->bKeyDown && IsActionableKey(KeyEvent) && !DetachSequence.empty())
681
- {
682
- if (CurrentSequence.size() >= DetachSequence.size())
683
- {
684
- CurrentSequence.pop_front();
685
- }
686
-
687
- CurrentSequence.push_back(CurrentInputRecord->Event.KeyEvent.uChar.AsciiChar);
688
- }
689
-
690
- //
691
- // Filter out key up events unless they are from an <Alt> key.
692
- // Key up with an <Alt> key could contain a Unicode character
693
- // pasted from the clipboard and converted to an <Alt>+<Numpad> sequence.
694
- //
695
-
696
- if ((KeyEvent->bKeyDown == FALSE) && (KeyEvent->wVirtualKeyCode != TTY_ALT_NUMPAD_VK_MENU))
697
- {
698
- break;
699
- }
700
-
701
- //
702
- // Filter out key presses that are not actionable, such as just
703
- // pressing <Ctrl>, <Alt>, <Shift> etc. These key presses return
704
- // the character of null but will have a valid scan code off the
705
- // keyboard. Certain other key sequences such as Ctrl+A,
706
- // Ctrl+<space>, and Ctrl+@ will also return the character null
707
- // but have no scan code.
708
- // <Alt> + <NumPad> sequences will show an <Alt> but will have
709
- // a scancode and character specified, so they should be actionable.
710
- //
711
-
712
- if (IsActionableKey(KeyEvent) == FALSE)
713
- {
714
- break;
715
- }
716
-
717
- Utf16String[Utf16StringSize] = KeyEvent->uChar.UnicodeChar;
718
- Utf16StringSize += 1;
719
- break;
720
-
721
- case WINDOW_BUFFER_SIZE_EVENT:
722
-
723
- //
724
- // Query the window size and send an update message via the
725
- // control channel.
726
- //
727
-
728
- UpdateTerminalSize();
729
- break;
730
- }
731
- }
732
-
733
- CHAR Utf8String[TTY_UTF8_TRANSLATION_BUFFER_SIZE];
734
- DWORD Utf8StringSize = 0;
735
- if (Utf16StringSize > 0)
736
- {
737
- //
738
- // Windows uses UTF-16LE encoding, Linux uses UTF-8 by default.
739
- // Convert each UTF-16LE character into the proper UTF-8 byte
740
- // sequence equivalent.
741
- //
742
-
743
- THROW_LAST_ERROR_IF(
744
- (Utf8StringSize = WideCharToMultiByte(
745
- CP_UTF8, 0, Utf16String, Utf16StringSize, Utf8String, sizeof(Utf8String), nullptr, nullptr)) == 0);
746
- }
747
-
748
- //
749
- // Send the input bytes to the terminal.
750
- //
751
-
752
- DWORD BytesWritten = 0;
753
- const auto Utf8Span = gslhelpers::struct_as_bytes(Utf8String).first(Utf8StringSize);
754
- if ((RecordsRead == 1) && (InputRecordBuffer[0].EventType == KEY_EVENT) && (InputRecordBuffer[0].Event.KeyEvent.wRepeatCount > 1))
755
- {
756
- WI_ASSERT(Utf16StringSize == 1);
757
-
758
- //
759
- // Handle repeated characters. They aren't part of an input
760
- // sequence, so there's only one event that's generating characters.
761
- //
762
-
763
- WORD RepeatIndex;
764
- for (RepeatIndex = 0; RepeatIndex < InputRecordBuffer[0].Event.KeyEvent.wRepeatCount; RepeatIndex += 1)
765
- {
766
- BytesWritten = wsl::windows::common::relay::InterruptableWrite(OutputHandle, Utf8Span, ExitHandles, &Overlapped);
767
- if (BytesWritten == 0)
768
- {
769
- break;
770
- }
771
- }
772
- }
773
- else if (Utf8StringSize > 0)
774
- {
775
- BytesWritten = wsl::windows::common::relay::InterruptableWrite(OutputHandle, Utf8Span, ExitHandles, &Overlapped);
776
- if (BytesWritten == 0)
777
- {
778
- break;
779
- }
780
- }
781
- }
427
+ return true;
428
}
429
CATCH_LOG();
430
785
- return true;
431
+ return false;
432
}
433
434
void wsl::windows::common::relay::SocketRelay(_In_ SOCKET LeftSocket, _In_ SOCKET RightSocket, _In_ size_t BufferSize)
src/windows/common/relay.hpp
+1
-6
@@ -44,12 +44,7 @@ bool InterruptableWait(_In_ HANDLE WaitObject, _In_ const std::vector<HANDLE>& E
44
DWORD
45
InterruptableWrite(_In_ HANDLE OutputHandle, _In_ gsl::span<const gsl::byte> Buffer, _In_ const std::vector<HANDLE>& ExitHandles, _In_ LPOVERLAPPED Overlapped);
46
47
-bool StandardInputRelay(
48
- HANDLE ConsoleHandle,
49
- HANDLE OutputHandle,
50
- const std::function<void()>& UpdateTerminalSize,
51
- HANDLE ExitEvent,
52
- const std::vector<char>& DetachSequence = {});
47
+bool StandardInputRelay(HANDLE ConsoleHandle, HANDLE OutputHandle, std::function<void()>&& UpdateTerminalSize, HANDLE ExitEvent);
48
49
enum class RelayFlags
50
{
src/windows/wslc/services/ConsoleService.cpp
+71
-38
@@ -18,63 +18,85 @@ Abstract:
18
namespace wsl::windows::wslc::services {
19
20
using wsl::windows::common::ClientRunningWSLCProcess;
21
+using wsl::windows::common::io::MultiHandleWait;
22
+using wsl::windows::common::io::OverlappedIOHandle;
23
+using wsl::windows::common::io::ReadConsoleHandle;
24
using wsl::windows::common::io::ReadHandle;
25
using wsl::windows::common::io::RelayHandle;
26
24
-bool ConsoleService::RelayInteractiveTty(wsl::windows::common::ConsoleState& console, ClientRunningWSLCProcess& Process, HANDLE Tty, bool triggerRefresh)
27
+bool ConsoleService::RelayInteractiveTty(wsl::windows::common::ConsoleState& Console, ClientRunningWSLCProcess& Process, HANDLE Tty, bool TriggerRefresh)
28
{
29
// Configure the console for interactive usage.
27
- console.SetInteractiveMode();
30
+ Console.SetInteractiveMode();
31
29
- if (triggerRefresh)
32
+ if (TriggerRefresh)
33
{
34
// In the case of an Attach, force a terminal resize to force the tty to refresh its display.
35
// The docker client uses the same trick.
36
34
- auto size = console.GetWindowSize();
37
+ auto size = Console.GetWindowSize();
38
39
LOG_IF_FAILED(Process.Get().ResizeTty(size.Y + 1, size.X + 1));
40
LOG_IF_FAILED(Process.Get().ResizeTty(size.Y, size.X));
41
}
42
40
- wil::unique_event exitEvent(wil::EventOptions::ManualReset);
43
+ wil::unique_event exitEvent;
44
+ std::thread inputThread;
45
+
46
+ auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
47
+ if (inputThread.joinable())
48
+ {
49
+ exitEvent.SetEvent();
50
+ inputThread.join();
51
+ }
52
+ });
53
+
54
+ bool detached = false;
55
+ MultiHandleWait io;
56
42
- bool completed = false;
57
+ auto inputHandle = GetStdHandle(STD_INPUT_HANDLE);
58
44
- // Create a thread to relay stdin to the pipe.
45
- std::thread inputThread([&]() {
46
- auto updateTerminal = [&console, &Process]() {
47
- const auto windowSize = console.GetWindowSize();
59
+ if (GetFileType(inputHandle) == FILE_TYPE_CHAR)
60
+ {
61
+ auto updateTerminal = [&Console, &Process]() {
62
+ const auto windowSize = Console.GetWindowSize();
63
LOG_IF_FAILED(Process.Get().ResizeTty(windowSize.Y, windowSize.X));
64
};
65
66
// TODO: Make this configurable (default to ctrl-p, ctrl-q).
67
std::vector<char> detachSequence{0x10, 0x11};
68
54
- completed = wsl::windows::common::relay::StandardInputRelay(
55
- GetStdHandle(STD_INPUT_HANDLE), Tty, updateTerminal, exitEvent.get(), detachSequence);
56
- });
69
+ auto onDetach = [&detached]() { detached = true; };
70
58
- auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
59
- exitEvent.SetEvent();
60
- inputThread.join();
61
- });
71
+ io.AddHandle(
72
+ std::make_unique<RelayHandle<ReadConsoleHandle>>(inputHandle, Tty, std::move(updateTerminal), detachSequence, std::move(onDetach)),
73
+ MultiHandleWait::NeedNotComplete);
74
+ }
75
+ else
76
+ {
77
+ exitEvent.create(wil::EventOptions::ManualReset);
78
63
- // Relay the contents of the pipe to stdout.
64
- wsl::windows::common::relay::InterruptableRelay(Tty, GetStdHandle(STD_OUTPUT_HANDLE), exitEvent.get());
79
+ inputThread = std::thread{[&]() {
80
+ try
81
+ {
82
+ windows::common::relay::InterruptableRelay(inputHandle, Tty, exitEvent.get());
83
+ }
84
+ CATCH_LOG();
85
+ }};
86
+ }
87
66
- joinThread.reset();
88
+ io.AddHandle(std::make_unique<RelayHandle<ReadHandle>>(Tty, GetStdHandle(STD_OUTPUT_HANDLE)));
89
68
- return completed;
90
+ io.Run({});
91
+
92
+ return !detached;
93
}
94
95
void ConsoleService::RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_handle&& Stdout, wil::unique_handle&& Stderr)
96
{
73
- wsl::windows::common::io::MultiHandleWait io;
74
-
75
- // Create a thread to relay stdin to the pipe.
76
- wil::unique_event exitEvent(wil::EventOptions::ManualReset);
97
+ windows::common::io::MultiHandleWait io;
98
99
+ wil::unique_event exitEvent;
100
std::thread inputThread;
101
102
auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
@@ -87,19 +109,30 @@ void ConsoleService::RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_
109
110
if (Stdin.is_valid())
111
{
90
- // Required because ReadFile() blocks if stdin doesn't support overlapped IO.
91
- // This can create pipe deadlocks if we get blocked reading stdin while data is available on stdout / stderr.
92
- // TODO: Will output CR instead of LF's which can confuse the linux app.
93
- // Consider a custom relay logic to fix this.
94
- inputThread = std::thread{[&]() {
95
- try
96
- {
97
- wsl::windows::common::relay::InterruptableRelay(GetStdHandle(STD_INPUT_HANDLE), Stdin.get(), exitEvent.get());
98
- }
99
- CATCH_LOG();
112
+ auto input = GetStdHandle(STD_INPUT_HANDLE);
113
101
- Stdin.reset();
102
- }};
114
+ if (GetFileType(input) == FILE_TYPE_CHAR)
115
+ {
116
+ io.AddHandle(std::make_unique<RelayHandle<ReadConsoleHandle>>(input, std::move(Stdin)), MultiHandleWait::NeedNotComplete);
117
+ }
118
+ else
119
+ {
120
+ // Required because ReadFile() blocks if stdin doesn't support overlapped IO.
121
+ // This can create pipe deadlocks if we get blocked reading stdin while data is available on stdout / stderr.
122
+ // TODO: Will output CR instead of LF's which can confuse the linux app.
123
+ // Consider a custom relay logic to fix this.
124
+ exitEvent.create(wil::EventOptions::ManualReset);
125
+
126
+ inputThread = std::thread{[&]() {
127
+ try
128
+ {
129
+ windows::common::relay::InterruptableRelay(GetStdHandle(STD_INPUT_HANDLE), Stdin.get(), exitEvent.get());
130
+ }
131
+ CATCH_LOG();
132
+
133
+ Stdin.reset();
134
+ }};
135
+ }
136
}
137
138
io.AddHandle(std::make_unique<RelayHandle<ReadHandle>>(std::move(Stdout), GetStdHandle(STD_OUTPUT_HANDLE)));
@@ -114,7 +147,7 @@ int ConsoleService::AttachToCurrentConsole(wsl::windows::common::ConsoleState& c
147
{
148
if (!RelayInteractiveTty(console, process, process.GetStdHandle(WSLCFDTty).get(), triggerRefresh))
149
{
117
- wsl::windows::common::wslutil::PrintMessage(L"[detached]", stderr);
150
+ windows::common::wslutil::PrintMessage(L"[detached]", stderr);
151
return 0;
152
}
153
}