Skip to content

Commit 5c55144

Browse files
authored
Properly implement WaitForData for ReadConsoleInput (#18228)
There were two bugs: * Ever since the conhost v1 -> v2 rewrite the `readDataDirect.cpp` implementation incorrectly passed `false` as the wait flag. The unintentional mistake is obvious in hindsight as the check for `CONSOLE_STATUS_WAIT` makes no sense in this case. * The ConPTY integration into `InputBuffer` was done incorrectly, as it would unconditionally wake up the readers/waiters without checking if the buffer is now actually non-empty. Closes #15859 ## Validation Steps Performed Test code: ```cpp #include <Windows.h> #include <stdio.h> int main() { HANDLE in = GetStdHandle(STD_INPUT_HANDLE); INPUT_RECORD buf[128]; DWORD read; SetConsoleMode( in, ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT ); for (int i = 0; ReadConsoleInputW(in, buf, 128, &read); ++i) { printf("%d read=%lu\n", i, read); } return 0; } ``` Run it under Windows Terminal and type any input. >50% of all inputs will result in `read=0`. This is fixed after this PR.
1 parent 32eeefd commit 5c55144

File tree

8 files changed

+51
-150
lines changed

8 files changed

+51
-150
lines changed

src/host/ApiRoutines.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class ApiRoutines : public IApiRoutines
5555
INPUT_READ_HANDLE_DATA& readHandleState,
5656
const bool IsUnicode,
5757
const bool IsPeek,
58+
const bool IsWaitAllowed,
5859
std::unique_ptr<IWaitRoutine>& waiter) noexcept override;
5960

6061
[[nodiscard]] HRESULT ReadConsoleImpl(IConsoleInputObject& context,

src/host/directio.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ using Microsoft::Console::Interactivity::ServiceLocator;
4545
// - ppWaiter - If we have to wait (not enough data to fill client
4646
// buffer), this contains context that will allow the server to
4747
// restore this call later.
48+
// - IsWaitAllowed - Whether an async read via CONSOLE_STATUS_WAIT is permitted.
4849
// Return Value:
4950
// - STATUS_SUCCESS - If data was found and ready for return to the client.
5051
// - CONSOLE_STATUS_WAIT - If we didn't have enough data or needed to
@@ -56,6 +57,7 @@ using Microsoft::Console::Interactivity::ServiceLocator;
5657
INPUT_READ_HANDLE_DATA& readHandleState,
5758
const bool IsUnicode,
5859
const bool IsPeek,
60+
const bool IsWaitAllowed,
5961
std::unique_ptr<IWaitRoutine>& waiter) noexcept
6062
{
6163
try
@@ -73,7 +75,7 @@ using Microsoft::Console::Interactivity::ServiceLocator;
7375
const auto Status = inputBuffer.Read(outEvents,
7476
eventReadCount,
7577
IsPeek,
76-
true,
78+
IsWaitAllowed,
7779
IsUnicode,
7880
false);
7981

src/host/inputBuffer.cpp

Lines changed: 29 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -492,8 +492,7 @@ size_t InputBuffer::Prepend(const std::span<const INPUT_RECORD>& inEvents)
492492
return STATUS_SUCCESS;
493493
}
494494

495-
_vtInputShouldSuppress = true;
496-
auto resetVtInputSuppress = wil::scope_exit([&]() { _vtInputShouldSuppress = false; });
495+
const auto wakeup = _wakeupReadersOnExit();
497496

498497
// read all of the records out of the buffer, then write the
499498
// prepend ones, then write the original set. We need to do it
@@ -503,35 +502,15 @@ size_t InputBuffer::Prepend(const std::span<const INPUT_RECORD>& inEvents)
503502
std::deque<INPUT_RECORD> existingStorage;
504503
existingStorage.swap(_storage);
505504

506-
// We will need this variable to pass to _WriteBuffer so it can attempt to determine wait status.
507-
// However, because we swapped the storage out from under it with an empty deque, it will always
508-
// return true after the first one (as it is filling the newly emptied backing deque.)
509-
// Then after the second one, because we've inserted some input, it will always say false.
510-
auto unusedWaitStatus = false;
511-
512505
// write the prepend records
513506
size_t prependEventsWritten;
514-
_WriteBuffer(inEvents, prependEventsWritten, unusedWaitStatus);
515-
FAIL_FAST_IF(!(unusedWaitStatus));
507+
_WriteBuffer(inEvents, prependEventsWritten);
516508

517509
for (const auto& event : existingStorage)
518510
{
519511
_storage.push_back(event);
520512
}
521513

