mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-19 18:11:39 -05:00
This fixes a lot of subtle issues:
* Avoid emitting another de-/iconify VT sequence when
we encounter a (de)iconify VT sequence during parsing.
* Avoid emitting a de-/iconify VT sequence when
a focus event is received on the signal pipe.
* Avoid emitting such sequences on startup.
* Avoid emitting multiple such sequences
when rapidly un-/focusing the window.
It's also a minor cleanup, because the `GA_ROOTOWNER` is not security
relevant. It was added because there was concern that someone can just
spawn a ConPTY session, tell it that it's focused, and spawn a child
which is now focused. But why would someone do that, when the console
IOCTLs to do so are not just publicly available but also documented?
I also disabled the IME window.
## Validation Steps Performed
* First:
```cpp
int main() {
for (bool show = false;; show = !show) {
printf(show ? "Show in 3s...\n" : "Hide in 3s...\n");
Sleep(3000);
ShowWindow(GetConsoleWindow(), show ? SW_SHOW : SW_HIDE);
}
}
```
* PowerShell 5's `Get-Credential` gains focus ✅
* `sleep 5; Get-Credential` and focus another app. WT should start
blinking in the taskbar. Restore it. The popup has focus ✅
* Run `:hardcopy` in vim: Window is shown centered at (0,0) ✖️
But that's okay because it does that already anyway ✅
* `Connect-AzAccount` doesn't crash PowerShell ✅
556 lines
19 KiB
C++
556 lines
19 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "precomp.h"
|
|
|
|
#include "InteractivityFactory.hpp"
|
|
|
|
#include "../inc/ServiceLocator.hpp"
|
|
|
|
#ifdef BUILD_ONECORE_INTERACTIVITY
|
|
#include "..\onecore\AccessibilityNotifier.hpp"
|
|
#include "..\onecore\ConsoleControl.hpp"
|
|
#include "..\onecore\ConsoleInputThread.hpp"
|
|
#include "..\onecore\ConsoleWindow.hpp"
|
|
#include "..\onecore\ConIoSrvComm.hpp"
|
|
#include "..\onecore\SystemConfigurationProvider.hpp"
|
|
#include "..\onecore\WindowMetrics.hpp"
|
|
#endif
|
|
|
|
#include "../win32/AccessibilityNotifier.hpp"
|
|
#include "../win32/ConsoleControl.hpp"
|
|
#include "../win32/ConsoleInputThread.hpp"
|
|
#include "../win32/WindowDpiApi.hpp"
|
|
#include "../win32/WindowMetrics.hpp"
|
|
#include "../win32/SystemConfigurationProvider.hpp"
|
|
|
|
#pragma hdrstop
|
|
|
|
using namespace std;
|
|
using namespace Microsoft::Console::Interactivity;
|
|
|
|
#pragma region Public Methods
|
|
|
|
[[nodiscard]] NTSTATUS InteractivityFactory::CreateConsoleControl(_Inout_ std::unique_ptr<IConsoleControl>& control)
|
|
{
|
|
auto status = STATUS_SUCCESS;
|
|
|
|
ApiLevel level;
|
|
status = ApiDetector::DetectNtUserWindow(&level);
|
|
|
|
if (SUCCEEDED_NTSTATUS(status))
|
|
{
|
|
std::unique_ptr<IConsoleControl> newControl;
|
|
try
|
|
{
|
|
switch (level)
|
|
{
|
|
case ApiLevel::Win32:
|
|
newControl = std::make_unique<Microsoft::Console::Interactivity::Win32::ConsoleControl>();
|
|
break;
|
|
|
|
#ifdef BUILD_ONECORE_INTERACTIVITY
|
|
case ApiLevel::OneCore:
|
|
newControl = std::make_unique<Microsoft::Console::Interactivity::OneCore::ConsoleControl>();
|
|
break;
|
|
#endif
|
|
default:
|
|
status = STATUS_INVALID_LEVEL;
|
|
break;
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
|
|
}
|
|
|
|
if (SUCCEEDED_NTSTATUS(status))
|
|
{
|
|
control.swap(newControl);
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
[[nodiscard]] NTSTATUS InteractivityFactory::CreateConsoleInputThread(_Inout_ std::unique_ptr<IConsoleInputThread>& thread)
|
|
{
|
|
auto status = STATUS_SUCCESS;
|
|
|
|
ApiLevel level;
|
|
status = ApiDetector::DetectNtUserWindow(&level);
|
|
|
|
if (SUCCEEDED_NTSTATUS(status))
|
|
{
|
|
std::unique_ptr<IConsoleInputThread> newThread;
|
|
try
|
|
{
|
|
switch (level)
|
|
{
|
|
case ApiLevel::Win32:
|
|
newThread = std::make_unique<Microsoft::Console::Interactivity::Win32::ConsoleInputThread>();
|
|
break;
|
|
|
|
#ifdef BUILD_ONECORE_INTERACTIVITY
|
|
case ApiLevel::OneCore:
|
|
newThread = std::make_unique<Microsoft::Console::Interactivity::OneCore::ConsoleInputThread>();
|
|
break;
|
|
#endif
|
|
default:
|
|
status = STATUS_INVALID_LEVEL;
|
|
break;
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
|
|
}
|
|
|
|
if (SUCCEEDED_NTSTATUS(status))
|
|
{
|
|
thread.swap(newThread);
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
[[nodiscard]] NTSTATUS InteractivityFactory::CreateHighDpiApi(_Inout_ std::unique_ptr<IHighDpiApi>& api)
|
|
{
|
|
auto status = STATUS_SUCCESS;
|
|
|
|
ApiLevel level;
|
|
status = ApiDetector::DetectNtUserWindow(&level);
|
|
|
|
if (SUCCEEDED_NTSTATUS(status))
|
|
{
|
|
std::unique_ptr<IHighDpiApi> newApi;
|
|
try
|
|
{
|
|
switch (level)
|
|
{
|
|
case ApiLevel::Win32:
|
|
newApi = std::make_unique<Microsoft::Console::Interactivity::Win32::WindowDpiApi>();
|
|
break;
|
|
|
|
#ifdef BUILD_ONECORE_INTERACTIVITY
|
|
case ApiLevel::OneCore:
|
|
newApi.reset();
|
|
break;
|
|
#endif
|
|
default:
|
|
status = STATUS_INVALID_LEVEL;
|
|
break;
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
|
|
}
|
|
|
|
if (SUCCEEDED_NTSTATUS(status))
|
|
{
|
|
api.swap(newApi);
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
[[nodiscard]] NTSTATUS InteractivityFactory::CreateWindowMetrics(_Inout_ std::unique_ptr<IWindowMetrics>& metrics)
|
|
{
|
|
auto status = STATUS_SUCCESS;
|
|
|
|
ApiLevel level;
|
|
status = ApiDetector::DetectNtUserWindow(&level);
|
|
|
|
if (SUCCEEDED_NTSTATUS(status))
|
|
{
|
|
std::unique_ptr<IWindowMetrics> newMetrics;
|
|
try
|
|
{
|
|
switch (level)
|
|
{
|
|
case ApiLevel::Win32:
|
|
newMetrics = std::make_unique<Microsoft::Console::Interactivity::Win32::WindowMetrics>();
|
|
break;
|
|
|
|
#ifdef BUILD_ONECORE_INTERACTIVITY
|
|
case ApiLevel::OneCore:
|
|
newMetrics = std::make_unique<Microsoft::Console::Interactivity::OneCore::WindowMetrics>();
|
|
break;
|
|
#endif
|
|
default:
|
|
status = STATUS_INVALID_LEVEL;
|
|
break;
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
|
|
}
|
|
|
|
if (SUCCEEDED_NTSTATUS(status))
|
|
{
|
|
metrics.swap(newMetrics);
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
[[nodiscard]] NTSTATUS InteractivityFactory::CreateAccessibilityNotifier(_Inout_ std::unique_ptr<IAccessibilityNotifier>& notifier)
|
|
{
|
|
auto status = STATUS_SUCCESS;
|
|
|
|
ApiLevel level;
|
|
status = ApiDetector::DetectNtUserWindow(&level);
|
|
|
|
if (SUCCEEDED_NTSTATUS(status))
|
|
{
|
|
std::unique_ptr<IAccessibilityNotifier> newNotifier;
|
|
try
|
|
{
|
|
switch (level)
|
|
{
|
|
case ApiLevel::Win32:
|
|
newNotifier = std::make_unique<Microsoft::Console::Interactivity::Win32::AccessibilityNotifier>();
|
|
break;
|
|
|
|
#ifdef BUILD_ONECORE_INTERACTIVITY
|
|
case ApiLevel::OneCore:
|
|
newNotifier = std::make_unique<Microsoft::Console::Interactivity::OneCore::AccessibilityNotifier>();
|
|
break;
|
|
#endif
|
|
default:
|
|
status = STATUS_INVALID_LEVEL;
|
|
break;
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
|
|
}
|
|
|
|
if (SUCCEEDED_NTSTATUS(status))
|
|
{
|
|
notifier.swap(newNotifier);
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
[[nodiscard]] NTSTATUS InteractivityFactory::CreateSystemConfigurationProvider(_Inout_ std::unique_ptr<ISystemConfigurationProvider>& provider)
|
|
{
|
|
auto status = STATUS_SUCCESS;
|
|
|
|
ApiLevel level;
|
|
status = ApiDetector::DetectNtUserWindow(&level);
|
|
|
|
if (SUCCEEDED_NTSTATUS(status))
|
|
{
|
|
std::unique_ptr<ISystemConfigurationProvider> NewProvider;
|
|
try
|
|
{
|
|
switch (level)
|
|
{
|
|
case ApiLevel::Win32:
|
|
NewProvider = std::make_unique<Microsoft::Console::Interactivity::Win32::SystemConfigurationProvider>();
|
|
break;
|
|
|
|
#ifdef BUILD_ONECORE_INTERACTIVITY
|
|
case ApiLevel::OneCore:
|
|
NewProvider = std::make_unique<Microsoft::Console::Interactivity::OneCore::SystemConfigurationProvider>();
|
|
break;
|
|
#endif
|
|
default:
|
|
status = STATUS_INVALID_LEVEL;
|
|
break;
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
|
|
}
|
|
|
|
if (SUCCEEDED_NTSTATUS(status))
|
|
{
|
|
provider.swap(NewProvider);
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Attempts to instantiate a "pseudo window" for when we're operating in
|
|
// pseudoconsole mode. There are some tools (cygwin & derivatives) that use
|
|
// the GetConsoleWindow API to uniquely identify console sessions. This
|
|
// function is used to create an invisible window for that scenario, so
|
|
// that GetConsoleWindow returns a real value.
|
|
// Arguments:
|
|
// - hwnd: Receives the value of the newly created window's HWND.
|
|
// - owner: the HWND that should be the initial owner of the pseudo window.
|
|
// Return Value:
|
|
// - STATUS_SUCCESS on success, otherwise an appropriate error.
|
|
[[nodiscard]] NTSTATUS InteractivityFactory::CreatePseudoWindow(HWND& hwnd)
|
|
{
|
|
hwnd = nullptr;
|
|
ApiLevel level;
|
|
auto status = ApiDetector::DetectNtUserWindow(&level);
|
|
|
|
if (SUCCEEDED_NTSTATUS(status))
|
|
{
|
|
try
|
|
{
|
|
static const auto PSEUDO_WINDOW_CLASS = L"PseudoConsoleWindow";
|
|
WNDCLASSEXW pseudoClass{ 0 };
|
|
switch (level)
|
|
{
|
|
case ApiLevel::Win32:
|
|
{
|
|
// We don't need an "Default IME" window for ConPTY. That's the terminal's job.
|
|
// -1, aka DWORD_MAX, tells the function to disable it for the entire process.
|
|
// Must be called before creating any window.
|
|
ImmDisableIME(DWORD_MAX);
|
|
|
|
pseudoClass.cbSize = sizeof(WNDCLASSEXW);
|
|
pseudoClass.lpszClassName = PSEUDO_WINDOW_CLASS;
|
|
pseudoClass.lpfnWndProc = s_PseudoWindowProc;
|
|
pseudoClass.cbWndExtra = GWL_CONSOLE_WNDALLOC; // this is required to store the owning thread/process override in NTUSER
|
|
auto windowClassAtom{ RegisterClassExW(&pseudoClass) };
|
|
|
|
// Note that because we're not specifying WS_CHILD, this window
|
|
// will become an _owned_ window, not a _child_ window. This is
|
|
// important - child windows report their position as relative
|
|
// to their parent window, while owned windows are still
|
|
// relative to the desktop. (there are other subtleties as well
|
|
// as far as the difference between parent/child and owner/owned
|
|
// windows). Evan K said we should do it this way, and he
|
|
// definitely knows.
|
|
//
|
|
// GH#13066: Load-bearing: Make sure to set WS_POPUP. If you
|
|
// don't, then GetAncestor(GetConsoleWindow(), GA_ROOTOWNER)
|
|
// will return the console handle again, not the owning
|
|
// terminal's handle. It's not entirely clear why, but WS_POPUP
|
|
// is absolutely vital for this to work correctly.
|
|
hwnd = CreateWindowExW(WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
|
|
reinterpret_cast<LPCWSTR>(windowClassAtom),
|
|
nullptr,
|
|
WS_POPUP,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
_owner.load(std::memory_order_relaxed),
|
|
nullptr,
|
|
nullptr,
|
|
this);
|
|
|
|
if (hwnd == nullptr)
|
|
{
|
|
const auto gle = GetLastError();
|
|
status = NTSTATUS_FROM_WIN32(gle);
|
|
}
|
|
_pseudoConsoleWindowHwnd = hwnd;
|
|
break;
|
|
}
|
|
#ifdef BUILD_ONECORE_INTERACTIVITY
|
|
case ApiLevel::OneCore:
|
|
hwnd = 0;
|
|
status = STATUS_SUCCESS;
|
|
break;
|
|
#endif
|
|
default:
|
|
status = STATUS_INVALID_LEVEL;
|
|
break;
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
void InteractivityFactory::SetOwner(HWND owner) noexcept
|
|
{
|
|
_owner.store(owner, std::memory_order_relaxed);
|
|
|
|
if (_pseudoConsoleWindowHwnd)
|
|
{
|
|
// DO NOT USE SetParent HERE!
|
|
//
|
|
// Calling SetParent on a window that is WS_VISIBLE will cause the OS to
|
|
// hide the window, make it a _child_ window, then call SW_SHOW on the
|
|
// window to re-show it. SW_SHOW, however, will cause the OS to also set
|
|
// that window as the _foreground_ window, which would result in the
|
|
// pty's hwnd stealing the foreground away from the owning terminal
|
|
// window. That's bad.
|
|
//
|
|
// SetWindowLongPtr seems to do the job of changing who the window owner
|
|
// is, without all the other side effects of reparenting the window.
|
|
// See #13066
|
|
::SetWindowLongPtrW(_pseudoConsoleWindowHwnd, GWLP_HWNDPARENT, reinterpret_cast<LONG_PTR>(owner));
|
|
}
|
|
}
|
|
|
|
void InteractivityFactory::SetVisibility(const bool isVisible) noexcept
|
|
{
|
|
if (_pseudoConsoleWindowHwnd && IsIconic(_pseudoConsoleWindowHwnd) != static_cast<BOOL>(isVisible))
|
|
{
|
|
_suppressVisibilityChange.store(true, std::memory_order_relaxed);
|
|
ShowWindow(_pseudoConsoleWindowHwnd, isVisible ? SW_SHOWNOACTIVATE : SW_MINIMIZE);
|
|
_suppressVisibilityChange.store(false, std::memory_order_relaxed);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Static window procedure for pseudo console windows
|
|
// - Processes set-up on create to stow the "this" pointer to specific instantiations and routes
|
|
// to the specific object on future calls.
|
|
// Arguments:
|
|
// - hWnd - Associated window handle from message
|
|
// - Message - ID of message in queue
|
|
// - wParam - Variable wParam depending on message type
|
|
// - lParam - Variable lParam depending on message type
|
|
// Return Value:
|
|
// - 0 if we processed this message. See details on how a WindowProc is implemented.
|
|
[[nodiscard]] LRESULT CALLBACK InteractivityFactory::s_PseudoWindowProc(_In_ HWND hWnd, _In_ UINT Message, _In_ WPARAM wParam, _In_ LPARAM lParam)
|
|
{
|
|
// Save the pointer here to the specific window instance when one is created
|
|
if (Message == WM_CREATE)
|
|
{
|
|
const CREATESTRUCT* const pCreateStruct = reinterpret_cast<CREATESTRUCT*>(lParam);
|
|
|
|
InteractivityFactory* const pFactory = reinterpret_cast<InteractivityFactory*>(pCreateStruct->lpCreateParams);
|
|
SetWindowLongPtrW(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(pFactory));
|
|
}
|
|
|
|
// Dispatch the message to the specific class instance
|
|
InteractivityFactory* const pFactory = reinterpret_cast<InteractivityFactory*>(GetWindowLongPtrW(hWnd, GWLP_USERDATA));
|
|
if (pFactory != nullptr)
|
|
{
|
|
return pFactory->PseudoWindowProc(hWnd, Message, wParam, lParam);
|
|
}
|
|
|
|
// If we get this far, call the default window proc
|
|
return DefWindowProcW(hWnd, Message, wParam, lParam);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Per-object window procedure for pseudo console windows
|
|
// Arguments:
|
|
// - hWnd - Associated window handle from message
|
|
// - Message - ID of message in queue
|
|
// - wParam - Variable wParam depending on message type
|
|
// - lParam - Variable lParam depending on message type
|
|
// Return Value:
|
|
// - 0 if we processed this message. See details on how a WindowProc is implemented.
|
|
[[nodiscard]] LRESULT CALLBACK InteractivityFactory::PseudoWindowProc(_In_ HWND hWnd, _In_ UINT Message, _In_ WPARAM wParam, _In_ LPARAM lParam)
|
|
{
|
|
switch (Message)
|
|
{
|
|
// NOTE: To the future reader, all window messages that are talked about but unused were tested
|
|
// during prototyping and didn't give quite the results needed to determine show/hide window
|
|
// state. The notes are left here for future expeditions into message queues.
|
|
// case WM_QUERYOPEN:
|
|
// It can be fun to toggle WM_QUERYOPEN but DefWindowProc returns TRUE.
|
|
case WM_SIZE:
|
|
{
|
|
// Curiously, at least on Windows 10 (and rarely on Windows 11), if you
|
|
// minimize the Terminal by clicking on the taskbar, then alt-tab to try
|
|
// and restore the window, the Taskbar will decide to call
|
|
// SwitchToWindow on us, the invisible, owned window of the main window.
|
|
// When that happens, we'll get a WM_SIZE(SIZE_RESTORED, lParam=0). The
|
|
// main window will NOT get a SwitchToWindow called. If we don't
|
|
// actually inform the hosting process about this, then the main HWND
|
|
// might stay hidden. Refer to GH#13589
|
|
|
|
if (wParam == SIZE_RESTORED)
|
|
{
|
|
_WritePseudoWindowCallback(true);
|
|
}
|
|
|
|
if (wParam == SIZE_MINIMIZED)
|
|
{
|
|
_WritePseudoWindowCallback(false);
|
|
}
|
|
return 0;
|
|
}
|
|
// case WM_WINDOWPOSCHANGING:
|
|
// As long as user32 didn't eat the `ShowWindow` call because the window state requested
|
|
// matches the existing WS_VISIBLE state of the HWND... we should hear from it in WM_WINDOWPOSCHANGING.
|
|
// WM_WINDOWPOSCHANGING can tell us a bunch through the flags fields.
|
|
// We can also check IsIconic/IsZoomed on the HWND during the message
|
|
// and we could suppress the change to prevent things from happening.
|
|
// case WM_SYSCOMMAND:
|
|
// WM_SYSCOMMAND will not come through. Don't try.
|
|
// WM_SHOWWINDOW does come through on some of the messages.
|
|
case WM_SHOWWINDOW:
|
|
{
|
|
if (0 == lParam) // Someone explicitly called ShowWindow on us.
|
|
{
|
|
_WritePseudoWindowCallback((bool)wParam);
|
|
}
|
|
return 0;
|
|
}
|
|
case WM_GETOBJECT:
|
|
{
|
|
if (static_cast<long>(lParam) == static_cast<long>(UiaRootObjectId))
|
|
{
|
|
if (nullptr == _pPseudoConsoleUiaProvider)
|
|
{
|
|
LOG_IF_FAILED(WRL::MakeAndInitialize<PseudoConsoleWindowAccessibilityProvider>(&_pPseudoConsoleUiaProvider, _pseudoConsoleWindowHwnd));
|
|
}
|
|
return UiaReturnRawElementProvider(hWnd, wParam, lParam, _pPseudoConsoleUiaProvider.Get());
|
|
}
|
|
return 0;
|
|
}
|
|
case WM_ACTIVATE:
|
|
{
|
|
if (const auto ownerHwnd = _owner.load(std::memory_order_relaxed))
|
|
{
|
|
SetFocus(ownerHwnd);
|
|
return 0;
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
// If we get this far, call the default window proc
|
|
return DefWindowProcW(hWnd, Message, wParam, lParam);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Helper for the pseudo console message loop to send a notification
|
|
// when it realizes we should be showing or hiding the window.
|
|
// - Simply skips if no callback is installed.
|
|
// Arguments:
|
|
// - showOrHide: Show is true; hide is false.
|
|
// Return Value:
|
|
// - <none>
|
|
void InteractivityFactory::_WritePseudoWindowCallback(bool showOrHide)
|
|
{
|
|
if (_suppressVisibilityChange.load(std::memory_order_relaxed))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// IMPORTANT!
|
|
//
|
|
// A hosting terminal window should only "restore" itself in response to
|
|
// this message, if it's already minimized. If the window is maximized a
|
|
// restore will restore-down the window instead.
|
|
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
|
|
if (auto writer = gci.GetVtWriter())
|
|
{
|
|
writer.WriteWindowVisibility(showOrHide);
|
|
writer.Submit();
|
|
}
|
|
}
|
|
|
|
#pragma endregion
|