// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "../host/scrolling.hpp" #include "../interactivity/inc/ServiceLocator.hpp" #pragma hdrstop using namespace Microsoft::Console; using namespace Microsoft::Console::Interactivity; CursorBlinker::CursorBlinker() : _hCaretBlinkTimer(INVALID_HANDLE_VALUE), _hCaretBlinkTimerQueue(THROW_LAST_ERROR_IF_NULL(CreateTimerQueue())), _uCaretBlinkTime(INFINITE) // default to no blink { } CursorBlinker::~CursorBlinker() { if (_hCaretBlinkTimerQueue) { DeleteTimerQueueEx(_hCaretBlinkTimerQueue, INVALID_HANDLE_VALUE); } } void CursorBlinker::UpdateSystemMetrics() { // This can be -1 in a TS session _uCaretBlinkTime = ServiceLocator::LocateSystemConfigurationProvider()->GetCaretBlinkTime(); } void CursorBlinker::SettingsChanged() { DWORD const dwCaretBlinkTime = ServiceLocator::LocateSystemConfigurationProvider()->GetCaretBlinkTime(); if (dwCaretBlinkTime != _uCaretBlinkTime) { KillCaretTimer(); _uCaretBlinkTime = dwCaretBlinkTime; SetCaretTimer(); } } void CursorBlinker::FocusEnd() { KillCaretTimer(); } void CursorBlinker::FocusStart() { SetCaretTimer(); } // Routine Description: // - This routine is called when the timer in the console with the focus goes off. It blinks the cursor. // Arguments: // - ScreenInfo - reference to screen info structure. // Return Value: // - void CursorBlinker::TimerRoutine(SCREEN_INFORMATION& ScreenInfo) { Cursor& cursor = ScreenInfo.GetTextBuffer().GetCursor(); const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto* const _pAccessibilityNotifier = ServiceLocator::LocateAccessibilityNotifier(); if (!WI_IsFlagSet(gci.Flags, CONSOLE_HAS_FOCUS)) { goto DoScroll; } // Update the cursor pos in USER so accessibility will work. if (cursor.HasMoved()) { const auto position = cursor.GetPosition(); const auto viewport = ScreenInfo.GetViewport(); const auto fontSize = ScreenInfo.GetScreenFontSize(); cursor.SetHasMoved(false); RECT rc; rc.left = (position.X - viewport.Left()) * fontSize.X; rc.top = (position.Y - viewport.Top()) * fontSize.Y; rc.right = rc.left + fontSize.X; rc.bottom = rc.top + fontSize.Y; _pAccessibilityNotifier->NotifyConsoleCaretEvent(rc); // Send accessibility information { IAccessibilityNotifier::ConsoleCaretEventFlags flags = IAccessibilityNotifier::ConsoleCaretEventFlags::CaretInvisible; // Flags is expected to be 2, 1, or 0. 2 in selecting (whether or not visible), 1 if just visible, 0 if invisible/noselect. if (WI_IsFlagSet(gci.Flags, CONSOLE_SELECTING)) { flags = IAccessibilityNotifier::ConsoleCaretEventFlags::CaretSelection; } else if (cursor.IsVisible()) { flags = IAccessibilityNotifier::ConsoleCaretEventFlags::CaretVisible; } _pAccessibilityNotifier->NotifyConsoleCaretEvent(flags, MAKELONG(position.X, position.Y)); } } // If the DelayCursor flag has been set, wait one more tick before toggle. // This is used to guarantee the cursor is on for a finite period of time // after a move and off for a finite period of time after a WriteString. if (cursor.GetDelay()) { cursor.SetDelay(false); goto DoScroll; } // Don't blink the cursor for remote sessions. if ((!ServiceLocator::LocateSystemConfigurationProvider()->IsCaretBlinkingEnabled() || _uCaretBlinkTime == -1 || (!cursor.IsBlinkingAllowed())) && cursor.IsOn()) { goto DoScroll; } // Blink only if the cursor isn't turned off via the API if (cursor.IsVisible()) { cursor.SetIsOn(!cursor.IsOn()); } DoScroll: Scrolling::s_ScrollIfNecessary(ScreenInfo); } void CALLBACK CursorTimerRoutineWrapper(_In_ PVOID /* lpParam */, _In_ BOOLEAN /* TimerOrWaitFired */) { // Suppose the following sequence of events takes place: // // 1. The user resizes the console; // 2. The console acquires the console lock; // 3. The current SCREEN_INFORMATION instance is deleted; // 4. This causes the current Cursor instance to be deleted, too; // 5. The Cursor's destructor is called; // => Somewhere between 1 and 5, the timer fires: // Timer queue timer callbacks execute asynchronously with respect to // the UI thread under which the numbered steps are taking place. // Because the callback touches console state, it needs to acquire the // console lock. But what if the timer callback fires at just the right // time such that 2 has already acquired the lock? // 6. The Cursor's destructor deletes the timer queue and thereby destroys // the timer queue timer used for blinking. However, because this // timer's callback modifies console state, it is prudent to not // continue the destruction if the callback has already started but has // not yet finished. Therefore, the destructor waits for the callback to // finish executing. // => Meanwhile, the callback just happens to be stuck waiting for the // console lock acquired in step 2. Since the destructor is waiting on // the callback to complete, and the callback is waiting on the lock, // which will only be released way after the Cursor instance is deleted, // the console has now deadlocked. // // As a solution, skip the blink if the console lock is already being held. // Note that critical sections to not have a waitable synchronization // object unless there readily is contention on it. As a result, if we // wanted to wait until the lock became available under the condition of // not being destroyed, things get too complicated. CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); if (gci.TryLockConsole() != false) { // Cursor& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); gci.GetCursorBlinker().TimerRoutine(gci.GetActiveOutputBuffer()); // This was originally just UnlockConsole, not CONSOLE_INFORMATION::UnlockConsole // Is there a reason it would need to be the global version? gci.UnlockConsole(); } } // Routine Description: // - If guCaretBlinkTime is -1, we don't want to blink the caret. However, we // need to make sure it gets drawn, so we'll set a short timer. When that // goes off, we'll hit CursorTimerRoutine, and it'll do the right thing if // guCaretBlinkTime is -1. void CursorBlinker::SetCaretTimer() { static const DWORD dwDefTimeout = 0x212; KillCaretTimer(); if (_hCaretBlinkTimer == INVALID_HANDLE_VALUE) { bool bRet = true; DWORD dwEffectivePeriod = _uCaretBlinkTime == -1 ? dwDefTimeout : _uCaretBlinkTime; bRet = CreateTimerQueueTimer(&_hCaretBlinkTimer, _hCaretBlinkTimerQueue, CursorTimerRoutineWrapper, this, dwEffectivePeriod, dwEffectivePeriod, 0); LOG_LAST_ERROR_IF(!bRet); } } void CursorBlinker::KillCaretTimer() { if (_hCaretBlinkTimer != INVALID_HANDLE_VALUE) { bool bRet = true; bRet = DeleteTimerQueueTimer(_hCaretBlinkTimerQueue, _hCaretBlinkTimer, NULL); // According to https://msdn.microsoft.com/en-us/library/windows/desktop/ms682569(v=vs.85).aspx // A failure to delete the timer with the LastError being ERROR_IO_PENDING means that the timer is // currently in use and will get cleaned up when released. Delete should not be called again. // We treat that case as a success. if (bRet == false && GetLastError() != ERROR_IO_PENDING) { LOG_LAST_ERROR(); } else { _hCaretBlinkTimer = INVALID_HANDLE_VALUE; } } }