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 }