Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid focus loops in ConPTY #17829

Merged
merged 3 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actions/spelling/allow/apis.txt
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ IGraphics
IImage
IInheritable
IMap
imm
IMonarch
IObject
iosfwd
Expand Down
23 changes: 21 additions & 2 deletions src/cascadia/TerminalControl/ControlCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation
}
});

// If you rapidly show/hide Windows Terminal, something about GotFocus()/LostFocus() gets broken.
// We'll then receive easily 10+ such calls from WinUI the next time the application is shown.
shared->focusChanged = std::make_unique<til::debounced_func_trailing<bool>>(
std::chrono::milliseconds{ 25 },
[weakThis = get_weak()](const bool focused) {
if (const auto core{ weakThis.get() })
{
core->_focusChanged(focused);
}
});
Comment on lines +206 to +213
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Avoid emitting multiple such sequences when rapidly un-/focusing the window.


// Scrollbar updates are also expensive (XAML), so we'll throttle them as well.
shared->updateScrollBar = std::make_shared<ThrottledFuncTrailing<Control::ScrollPositionChangedArgs>>(
_dispatcher,
Expand Down Expand Up @@ -2480,13 +2491,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// - This is related to work done for GH#2988.
void ControlCore::GotFocus()
{
_focusChanged(true);
const auto shared = _shared.lock_shared();
if (shared->focusChanged)
{
(*shared->focusChanged)(true);
}
}

// See GotFocus.
void ControlCore::LostFocus()
{
_focusChanged(false);
const auto shared = _shared.lock_shared();
if (shared->focusChanged)
{
(*shared->focusChanged)(false);
}
}

void ControlCore::_focusChanged(bool focused)
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalControl/ControlCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
struct SharedState
{
std::unique_ptr<til::debounced_func_trailing<>> outputIdle;
std::unique_ptr<til::debounced_func_trailing<bool>> focusChanged;
std::shared_ptr<ThrottledFuncTrailing<Control::ScrollPositionChangedArgs>> updateScrollBar;
};

Expand Down
2 changes: 1 addition & 1 deletion src/cascadia/UnitTests_TerminalCore/UnitTests.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<AdditionalDependencies>WindowsApp.lib;WinMM.Lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>WindowsApp.lib;winmm.Lib;imm32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<!-- Careful reordering these. Some default props (contained in these files) are order sensitive. -->
Expand Down
46 changes: 3 additions & 43 deletions src/host/PtySignalInputThread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@ void PtySignalInputThread::ConnectConsole() noexcept
{
_DoShowHide(*_initialShowHide);
}

// We should have successfully used the _earlyReparent message in CreatePseudoWindow.
}

// Method Description:
Expand All @@ -87,8 +85,7 @@ void PtySignalInputThread::ConnectConsole() noexcept
// - Refer to GH#13066 for details.
void PtySignalInputThread::CreatePseudoWindow()
{
HWND owner = _earlyReparent.has_value() ? reinterpret_cast<HWND>((*_earlyReparent).handle) : HWND_DESKTOP;
ServiceLocator::LocatePseudoWindow(owner);
ServiceLocator::LocatePseudoWindow();
}

// Method Description:
Expand Down Expand Up @@ -220,7 +217,7 @@ void PtySignalInputThread::_DoShowHide(const ShowHideData& data)
return;
}

_api.ShowWindow(data.show);
ServiceLocator::SetPseudoWindowVisibility(data.show);
}

// Method Description:
Expand All @@ -237,44 +234,7 @@ void PtySignalInputThread::_DoSetWindowParent(const SetParentData& data)
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });

// If the client app hasn't yet connected, stash the new owner.
// We'll later (PtySignalInputThread::ConnectConsole) use the value
// to set up the owner of the conpty window.
if (!_consoleConnected)
{
_earlyReparent = data;
return;
}

const auto owner{ reinterpret_cast<HWND>(data.handle) };
// This will initialize s_interactivityFactory for us. It will also
// conveniently return 0 when we're on OneCore.
//
// If the window hasn't been created yet, by some other call to
// LocatePseudoWindow, then this will also initialize the owner of the
// window.
if (const auto pseudoHwnd{ ServiceLocator::LocatePseudoWindow(owner) })
{
// SetWindowLongPtrW may call back into the message handler and wait for it to finish,
// similar to SendMessageW(). If the conhost message handler is already processing and
// waiting to acquire the console lock, which we're currently holding, we'd deadlock.
// --> Release the lock now.
Unlock.reset();

// 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(pseudoHwnd, GWLP_HWNDPARENT, reinterpret_cast<LONG_PTR>(owner));
}
ServiceLocator::SetPseudoWindowOwner(reinterpret_cast<HWND>(data.handle));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Azure PowerShell extension crashes the PowerShell process hard (not even an exception or anything) if you give it a HWND_MESSAGE. But my entire concept was based around that! That's why I moved all of the parent/owner code to InteractivityFactory too (below in this PR): A HWND_MESSAGE window cannot have a custom parent like this and so I had to store the owner in a member. I felt like InteractivityFactory should store it.

