Skip to content

Commit 1040035

Browse files
authored
Reduce likelihood of races between stdout and cooked stdin reads (#18326)
As explained in the comment on `_getViewportCursorPosition`, printing to stdout after initiating a cooked stdin reads is a race condition between the application and the terminal. But we can significantly reduce the likelihood of this being obvious with this change. Related to #18265 Possibly related to #18081 ## Validation Steps Performed Execute the following Go code and start typing: ```go package main import ( "fmt" "time" ) func main() { go func() { time.Sleep(50 * time.Millisecond) fmt.Printf("Here is a prompt! >") }() var text string fmt.Scanln(&text) } ``` Without this change the prompt will disappear, and with this change in place, it'll work as expected. ✅
1 parent 08f9afe commit 1040035

File tree

3 files changed

+42
-18
lines changed

3 files changed

+42
-18
lines changed

src/host/readDataCooked.cpp

+38-15
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,6 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer,
6161
THROW_IF_FAILED(_screenInfo.GetMainBuffer().AllocateIoHandle(ConsoleHandleData::HandleType::Output, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, _tempHandle));
6262
#endif
6363

64-
const auto cursorPos = _getViewportCursorPosition();
65-
_originInViewport = cursorPos;
66-
6764
if (!initialData.empty())
6865
{
6966
// The console API around `nInitialChars` in `CONSOLE_READCONSOLE_CONTROL` is pretty weird.
@@ -94,6 +91,7 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer,
9491
// It replicates part of the _redisplay() logic to layout the text at various
9592
// starting positions until it finds one that matches the current cursor position.
9693

94+
const auto cursorPos = _getViewportCursorPosition();
9795
const auto size = _screenInfo.GetVtPageArea().Dimensions();
9896

9997
// Guess the initial cursor position based on the string length, assuming that 1 char = 1 column.
@@ -285,22 +283,29 @@ bool COOKED_READ_DATA::Read(const bool isUnicode, size_t& numBytes, ULONG& contr
285283

286284
// Printing wide glyphs at the end of a row results in a forced line wrap and a padding whitespace to be inserted.
287285
// When the text buffer resizes these padding spaces may vanish and the _distanceCursor and _distanceEnd measurements become inaccurate.
288-
// To fix this, this function is called before a resize and will clear the input line. Afterwards, RedrawAfterResize() will restore it.
286+
// To fix this, this function is called before a resize and will clear the input line. Afterward, RedrawAfterResize() will restore it.
289287
void COOKED_READ_DATA::EraseBeforeResize()
290288
{
289+
// If we've already erased the buffer, we don't need to do it again.
291290
if (_redrawPending)
292291
{
293292
return;
294293
}
295294

295+
// If we don't have an origin, we've never had user input, and consequently there's nothing to erase.
296+
if (!_originInViewport)
297+
{
298+
return;
299+
}
300+
296301
_redrawPending = true;
297302

298303
// Position the cursor the start of the prompt before reflow.
299304
// Then, after reflow, we'll be able to ask the buffer where it went (the new origin).
300305
// This uses the buffer APIs directly, so that we don't emit unnecessary VT into ConPTY's output.
301306
auto& textBuffer = _screenInfo.GetTextBuffer();
302307
auto& cursor = textBuffer.GetCursor();
303-
auto cursorPos = _originInViewport;
308+
auto cursorPos = *_originInViewport;
304309
_screenInfo.GetVtPageArea().ConvertFromOrigin(&cursorPos);
305310
cursor.SetPosition(cursorPos);
306311
}
@@ -316,7 +321,10 @@ void COOKED_READ_DATA::RedrawAfterResize()
316321
_redrawPending = false;
317322

318323
// Get the new cursor position after the reflow, since it may have changed.
319-
_originInViewport = _getViewportCursorPosition();
324+
if (_originInViewport)
325+
{
326+
_originInViewport = _getViewportCursorPosition();
327+
}
320328

321329
// Ensure that we don't use any scroll sequences or try to clear previous pager contents.
322330
// They have all been erased with the CSI J above.
@@ -348,7 +356,7 @@ bool COOKED_READ_DATA::PresentingPopup() const noexcept
348356
return !_popups.empty();
349357
}
350358

351-
til::point_span COOKED_READ_DATA::GetBoundaries() const noexcept
359+
til::point_span COOKED_READ_DATA::GetBoundaries() noexcept
352360
{
353361
const auto viewport = _screenInfo.GetViewport();
354362
const auto virtualViewport = _screenInfo.GetVtPageArea();
@@ -357,7 +365,7 @@ til::point_span COOKED_READ_DATA::GetBoundaries() const noexcept
357365
const til::point max{ viewport.RightInclusive(), viewport.BottomInclusive() };
358366

359367
// Convert from VT-viewport-relative coordinate space back to the console one.
360-
auto beg = _originInViewport;
368+
auto beg = _getOriginInViewport();
361369
virtualViewport.ConvertFromOrigin(&beg);
362370

363371
// Since the pager may be longer than the viewport is tall, we need to clamp the coordinates to still remain within
@@ -831,6 +839,20 @@ til::point COOKED_READ_DATA::_getViewportCursorPosition() const noexcept
831839
return cursorPos;
832840
}
833841

842+
// Some applications initiate a read on stdin and _then_ print the prompt prefix to stdout.
843+
// While that's not correct (because it's a race condition), we can make it significantly
844+
// less bad by delaying the calculation of the origin until we actually need it.
845+
// This turns it from a race between application and terminal into a race between
846+
// application and user, which is much less likely to hit.
847+
til::point COOKED_READ_DATA::_getOriginInViewport() noexcept
848+
{
849+
if (!_originInViewport)
850+
{
851+
_originInViewport.emplace(_getViewportCursorPosition());
852+
}
853+
return *_originInViewport;
854+
}
855+
834856
void COOKED_READ_DATA::_replace(size_t offset, size_t remove, const wchar_t* input, size_t count)
835857
{
836858
const auto size = _buffer.size();
@@ -885,7 +907,8 @@ void COOKED_READ_DATA::_redisplay()
885907
}
886908

887909
const auto size = _screenInfo.GetVtPageArea().Dimensions();
888-
auto originInViewportFinal = _originInViewport;
910+
auto originInViewport = _getOriginInViewport();
911+
auto originInViewportFinal = originInViewport;
889912
til::point cursorPositionFinal;
890913
til::point pagerPromptEnd;
891914
std::vector<Line> lines;
@@ -894,7 +917,7 @@ void COOKED_READ_DATA::_redisplay()
894917
// and if MSVC says that then that must be true.
895918
for (;;)
896919
{
897-
cursorPositionFinal = { _originInViewport.x, 0 };
920+
cursorPositionFinal = { originInViewport.x, 0 };
898921

899922
// Construct the first line manually so that it starts at the correct horizontal position.
900923
LayoutResult res{ .column = cursorPositionFinal.x };
@@ -1057,8 +1080,8 @@ void COOKED_READ_DATA::_redisplay()
10571080
if (gsl::narrow_cast<til::CoordType>(lines.size()) > size.height && originInViewportFinal.x != 0)
10581081
{
10591082
lines.clear();
1060-
_originInViewport.x = 0;
10611083
_bufferDirtyBeg = 0;
1084+
originInViewport.x = 0;
10621085
originInViewportFinal = {};
10631086
continue;
10641087
}
@@ -1092,7 +1115,7 @@ void COOKED_READ_DATA::_redisplay()
10921115
if (_clearPending)
10931116
{
10941117
_clearPending = false;
1095-
_appendCUP(output, _originInViewport);
1118+
_appendCUP(output, originInViewport);
10961119
output.append(L"\x1b[J");
10971120
}
10981121

@@ -1111,7 +1134,7 @@ void COOKED_READ_DATA::_redisplay()
11111134
// The check for origin == {0,0} is important because it ensures that we "own" the entire viewport and
11121135
// that scrolling our contents doesn't scroll away the user's output that may still be in the viewport.
11131136
// (Anything below the origin is assumed to belong to us.)
1114-
if (const auto delta = pagerContentTop - _pagerContentTop; delta != 0 && _originInViewport == til::point{})
1137+
if (const auto delta = pagerContentTop - _pagerContentTop; delta != 0 && originInViewport == til::point{})
11151138
{
11161139
const auto deltaAbs = abs(delta);
11171140
til::CoordType beg = 0;
@@ -1179,7 +1202,7 @@ void COOKED_READ_DATA::_redisplay()
11791202

11801203
for (til::CoordType i = 0; i < pagerHeight; i++)
11811204
{
1182-
const auto row = std::min(_originInViewport.y + i, size.height - 1);
1205+
const auto row = std::min(originInViewport.y + i, size.height - 1);
11831206

11841207
// If the last write left the cursor at the end of a line, the next write will start at the beginning of the next line.
11851208
// This avoids needless calls to _appendCUP. The reason it's here and not at the end of the loop is similar to how
@@ -1223,7 +1246,7 @@ void COOKED_READ_DATA::_redisplay()
12231246

12241247
if (pagerHeight < pagerHeightPrevious)
12251248
{
1226-
const auto row = std::min(_originInViewport.y + pagerHeight, size.height - 1);
1249+
const auto row = std::min(originInViewport.y + pagerHeight, size.height - 1);
12271250
_appendCUP(output, { 0, row });
12281251
output.append(L"\x1b[K");
12291252

src/host/readDataCooked.hpp

+3-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class COOKED_READ_DATA final : public ReadData
3636
void SetInsertMode(bool insertMode) noexcept;
3737
bool IsEmpty() const noexcept;
3838
bool PresentingPopup() const noexcept;
39-
til::point_span GetBoundaries() const noexcept;
39+
til::point_span GetBoundaries() noexcept;
4040

4141
private:
4242
static constexpr size_t CommandNumberMaxInputLength = 5;
@@ -129,6 +129,7 @@ class COOKED_READ_DATA final : public ReadData
129129
void _handlePostCharInputLoop(bool isUnicode, size_t& numBytes, ULONG& controlKeyState);
130130
void _transitionState(State state) noexcept;
131131
til::point _getViewportCursorPosition() const noexcept;
132+
til::point _getOriginInViewport() noexcept;
132133
void _replace(size_t offset, size_t remove, const wchar_t* input, size_t count);
133134
void _replace(const std::wstring_view& str);
134135
std::wstring_view _slice(size_t from, size_t to) const noexcept;
@@ -166,7 +167,7 @@ class COOKED_READ_DATA final : public ReadData
166167
bool _redrawPending = false;
167168
bool _clearPending = false;
168169

169-
til::point _originInViewport;
170+
std::optional<til::point> _originInViewport;
170171
// This value is in the pager coordinate space. (0,0) is the first character of the
171172
// first line, independent on where the prompt actually appears on the screen.
172173
// The coordinate is "end exclusive", so the last character is 1 in front of it.

src/host/selectionInput.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -926,7 +926,7 @@ bool Selection::_HandleMarkModeSelectionNav(const INPUT_KEY_INFO* const pInputKe
926926
// - If true, the boundaries returned are valid. If false, they should be discarded.
927927
[[nodiscard]] bool Selection::s_GetInputLineBoundaries(_Out_opt_ til::point* const pcoordInputStart, _Out_opt_ til::point* const pcoordInputEnd)
928928
{
929-
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
929+
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
930930

931931
if (gci.HasPendingCookedRead())
932932
{

0 commit comments

Comments
 (0)