522-
// We need to set the wait event if there were 0 events in the
523-
// input queue when we started.
524-
// Because we did interesting manipulation of the wait queue
525-
// in order to prepend, we can't trust what _WriteBuffer said
526-
// and instead need to set the event if the original backing
527-
// buffer (the one we swapped out at the top) was empty
528-
// when this whole thing started.
529-
if (existingStorage.empty())
530-
{
531-
ServiceLocator::LocateGlobals().hInputEvent.SetEvent();
532-
}
533-
WakeUpReadersWaitingForData();
534-
535514
return prependEventsWritten;
536515
}
537516
catch (...)
@@ -575,21 +554,9 @@ size_t InputBuffer::Write(const std::span<const INPUT_RECORD>& inEvents)
575554
return 0;
576555
}
577556

578-
_vtInputShouldSuppress = true;
579-
auto resetVtInputSuppress = wil::scope_exit([&]() { _vtInputShouldSuppress = false; });
580-
581-
// Write to buffer.
557+
const auto wakeup = _wakeupReadersOnExit();
582558
size_t EventsWritten;
583-
bool SetWaitEvent;
584-
_WriteBuffer(inEvents, EventsWritten, SetWaitEvent);
585-
586-
if (SetWaitEvent)
587-
{
588-
ServiceLocator::LocateGlobals().hInputEvent.SetEvent();
589-
}
590-
591-
// Alert any writers waiting for space.
592-
WakeUpReadersWaitingForData();
559+
_WriteBuffer(inEvents, EventsWritten);
593560
return EventsWritten;
594561
}
595562
catch (...)
@@ -607,16 +574,8 @@ try
607574
return;
608575
}
609576

610-
const auto initiallyEmptyQueue = _storage.empty();
611-
577+
const auto wakeup = _wakeupReadersOnExit();
612578
_writeString(text);
613-
614-
if (initiallyEmptyQueue && !_storage.empty())
615-
{
616-
ServiceLocator::LocateGlobals().hInputEvent.SetEvent();
617-
}
618-
619-
WakeUpReadersWaitingForData();
620579
}
621580
CATCH_LOG()
622581

@@ -625,29 +584,26 @@ CATCH_LOG()
625584
// input buffer and the next application will suddenly get a "\x1b[I" sequence in their input. See GH#13238.
626585
void InputBuffer::WriteFocusEvent(bool focused) noexcept
627586
{
587+
const auto wakeup = _wakeupReadersOnExit();
588+
628589
if (IsInVirtualTerminalInputMode())
629590
{
630591
if (const auto out = _termInput.HandleFocus(focused))
631592
{
632-
_HandleTerminalInputCallback(*out);
593+
_writeString(*out);
633594
}
634595
}
635596
else
636597
{
637-
// This is a mini-version of Write().
638-
const auto wasEmpty = _storage.empty();
639598
_storage.push_back(SynthesizeFocusEvent(focused));
640-
if (wasEmpty)
641-
{
642-
ServiceLocator::LocateGlobals().hInputEvent.SetEvent();
643-
}
644-
WakeUpReadersWaitingForData();
645599
}
646600
}
647601