I left these changes in, because it IMO still makes the code easier to maintain: Now all of the parent/owner code is centralized in a single file.

}

// Method Description:
Expand Down
3 changes: 0 additions & 3 deletions src/host/PtySignalInputThread.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,5 @@ namespace Microsoft::Console
std::optional<ResizeWindowData> _earlyResize;
std::optional<ShowHideData> _initialShowHide;
ConhostInternalGetSet _api;

public:
std::optional<SetParentData> _earlyReparent;
};
}
2 changes: 1 addition & 1 deletion src/host/exe/Host.EXE.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
</ClCompile>
<Link>
<AllowIsolation>true</AllowIsolation>
<AdditionalDependencies>WinMM.Lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>winmm.lib;imm32.lib;%(AdditionalDependencies)</AdditionalDependencies>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which API do we need from this? If it is part of an APIset, we should prefer apiset linkage. We may also need to update sources.

</Link>
</ItemDefinitionGroup>
<!-- Careful reordering these. Some default props (contained in these files) are order sensitive. -->
Expand Down
4 changes: 2 additions & 2 deletions src/host/ft_fuzzer/Host.FuzzWrapper.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@
<AdditionalIncludeDirectories>..;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalDependencies>winmm.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>winmm.lib;imm32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<SubSystem>Console</SubSystem>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Fuzzing'">
<!-- In theory, we may want to build with a normal main() when Fuzzing is not enabled. -->
<!-- So, let's only add the fuzzer to the link line when we're building for Fuzzing. -->
<Link>
<AdditionalDependencies>WinMM.Lib;clang_rt.fuzzer_MT-$(OCClangArchitectureName).lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>winmm.lib;imm32.lib;clang_rt.fuzzer_MT-$(OCClangArchitectureName).lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<!-- Careful reordering these. Some default props (contained in these files) are order sensitive. -->
Expand Down
11 changes: 9 additions & 2 deletions src/host/outputStream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -208,15 +208,22 @@ CursorType ConhostInternalGetSet::GetUserDefaultCursorStyle() const
// - <none>
void ConhostInternalGetSet::ShowWindow(bool showOrHide)
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const auto hwnd = gci.IsInVtIoMode() ? ServiceLocator::LocatePseudoWindow() : ServiceLocator::LocateConsoleWindow()->GetWindowHandle();
// ConPTY is supposed to be "transparent" to the VT application. Any VT it processes is given to the terminal.
// As such, it must not react to this "CSI 1 t" or "CSI 2 t" sequence. That's the job of the terminal.
// If the terminal encounters such a sequence, it can show/hide itself and let ConPTY know via its signal API.
const auto window = ServiceLocator::LocateConsoleWindow();
if (!window)
{
return;
}
Comment on lines +211 to +218
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Avoid emitting another de-/iconify VT sequence when we encounter a (de)iconify VT sequence during parsing.


// GH#13301 - When we send this ShowWindow message, if we send it to the
// conhost HWND, it's going to need to get processed by the window message
// thread before returning.
// However, ShowWindowAsync doesn't have this problem. It'll post the
// message to the window thread, then immediately return, so we don't have
// to worry about deadlocking.
const auto hwnd = window->GetWindowHandle();
::ShowWindowAsync(hwnd, showOrHide ? SW_SHOWNOACTIVATE : SW_MINIMIZE);
}

