Skip to content

Commit 898b9e3

Browse files
authored
Fetch the terminal cursor position after a resize (#19535)
Closes #18725 ## Validation Steps Performed Functionality was observed under a debugger while using PowerShell 5.
1 parent fb668f3 commit 898b9e3

File tree

12 files changed

+147
-26
lines changed

12 files changed

+147
-26
lines changed

.github/actions/spelling/expect/expect.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ DSBUFFERDESC
495495
DSBVOLUME
496496
dsm
497497
dsound
498+
DSRCPR
498499
DSSCL
499500
DSwap
500501
DTo

src/host/VtInputThread.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ using namespace Microsoft::Console::VirtualTerminal;
2121
// - hPipe - a handle to the file representing the read end of the VT pipe.
2222
// - inheritCursor - a bool indicating if the state machine should expect a
2323
// cursor positioning sequence. See MSFT:15681311.
24-
VtInputThread::VtInputThread(_In_ wil::unique_hfile hPipe, const bool inheritCursor) :
24+
VtInputThread::VtInputThread(_In_ wil::unique_hfile hPipe) :
2525
_hFile{ std::move(hPipe) }
2626
{
2727
THROW_HR_IF(E_HANDLE, _hFile.get() == INVALID_HANDLE_VALUE);
2828

2929
auto dispatch = std::make_unique<InteractDispatch>();
30-
auto engine = std::make_unique<InputStateMachineEngine>(std::move(dispatch), inheritCursor);
30+
auto engine = std::make_unique<InputStateMachineEngine>(std::move(dispatch));
3131
_pInputStateMachine = std::make_unique<StateMachine>(std::move(engine));
3232
}
3333

@@ -185,8 +185,14 @@ void VtInputThread::_InputThread()
185185
return S_OK;
186186
}
187187

188+
void VtInputThread::CaptureNextCursorPositionReport() const noexcept
189+
{
190+
auto& engine = static_cast<InputStateMachineEngine&>(_pInputStateMachine->Engine());
191+
engine.CaptureNextCursorPositionReport();
192+
}
193+
188194
til::enumset<DeviceAttribute, uint64_t> VtInputThread::WaitUntilDA1(DWORD timeout) const noexcept
189195
{
190-
const auto& engine = static_cast<InputStateMachineEngine&>(_pInputStateMachine->Engine());
196+
auto& engine = static_cast<InputStateMachineEngine&>(_pInputStateMachine->Engine());
191197
return engine.WaitUntilDA1(timeout);
192198
}

src/host/VtInputThread.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ namespace Microsoft::Console
2626
class VtInputThread
2727
{
2828
public:
29-
VtInputThread(_In_ wil::unique_hfile hPipe, const bool inheritCursor);
29+
explicit VtInputThread(_In_ wil::unique_hfile hPipe);
3030

3131
[[nodiscard]] HRESULT Start();
32+
void CaptureNextCursorPositionReport() const noexcept;
3233
til::enumset<VirtualTerminal::DeviceAttribute, uint64_t> WaitUntilDA1(DWORD timeout) const noexcept;
3334

3435
private:

src/host/VtIo.cpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ bool VtIo::IsUsingVt() const
155155
{
156156
if (IsValidHandle(_hInput.get()))
157157
{
158-
_pVtInputThread = std::make_unique<VtInputThread>(std::move(_hInput), _lookingForCursorPosition);
158+
_pVtInputThread = std::make_unique<VtInputThread>(std::move(_hInput));
159159
}
160160
}
161161
CATCH_RETURN();
@@ -177,7 +177,7 @@ bool VtIo::IsUsingVt() const
177177
// wait for the DA1 response below and effectively wait for both.
178178
if (_lookingForCursorPosition)
179179
{
180-
writer.WriteUTF8("\x1b[6n"); // Cursor Position Report (DSR CPR)
180+
writer.WriteDSRCPR();
181181
}
182182

183183
// GH#4999 - Send a sequence to the connected terminal to request
@@ -720,6 +720,19 @@ void VtIo::Writer::WriteASB(bool enabled) const
720720
_io->_back.append(&buf[0], std::size(buf) - 1);
721721
}
722722