648602
// Returns true when mouse input started. You should then capture the mouse and produce further events.
649603
bool InputBuffer::WriteMouseEvent(til::point position, const unsigned int button, const short keyState, const short wheelDelta)
650604
{
605+
const auto wakeup = _wakeupReadersOnExit();
606+
651607
if (IsInVirtualTerminalInputMode())
652608
{
653609
// This magic flag is "documented" at https://msdn.microsoft.com/en-us/library/windows/desktop/ms646301(v=vs.85).aspx
@@ -669,7 +625,7 @@ bool InputBuffer::WriteMouseEvent(til::point position, const unsigned int button
669625

670626
if (const auto out = _termInput.HandleMouse(position, button, keyState, wheelDelta, state))
671627
{
672-
_HandleTerminalInputCallback(*out);
628+
_writeString(*out);
673629
return true;
674630
}
675631
}
@@ -690,25 +646,38 @@ static bool IsPauseKey(const KEY_EVENT_RECORD& event)
690646
return ctrlButNotAlt && event.wVirtualKeyCode == L'S';
691647
}
692648

649+
void InputBuffer::_wakeupReadersImpl(bool initiallyEmpty)
650+
{
651+
if (!_storage.empty())
652+
{
653+
// It would be fine to call SetEvent() unconditionally,
654+
// but technically we only need to ResetEvent() if the buffer is empty,
655+
// and SetEvent() once it stopped being so, which is what this code does.
656+
if (initiallyEmpty)
657+
{
658+
ServiceLocator::LocateGlobals().hInputEvent.SetEvent();
659+
}
660+
661+
WakeUpReadersWaitingForData();
662+
}
663+
}
664+
693665
// Routine Description:
694666
// - Coalesces input events and transfers them to storage queue.
695667
// Arguments:
696668
// - inRecords - The events to store.
697669
// - eventsWritten - The number of events written since this function
698670
// was called.
699-
// - setWaitEvent - on exit, true if buffer became non-empty.
700671
// Return Value:
701672
// - None
702673
// Note:
703674
// - The console lock must be held when calling this routine.
704675
// - will throw on failure
705-
void InputBuffer::_WriteBuffer(const std::span<const INPUT_RECORD>& inEvents, _Out_ size_t& eventsWritten, _Out_ bool& setWaitEvent)
676+
void InputBuffer::_WriteBuffer(const std::span<const INPUT_RECORD>& inEvents, _Out_ size_t& eventsWritten)
706677
{
707678
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
708679

709680
eventsWritten = 0;
710-
setWaitEvent = false;
711-
const auto initiallyEmptyQueue = _storage.empty();
712681
const auto initialInEventsSize = inEvents.size();
713682
const auto vtInputMode = IsInVirtualTerminalInputMode();
714683

@@ -739,7 +708,7 @@ void InputBuffer::_WriteBuffer(const std::span<const INPUT_RECORD>& inEvents, _O
739708
// GH#11682: TerminalInput::HandleKey can handle both KeyEvents and Focus events seamlessly
740709
if (const auto out = _termInput.HandleKey(inEvent))
741710
{
742-
_HandleTerminalInputCallback(*out);
711+
_writeString(*out);
743712
eventsWritten++;
744713
continue;
745714
}
@@ -759,10 +728,6 @@ void InputBuffer::_WriteBuffer(const std::span<const INPUT_RECORD>& inEvents, _O
759728
_storage.push_back(inEvent);
760729
++eventsWritten;
761730
}
762-
if (initiallyEmptyQueue && !_storage.empty())
763-
{
764-
setWaitEvent = true;
765-
}
766731
}
767732

768733
// Routine Description::
@@ -826,36 +791,6 @@ bool InputBuffer::IsInVirtualTerminalInputMode() const
826791
return WI_IsFlagSet(InputMode, ENABLE_VIRTUAL_TERMINAL_INPUT);
827792
}
828793

829-
// Routine Description:
830-
// - Handler for inserting key sequences into the buffer when the terminal emulation layer
831-
// has determined a key can be converted appropriately into a sequence of inputs
832-
// Arguments:
833-
// - inEvents - Series of input records to insert into the buffer
834-
// Return Value:
835-
// - <none>
836-
void InputBuffer::_HandleTerminalInputCallback(const TerminalInput::StringType& text)
837-
{
838-
try
839-
{
840-
if (text.empty())
841-
{
842-
return;
843-
}
844-
845-
_writeString(text);
846-
847-
if (!_vtInputShouldSuppress)
848-
{
849-
ServiceLocator::LocateGlobals().hInputEvent.SetEvent();
850-
WakeUpReadersWaitingForData();
851-
}
852-
}
853-
catch (...)
854-
{
855-
LOG_HR(wil::ResultFromCaughtException());
856-
}
857-
}
858-
859794
void InputBuffer::_writeString(const std::wstring_view& text)
860795
{
861796
for (const auto& wch : text)

src/host/inputBuffer.hpp

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,20 @@ class InputBuffer final : public ConsoleObjectHeader
8484
bool _writePartialByteSequenceAvailable = false;
8585
Microsoft::Console::VirtualTerminal::TerminalInput _termInput;
8686

87-
// This flag is used in _HandleTerminalInputCallback
88-
// If the InputBuffer leads to a _HandleTerminalInputCallback call,
89-
// we should suppress the wakeup functions.
90-
// Otherwise, we should be calling them.
91-
bool _vtInputShouldSuppress{ false };
87+
// Wakes up readers waiting for data to be in the input buffer.
88+
auto _wakeupReadersOnExit() noexcept
89+
{
90+
const auto initiallyEmpty = _storage.empty();
91+
return wil::scope_exit([this, initiallyEmpty]() {
92+
_wakeupReadersImpl(initiallyEmpty);
93+
});
94+
}
9295

96+
void _wakeupReadersImpl(bool initiallyEmpty);
9397
void _switchReadingMode(ReadingMode mode);
9498
void _switchReadingModeSlowPath(ReadingMode mode);
95-
void _WriteBuffer(const std::span<const INPUT_RECORD>& inRecords, _Out_ size_t& eventsWritten, _Out_ bool& setWaitEvent);
99+
void _WriteBuffer(const std::span<const INPUT_RECORD>& inRecords, _Out_ size_t& eventsWritten);
96100
bool _CoalesceEvent(const INPUT_RECORD& inEvent) noexcept;
97-
void _HandleTerminalInputCallback(const Microsoft::Console::VirtualTerminal::TerminalInput::StringType& text);
98101
void _writeString(const std::wstring_view& text);
99102

100103
#ifdef UNIT_TESTING

src/host/readDataDirect.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ try
107107
*pReplyStatus = _pInputBuffer->Read(_outEvents,
108108
amountToRead,
109109
false,
110-
false,
110+
true,
111111
fIsUnicode,
112112
false);
113113

src/host/ut_host/InputBufferTests.cpp

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -575,31 +575,6 @@ class InputBufferTests
575575
false));
576576
}
577577