Expand Down
2 changes: 2 additions & 0 deletions src/host/sources.inc
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ TARGETLIBS = \
$(ONECOREUAP_EXTERNAL_SDK_LIB_PATH)\dxgi.lib \
$(ONECOREUAP_EXTERNAL_SDK_LIB_PATH)\d3d11.lib \
$(MODERNCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\api-ms-win-mm-playsound-l1.lib \
$(MODERNCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-imm-l1-1-0.lib \
$(ONECORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-dwmapi-ext-l1.lib \
$(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-dc-l1.lib \
$(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-dc-create-l1.lib \
Expand Down Expand Up @@ -195,6 +196,7 @@ DELAYLOAD = \
api-ms-win-core-com-l1.dll; \
api-ms-win-core-registry-l2.dll; \
api-ms-win-mm-playsound-l1.dll; \
ext-ms-win-imm-l1-1-0.lib; \
api-ms-win-shcore-obsolete-l1.dll; \
api-ms-win-shcore-scaling-l1.dll; \
api-ms-win-shell-dataobject-l1.dll; \
Expand Down
2 changes: 1 addition & 1 deletion src/host/ut_host/Host.UnitTests.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
<AdditionalIncludeDirectories>..;$(SolutionDir)src\inc;$(SolutionDir)src\inc\test;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalDependencies>WinMM.Lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>winmm.lib;imm32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<!-- Careful reordering these. Some default props (contained in these files) are order sensitive. -->
Expand Down
60 changes: 49 additions & 11 deletions src/interactivity/base/InteractivityFactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ using namespace Microsoft::Console::Interactivity;
// - 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, const HWND owner)
[[nodiscard]] NTSTATUS InteractivityFactory::CreatePseudoWindow(HWND& hwnd)
{
hwnd = nullptr;
ApiLevel level;
Expand All @@ -310,6 +310,11 @@ using namespace Microsoft::Console::Interactivity;
{
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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This avoids spawning the entire IME machinery inside ConPTY.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this answers my question about imm32.

For sources, can you instead link ext-ms-win-imm-l1-1-0? It is an extension APIset and can be delay-loaded.

That should be sufficient.
No need to change the link line for the vcxproj.


pseudoClass.cbSize = sizeof(WNDCLASSEXW);
pseudoClass.lpszClassName = PSEUDO_WINDOW_CLASS;
pseudoClass.lpfnWndProc = s_PseudoWindowProc;
Expand All @@ -330,19 +335,15 @@ using namespace Microsoft::Console::Interactivity;
// 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.
const auto windowStyle = WS_OVERLAPPEDWINDOW | WS_POPUP;
const auto exStyles = WS_EX_TOOLWINDOW | WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_NOACTIVATE;

// Attempt to create window.
hwnd = CreateWindowExW(exStyles,
hwnd = CreateWindowExW(WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
reinterpret_cast<LPCWSTR>(windowClassAtom),
nullptr,
windowStyle,
WS_POPUP,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No WS_OVERLAPPEDWINDOW = no DWM, no border, no size. Makes it cheaper and easier. Seems to still work too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder if this fixes the "openconsole has a 0x0 window at 0x0" problem

0,
0,
0,
0,
owner,
_owner.load(std::memory_order_relaxed),
nullptr,
nullptr,
this);
Expand Down Expand Up @@ -375,6 +376,38 @@ using namespace Microsoft::Console::Interactivity;
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we certain that this is processed in order on all versions of Windows we care about?

Alternatively... should we do the management of suppress in a custom window message?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open question

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I overlooked this one. When you say in-order, what do you mean? If it's what I think it is, then yes, this will work: ShowWindow is synchronous by spec. There's an alternative. ShowWindowAsync.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perfect. thanks.

_suppressVisibilityChange.store(false, std::memory_order_relaxed);
}
Comment on lines +403 to +408
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Avoid emitting a de-/iconify VT sequence when a focus event is received on the signal pipe.

}

// Method Description:
// - Static window procedure for pseudo console windows
// - Processes set-up on create to stow the "this" pointer to specific instantiations and routes
Expand Down Expand Up @@ -446,7 +479,7 @@ using namespace Microsoft::Console::Interactivity;
{
_WritePseudoWindowCallback(false);
}
break;
return 0;
}
// case WM_WINDOWPOSCHANGING:
// As long as user32 didn't eat the `ShowWindow` call because the window state requested
Expand Down Expand Up @@ -479,12 +512,12 @@ using namespace Microsoft::Console::Interactivity;
}
case WM_ACTIVATE:
{
if (const auto ownerHwnd{ ::GetAncestor(hWnd, GA_ROOTOWNER) })
if (const auto ownerHwnd = _owner.load(std::memory_order_relaxed))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The benefit of this change is that we now assign focus to the exact HWND that we were told is our owning window and not any window up the GA_ROOTOWNER chain.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this is great

{
SetFocus(ownerHwnd);
return 0;
}
break;
return 0;
}
}
// If we get this far, call the default window proc
Expand All @@ -501,6 +534,11 @@ using namespace Microsoft::Console::Interactivity;
// - <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
Expand Down
7 changes: 6 additions & 1 deletion src/interactivity/base/InteractivityFactory.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ namespace Microsoft::Console::Interactivity
[[nodiscard]] NTSTATUS CreateAccessibilityNotifier(_Inout_ std::unique_ptr<IAccessibilityNotifier>& notifier);
[[nodiscard]] NTSTATUS CreateSystemConfigurationProvider(_Inout_ std::unique_ptr<ISystemConfigurationProvider>& provider);

[[nodiscard]] NTSTATUS CreatePseudoWindow(HWND& hwnd, const HWND owner);
[[nodiscard]] NTSTATUS CreatePseudoWindow(HWND& hwnd);

void SetOwner(HWND owner) noexcept;
void SetVisibility(const bool isVisible) noexcept;

// Wndproc
[[nodiscard]] static LRESULT CALLBACK s_PseudoWindowProc(_In_ HWND hwnd,
Expand All @@ -43,6 +46,8 @@ namespace Microsoft::Console::Interactivity
void _WritePseudoWindowCallback(bool showOrHide);

HWND _pseudoConsoleWindowHwnd{ nullptr };
std::atomic<HWND> _owner{ HWND_DESKTOP };
std::atomic<bool> _suppressVisibilityChange{ false };
WRL::ComPtr<PseudoConsoleWindowAccessibilityProvider> _pPseudoConsoleUiaProvider{ nullptr };
};
}
Loading
Loading