From 0d23624fa9d620b13155f491c8b1c5d91dbf0ba4 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Tue, 12 Aug 2025 19:09:50 -0500 Subject: [PATCH] Use a new API to propagate foreground state to child processes (#19192) Windows 11 uses some additional signals to determine what the user cares about and give it a bit of a QoS boost. One of those signals is whether it is associated with a window that is in the foreground or which has input focus. Association today takes two forms: - Process has a window which is in the foreground or which has input focus - Process has a *parent* that meets the above criterion. Console applications that are spawned "inside" terminal by handoff do not fall into either bucket. They don't have a window. Their parent is `dllhost` or `explorer`, who is definitely not in focus. We are piloting a new API that allows us to associate those processes with Terminal's window. When Terminal is in focus, it will attach every process from the active tab to its QoS group. This means that whatever is running in that tab is put into the "foreground" bucket, and everything running in other background tabs is not. When Terminal is out of focus, it attaches every process to its QoS group. This ensures that they all go into the "background" bucket together, following the window. --- dep/nuget/packages.config | 2 +- .../TerminalApp.LocalTests.vcxproj | 1 + src/cascadia/TerminalApp/TabManagement.cpp | 2 + .../TerminalApp/TerminalAppLib.vcxproj | 1 + src/cascadia/TerminalApp/TerminalPage.cpp | 111 +++++++++++++++++- src/cascadia/TerminalApp/TerminalPage.h | 9 +- .../TerminalApp/dll/TerminalApp.vcxproj | 1 + .../TerminalConnection/ConptyConnection.cpp | 16 ++- .../TerminalConnection/ConptyConnection.h | 1 + .../TerminalConnection/ConptyConnection.idl | 2 + src/common.nugetversions.targets | 4 +- src/host/srvinit.cpp | 2 +- 12 files changed, 143 insertions(+), 9 deletions(-) diff --git a/dep/nuget/packages.config b/dep/nuget/packages.config index a641da7c51..f28aa77888 100644 --- a/dep/nuget/packages.config +++ b/dep/nuget/packages.config @@ -5,7 +5,7 @@ - + diff --git a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj index 91134e3f08..8ab41a6eca 100644 --- a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj +++ b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj @@ -24,6 +24,7 @@ true + true diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 9fbf191b1f..5b1a1b9c11 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -1009,6 +1009,8 @@ namespace winrt::TerminalApp::implementation auto profile = tabImpl->GetFocusedProfile(); _UpdateBackground(profile); } + + _adjustProcessPriorityThrottled->Run(); } CATCH_LOG(); } diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index 3be78cbce0..3d8a82af2a 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj @@ -26,6 +26,7 @@ true true true + true diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 507d29392b..fbbc8a4076 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -6,6 +6,7 @@ #include "TerminalPage.h" #include +#include #include #include @@ -217,6 +218,17 @@ namespace winrt::TerminalApp::implementation // before restoring previous tabs in that scenario. } } + + _adjustProcessPriorityThrottled = std::make_shared>( + DispatcherQueue::GetForCurrentThread(), + til::throttled_func_options{ + .delay = std::chrono::milliseconds{ 100 }, + .debounce = true, + .trailing = true, + }, + [=]() { + _adjustProcessPriority(); + }); _hostingHwnd = hwnd; return S_OK; } @@ -2076,7 +2088,7 @@ namespace winrt::TerminalApp::implementation return false; } - TermControl TerminalPage::_GetActiveControl() + TermControl TerminalPage::_GetActiveControl() const { if (const auto tabImpl{ _GetFocusedTabImpl() }) { @@ -2537,6 +2549,8 @@ namespace winrt::TerminalApp::implementation auto profile = tab->GetFocusedProfile(); _UpdateBackground(profile); } + + _adjustProcessPriorityThrottled->Run(); } uint32_t TerminalPage::NumberOfTabs() const @@ -4669,9 +4683,12 @@ namespace winrt::TerminalApp::implementation if (const auto coreState{ sender.try_as() }) { const auto newConnectionState = coreState.ConnectionState(); + co_await wil::resume_foreground(Dispatcher()); + + _adjustProcessPriorityThrottled->Run(); + if (newConnectionState == ConnectionState::Failed && !_IsMessageDismissed(InfoBarMessage::CloseOnExitInfo)) { - co_await wil::resume_foreground(Dispatcher()); if (const auto infoBar = FindName(L"CloseOnExitInfoBar").try_as()) { infoBar.IsOpen(true); @@ -4936,6 +4953,94 @@ namespace winrt::TerminalApp::implementation } } + void TerminalPage::_adjustProcessPriority() const + { + // Windowing is single-threaded, so this will not cause a race condition. + static bool supported{ true }; + + if (!supported || !_hostingHwnd.has_value()) + { + return; + } + + std::array processes; + auto it = processes.begin(); + const auto end = processes.end(); + + auto&& appendFromControl = [&](auto&& control) { + if (it == end) + { + return; + } + if (control) + { + if (const auto conn{ control.Connection() }) + { + if (const auto pty{ conn.try_as() }) + { + if (const uint64_t process{ pty.RootProcessHandle() }; process != 0) + { + *it++ = reinterpret_cast(process); + } + } + } + } + }; + + auto&& appendFromTab = [&](auto&& tabImpl) { + if (const auto pane{ tabImpl->GetRootPane() }) + { + pane->WalkTree([&](auto&& child) { + if (const auto& control{ child->GetTerminalControl() }) + { + appendFromControl(control); + } + }); + } + }; + + if (!_activated) + { + // When a window is out of focus, we want to attach all of the processes + // under it to the window so they all go into the background at the same time. + for (auto&& tab : _tabs) + { + if (auto tabImpl{ _GetTabImpl(tab) }) + { + appendFromTab(tabImpl); + } + } + } + else + { + // When a window is in focus, propagate our foreground boost (if we have one) + // to current all panes in the current tab. + if (auto tabImpl{ _GetFocusedTabImpl() }) + { + appendFromTab(tabImpl); + } + } + + const auto count{ gsl::narrow_cast(it - processes.begin()) }; + const auto hr = TerminalTrySetWindowAssociatedProcesses(_hostingHwnd.value(), count, count ? processes.data() : nullptr); + if (S_FALSE == hr) + { + // Don't bother trying again or logging. The wrapper tells us it's unsupported. + supported = false; + return; + } + + TraceLoggingWrite( + g_hTerminalAppProvider, + "CalledNewQoSAPI", + TraceLoggingValue(reinterpret_cast(_hostingHwnd.value()), "hwnd"), + TraceLoggingValue(count), + TraceLoggingHResult(hr)); +#ifdef _DEBUG + OutputDebugStringW(fmt::format(FMT_COMPILE(L"Submitted {} processes to TerminalTrySetWindowAssociatedProcesses; return=0x{:08x}\n"), count, hr).c_str()); +#endif + } + void TerminalPage::WindowActivated(const bool activated) { // Stash if we're activated. Use that when we reload @@ -4943,6 +5048,8 @@ namespace winrt::TerminalApp::implementation _activated = activated; _updateThemeColors(); + _adjustProcessPriorityThrottled->Run(); + if (const auto& tab{ _GetFocusedTabImpl() }) { if (tab->TabStatus().IsInputBroadcastActive()) diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 36ecec4fd2..d095f008dc 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -3,6 +3,8 @@ #pragma once +#include + #include "TerminalPage.g.h" #include "Tab.h" #include "AppKeyBindings.h" @@ -359,8 +361,11 @@ namespace winrt::TerminalApp::implementation bool _MovePane(const Microsoft::Terminal::Settings::Model::MovePaneArgs args); bool _MoveTab(winrt::com_ptr tab, const Microsoft::Terminal::Settings::Model::MoveTabArgs args); + std::shared_ptr> _adjustProcessPriorityThrottled; + void _adjustProcessPriority() const; + template - bool _ApplyToActiveControls(F f) + bool _ApplyToActiveControls(F f) const { if (const auto tab{ _GetFocusedTabImpl() }) { @@ -379,7 +384,7 @@ namespace winrt::TerminalApp::implementation return false; } - winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl(); + winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl() const; std::optional _GetFocusedTabIndex() const noexcept; std::optional _GetTabIndex(const TerminalApp::Tab& tab) const noexcept; TerminalApp::Tab _GetFocusedTab() const noexcept; diff --git a/src/cascadia/TerminalApp/dll/TerminalApp.vcxproj b/src/cascadia/TerminalApp/dll/TerminalApp.vcxproj index 282f68a589..69ccb921f8 100644 --- a/src/cascadia/TerminalApp/dll/TerminalApp.vcxproj +++ b/src/cascadia/TerminalApp/dll/TerminalApp.vcxproj @@ -16,6 +16,7 @@ true true + true diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp index f511ff8afa..ba14ee618c 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.cpp +++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp @@ -340,7 +340,13 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation auto ownedSignal = duplicateHandle(signal); auto ownedReference = duplicateHandle(reference); auto ownedServer = duplicateHandle(server); - auto ownedClient = duplicateHandle(client); + wil::unique_hfile ownedClient; + LOG_IF_WIN32_BOOL_FALSE(DuplicateHandle(GetCurrentProcess(), client, GetCurrentProcess(), ownedClient.addressof(), PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_SET_INFORMATION | SYNCHRONIZE, FALSE, 0)); + if (!ownedClient) + { + // If we couldn't reopen the handle with SET_INFORMATION, which may be required to do things like QoS management, fall back. + ownedClient = duplicateHandle(client); + } THROW_IF_FAILED(ConptyPackPseudoConsole(ownedServer.get(), ownedReference.get(), ownedSignal.get(), &_hPC)); ownedServer.release(); @@ -533,6 +539,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation DWORD exitCode{ 0 }; GetExitCodeProcess(_piClient.hProcess, &exitCode); + _piClient.reset(); + // Signal the closing or failure of the process. // exitCode might be STILL_ACTIVE if a client has called FreeConsole() and // thus caused the tab to close, even though the CLI app is still running. @@ -649,6 +657,12 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation } } + uint64_t ConptyConnection::RootProcessHandle() noexcept + { +#pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1). + return reinterpret_cast(_piClient.hProcess); + } + void ConptyConnection::Close() noexcept try { diff --git a/src/cascadia/TerminalConnection/ConptyConnection.h b/src/cascadia/TerminalConnection/ConptyConnection.h index cdcd99981f..f54c92e547 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.h +++ b/src/cascadia/TerminalConnection/ConptyConnection.h @@ -30,6 +30,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation void ShowHide(const bool show); void ReparentWindow(const uint64_t newParent); + uint64_t RootProcessHandle() noexcept; winrt::hstring Commandline() const; winrt::hstring StartingTitle() const; diff --git a/src/cascadia/TerminalConnection/ConptyConnection.idl b/src/cascadia/TerminalConnection/ConptyConnection.idl index 5264238c79..f0f074b01d 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.idl +++ b/src/cascadia/TerminalConnection/ConptyConnection.idl @@ -21,6 +21,8 @@ namespace Microsoft.Terminal.TerminalConnection void ReparentWindow(UInt64 newParent); + UInt64 RootProcessHandle(); + static event NewConnectionHandler NewConnection; static void StartInboundListener(); diff --git a/src/common.nugetversions.targets b/src/common.nugetversions.targets index 837c575fd8..fc2ee146e7 100644 --- a/src/common.nugetversions.targets +++ b/src/common.nugetversions.targets @@ -47,7 +47,7 @@ - + @@ -85,7 +85,7 @@ - + diff --git a/src/host/srvinit.cpp b/src/host/srvinit.cpp index cd68c921e9..49551a25bd 100644 --- a/src/host/srvinit.cpp +++ b/src/host/srvinit.cpp @@ -463,7 +463,7 @@ try TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); - wil::unique_handle clientProcess{ OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | SYNCHRONIZE, TRUE, static_cast(connectMessage->Descriptor.Process)) }; + wil::unique_handle clientProcess{ OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_SET_INFORMATION | SYNCHRONIZE, TRUE, static_cast(connectMessage->Descriptor.Process)) }; RETURN_LAST_ERROR_IF_NULL(clientProcess.get()); TraceLoggingWrite(g_hConhostV2EventTraceProvider,