723+
// DSR CPR: Cursor Position Report
724+
bool VtIo::Writer::WriteDSRCPR() const
725+
{
726+
if (!_io->_pVtInputThread)
727+
{
728+
return false;
729+
}
730+
731+
_io->_back.append("\x1b[6n");
732+
_io->_pVtInputThread->CaptureNextCursorPositionReport();
733+
return true;
734+
}
735+
723736
void VtIo::Writer::WriteWindowVisibility(bool visible) const
724737
{
725738
char buf[] = "\x1b[1t";

src/host/VtIo.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ namespace Microsoft::Console::VirtualTerminal
4040
void WriteSGR1006(bool enabled) const;
4141
void WriteDECAWM(bool enabled) const;
4242
void WriteASB(bool enabled) const;
43+
bool WriteDSRCPR() const;
4344
void WriteWindowVisibility(bool visible) const;
4445
void WriteWindowTitle(std::wstring_view title) const;
4546
void WriteAttributes(const TextAttribute& attributes) const;

src/host/screenInfo.cpp

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,6 +1404,8 @@ NT_CATCH_RETURN()
14041404

14051405
if (SUCCEEDED_NTSTATUS(status))
14061406
{
1407+
SetConptyCursorPositionMayBeWrong();
1408+
14071409
// Fire off an event to let accessibility apps know the layout has changed.
14081410
if (IsActiveScreenBuffer())
14091411
{
@@ -1422,6 +1424,86 @@ NT_CATCH_RETURN()
14221424
return status;
14231425
}
14241426

1427+
// If we're ConPTY, our copy of the buffer may be out of sync with the terminal,
1428+
// because our VT, resize reflow, etc., implementation may be different.
1429+
// For some operations we set a flag to indicate the cursor position may be wrong.
1430+
// For GetConsoleScreenBufferInfo(Ex) we then fetch the latest position from the terminal.
1431+
// This fixes some of the most glaring out of sync issues. See GH#18725.
1432+
bool SCREEN_INFORMATION::ConptyCursorPositionMayBeWrong() const noexcept
1433+
{
1434+
return _conptyCursorPositionMayBeWrong.load(std::memory_order_relaxed);
1435+
}
1436+
1437+
// This should be called whenever we do something that may desynchronize
1438+
// our cursor position from the terminal's, e.g. a buffer resize.
1439+
// See ConptyCursorPositionMayBeWrong().
1440+
void SCREEN_INFORMATION::SetConptyCursorPositionMayBeWrong() noexcept
1441+
{
1442+
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
1443+
assert(gci.IsConsoleLocked());
1444+
1445+
if (gci.IsInVtIoMode())
1446+
{
1447+
_conptyCursorPositionMayBeWrong.store(true, std::memory_order_relaxed);
1448+
}
1449+
}
1450+
1451+
// This should be called whenever we've synchronized our cursor position again.
1452+
// See ConptyCursorPositionMayBeWrong().
1453+
void SCREEN_INFORMATION::ResetConptyCursorPositionMayBeWrong() noexcept
1454+
{
1455+
_conptyCursorPositionMayBeWrong.store(false, std::memory_order_relaxed);
1456+
til::atomic_notify_all(_conptyCursorPositionMayBeWrong);
1457+
}
1458+
1459+
// Call this to synchronously wait until the ConPTY cursor position
1460+
// is known to be correct again. To do so, this emits a DSR CPR sequence
1461+
// and waits for a response from the terminal.
1462+
// See ConptyCursorPositionMayBeWrong().
1463+
void SCREEN_INFORMATION::WaitForConptyCursorPositionToBeSynchronized() noexcept
1464+
{
1465+
if (!_conptyCursorPositionMayBeWrong.load(std::memory_order_relaxed))
1466+
{
1467+
return;
1468+
}
1469+
1470+
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
1471+
1472+
{
1473+
gci.LockConsole();
1474+
const auto exit = wil::scope_exit([&] { gci.UnlockConsole(); });
1475+
auto writer = gci.GetVtWriterForBuffer(this);
1476+
1477+
if (!writer || !writer.WriteDSRCPR())
1478+
{
1479+
_conptyCursorPositionMayBeWrong.store(false, std::memory_order_relaxed);
1480+
return;
1481+
}
1482+
1483+
writer.Submit();
1484+
}
1485+
1486+
// If you were to hold the lock, the ConPTY input thread couldn't
1487+
// process any input and thus couldn't update the cursor position.
1488+
assert(!gci.IsConsoleLocked());
1489+
1490+
for (;;)
1491+
{
1492+
if (!_conptyCursorPositionMayBeWrong.load(std::memory_order::relaxed))
1493+
{
1494+
break;
1495+
}
1496+
1497+
// atomic_wait() returns false when the timeout expires.
1498+
// Technically we should decrement the timeout with each iteration,
1499+
// but I suspect infinite spurious wake-ups are a theoretical problem.
1500+
if (!til::atomic_wait(_conptyCursorPositionMayBeWrong, true, 500))
1501+
{
1502+
break;
1503+
}
1504+
}
1505+
}
1506+
14251507
// Routine Description:
14261508
// - Given a rectangle containing screen buffer coordinates (character-level positioning, not pixel)
14271509
// This method will trim the rectangle to ensure it is within the buffer.

src/host/screenInfo.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,11 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console
222222
[[nodiscard]] NTSTATUS ResizeWithReflow(const til::size coordnewScreenSize);
223223
[[nodiscard]] NTSTATUS ResizeTraditional(const til::size coordNewScreenSize);
224224

225+
bool ConptyCursorPositionMayBeWrong() const noexcept;
226+
void SetConptyCursorPositionMayBeWrong() noexcept;
227+
void ResetConptyCursorPositionMayBeWrong() noexcept;
228+
void WaitForConptyCursorPositionToBeSynchronized() noexcept;
229+
225230
private:
226231
SCREEN_INFORMATION(_In_ Microsoft::Console::Interactivity::IWindowMetrics* pMetrics,
227232
const TextAttribute popupAttributes,
@@ -280,6 +285,7 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console
280285
til::CoordType _virtualBottom;
281286

282287
std::optional<til::size> _deferredPtyResize{ std::nullopt };
288+
std::atomic<bool> _conptyCursorPositionMayBeWrong = false;
283289

284290
static void _handleDeferredResize(SCREEN_INFORMATION& siMain);
285291

src/server/ApiDispatchers.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,12 @@ constexpr T saturate(auto val)
584584
SCREEN_INFORMATION* pObj;
585585
RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_READ, &pObj));
586586

587+
// See ConptyCursorPositionMayBeWrong() for details.
588+
if (pObj->ConptyCursorPositionMayBeWrong())
589+
{
590+
pObj->WaitForConptyCursorPositionToBeSynchronized();
591+
}
592+
587593
m->_pApiRoutines->GetConsoleScreenBufferInfoExImpl(*pObj, ex);
588594

589595
a->FullscreenSupported = !!ex.bFullscreenSupported;

src/terminal/adapter/InteractDispatch.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ void InteractDispatch::WindowManipulation(const DispatchTypes::WindowManipulatio
149149
// - col: The column to move the cursor to.
150150
void InteractDispatch::MoveCursor(const VTInt row, const VTInt col)
151151
{
152+
const auto& api = ServiceLocator::LocateGlobals().api;
153+
auto& info = ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer();
154+
152155
// First retrieve some information about the buffer
153156
const auto viewport = _api.GetBufferAndViewport().viewport;
154157

@@ -159,9 +162,11 @@ void InteractDispatch::MoveCursor(const VTInt row, const VTInt col)
159162
coordCursor.x = std::clamp(coordCursor.x, viewport.left, viewport.right);
160163

161164
// Finally, attempt to set the adjusted cursor position back into the console.
162-
const auto api = gsl::not_null{ ServiceLocator::LocateGlobals().api };
163-
auto& info = ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer();
164165
LOG_IF_FAILED(api->SetConsoleCursorPositionImpl(info, coordCursor));
166+
167+
// Unblock any callers inside SCREEN_INFORMATION::WaitForConptyCursorPositionToBeSynchronized().
168+
// The cursor position has now been updated to the terminal's.
169+
info.ResetConptyCursorPositionMayBeWrong();
165170
}
166171

167172
// Routine Description:

src/terminal/parser/InputStateMachineEngine.cpp

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,19 @@ static bool operator==(const Ss3ToVkey& pair, const Ss3ActionCodes code) noexcep
8989
return pair.action == code;
9090
}
9191

92-
InputStateMachineEngine::InputStateMachineEngine(std::unique_ptr<IInteractDispatch> pDispatch, const bool lookingForDSR) :
92+
InputStateMachineEngine::InputStateMachineEngine(std::unique_ptr<IInteractDispatch> pDispatch) :
9393
_pDispatch(std::move(pDispatch)),
94-
_lookingForDSR(lookingForDSR),
9594
_doubleClickTime(std::chrono::milliseconds(GetDoubleClickTime()))
9695
{
9796
THROW_HR_IF_NULL(E_INVALIDARG, _pDispatch.get());
9897
}
9998

100-
til::enumset<DeviceAttribute, uint64_t> InputStateMachineEngine::WaitUntilDA1(DWORD timeout) const noexcept
99+
void InputStateMachineEngine::CaptureNextCursorPositionReport() noexcept
100+
{
101+
_captureNextCursorPositionReport.store(true, std::memory_order_relaxed);
102+
}
103+
104+
til::enumset<DeviceAttribute, uint64_t> InputStateMachineEngine::WaitUntilDA1(DWORD timeout) noexcept
101105
{
102106
uint64_t val = 0;
103107

@@ -118,6 +122,10 @@ til::enumset<DeviceAttribute, uint64_t> InputStateMachineEngine::WaitUntilDA1(DW
118122
}
119123
}
120124

125+
// VtIo first sends a DSR CPR and then a DA1 request.
126+
// If we encountered a DA1 response here, the DSR request is definitely done now.
127+
_captureNextCursorPositionReport.store(false, std::memory_order_relaxed);
128+
121129
return til::enumset<DeviceAttribute, uint64_t>::from_bits(val);
122130
}
123131

@@ -413,12 +421,9 @@ bool InputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParameter
413421
// The F3 case is special - it shares a code with the DeviceStatusResponse.
414422
// If we're looking for that response, then do that, and break out.
415423
// Else, fall though to the _GetCursorKeysModifierState handler.
416-
if (_lookingForDSR)
424+
if (_captureNextCursorPositionReport.exchange(false, std::memory_order_relaxed))
417425
{
418426
_pDispatch->MoveCursor(parameters.at(0), parameters.at(1));
419-
// Right now we're only looking for on initial cursor
420-
// position response. After that, only look for F3.
421-
_lookingForDSR = false;
422427
return true;
423428
}
424429
// Heuristic: If the hosting terminal used the win32 input mode, chances are high
@@ -491,10 +496,6 @@ bool InputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParameter
491496

492497
_deviceAttributes.fetch_or(attributes.bits(), std::memory_order_relaxed);
493498
til::atomic_notify_all(_deviceAttributes);
494-
495-
// VtIo first sends a DSR CPR and then a DA1 request.
496-
// If we encountered a DA1 response here, the DSR request is definitely done now.
497-
_lookingForDSR = false;
498499
return true;
499500
}
500501
return false;

0 commit comments

Comments
 (0)