578-
TEST_METHOD(WritingToEmptyBufferSignalsWaitEvent)
579-
{
580-
InputBuffer inputBuffer;
581-
auto record = MakeKeyEvent(true, 1, L'a', 0, L'a', 0);
582-
auto inputEvent = record;
583-
size_t eventsWritten;
584-
auto waitEvent = false;
585-
inputBuffer.Flush();
586-
// write one event to an empty buffer
587-
InputEventQueue storage;
588-
storage.push_back(std::move(inputEvent));
589-
inputBuffer._WriteBuffer(storage, eventsWritten, waitEvent);
590-
VERIFY_IS_TRUE(waitEvent);
591-
// write another, it shouldn't signal this time
592-
auto record2 = MakeKeyEvent(true, 1, L'b', 0, L'b', 0);
593-
auto inputEvent2 = record2;
594-
// write another event to a non-empty buffer
595-
waitEvent = false;
596-
storage.clear();
597-
storage.push_back(std::move(inputEvent2));
598-
inputBuffer._WriteBuffer(storage, eventsWritten, waitEvent);
599-
600-
VERIFY_IS_FALSE(waitEvent);
601-
}
602-
603578
TEST_METHOD(StreamReadingDeCoalesces)
604579
{
605580
InputBuffer inputBuffer;

src/server/ApiDispatchers.cpp

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ constexpr T saturate(auto val)
182182
*pInputReadHandleData,
183183
a->Unicode,
184184
fIsPeek,
185+
fIsWaitAllowed,
185186
waiter);
186187

187188
// We must return the number of records in the message payload (to alert the client)
@@ -191,30 +192,13 @@ constexpr T saturate(auto val)
191192
size_t cbWritten;
192193
LOG_IF_FAILED(SizeTMult(outEvents.size(), sizeof(INPUT_RECORD), &cbWritten));
193194

194-
if (nullptr != waiter.get())
195+
if (waiter)
195196
{
196-
// In some circumstances, the read may have told us to wait because it didn't have data,
197-
// but the client explicitly asked us to return immediate. In that case, we'll convert the
198-
// wait request into a "0 bytes found, OK".
199-
200-
if (fIsWaitAllowed)
201-
{
202-
hr = ConsoleWaitQueue::s_CreateWait(m, waiter.release());
203-
if (SUCCEEDED(hr))
204-
{
205-
*pbReplyPending = TRUE;
206-
hr = CONSOLE_STATUS_WAIT;
207-
}
208-
}
209-
else
197+
hr = ConsoleWaitQueue::s_CreateWait(m, waiter.release());
198+
if (SUCCEEDED(hr))
210199
{
211-
// If wait isn't allowed and the routine generated a
212-
// waiter, say there was nothing to be
213-
// retrieved right now.
214-
// The waiter will be auto-freed in the smart pointer.
215-
216-
cbWritten = 0;
217-
hr = S_OK;
200+
*pbReplyPending = TRUE;
201+
hr = CONSOLE_STATUS_WAIT;
218202
}
219203
}
220204
else

src/server/IApiRoutines.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class __declspec(novtable) IApiRoutines
6363
INPUT_READ_HANDLE_DATA& readHandleState,
6464
const bool IsUnicode,
6565
const bool IsPeek,
66+
const bool IsWaitAllowed,
6667
std::unique_ptr<IWaitRoutine>& waiter) noexcept = 0;
6768

6869
[[nodiscard]] virtual HRESULT ReadConsoleImpl(IConsoleInputObject& context,

0 commit comments

Comments
 (0)