[Unpackaged] Fix taskbar glomming due to AUMID (#20064)

The bug was caused by an AUMID mismatch between the Taskbar's .lnk file
and Windows Terminal. Since no AUMID was associated with the .exe, the
OS automatically creates one for us. However, #20018 added an AUMID for
unpackaged scenarios, so now there was a mismatch, resulting in a new
taskbar entry being created.

To fix this, we check if a .lnk points to our .exe in the taskbar.
There's 3 cases here:
1. no .lnk of interest exists --> set the AUMID normally
2. the .lnk carries our AUMID --> set the AUMID normally
3. the .lnk doesn't have an AUMID (aka uses the auto-resolved one) -->
- for this launch: don't set the AUMID so that we both use the
auto-resolved one
- on next launch: set the AUMID on the .lnk and process so that they all
agree

## Validation Steps Performed
In unpackaged folder, move WindowsTerminal.exe to the taskbar (creates
.lnk)...
 Double-click the .exe --> Same taskbar entry is used
 Double-click the .exe _again_ --> second window goes to same taskbar
entry

The first window doesn't have to close for this to work. It just * works
*!

Bug introduced in #20018 
Closes #20053
This commit is contained in:
Carlos Zamora
2026-05-01 14:56:19 -07:00
committed by GitHub
parent e4e3f08efc
commit 059986ebce
2 changed files with 130 additions and 1 deletions

View File

@@ -11,6 +11,8 @@
#include <wil/token_helpers.h>
#include <winrt/TerminalApp.h>
#include <sddl.h>
#include <propkey.h>
#include <propvarutil.h>
#include "AppHost.h"
#include "resource.h"
@@ -313,6 +315,107 @@ AppHost* WindowEmperor::_mostRecentWindow() const noexcept
return mostRecent;
}
// GH#20053: The shell resolves taskbar grouping identity as: per-window AUMID >
// per-process AUMID > auto-derived from exe path. Before we started setting a
// process AUMID, both the pinned .lnk and the process used auto-derived
// identity, so they matched. Now that we set an explicit AUMID, a pinned .lnk
// that predates the AUMID change has no AUMID and still uses auto-derived
// identity, causing a mismatch and a duplicate taskbar button.
//
// To fix this, we check if a pinned taskbar shortcut (.lnk) points to our exe.
// If it already carries our AUMID (or no pin exists), we set the process AUMID
// normally. If a pin exists WITHOUT our AUMID, we skip setting the process
// AUMID for THIS launch (both sides use auto-derived identity, so they match)
// and defer stamping the shortcut to process exit. On the next launch, the pin
// has our AUMID, so we set the process AUMID to match, and both agree.
//
// NOTE: On the first launch after pinning, the process AUMID is not set. If
// toast notifications are needed in the future, use
// ToastNotificationManager::CreateToastNotifier(aumid) with the AUMID string
// directly. That API does not depend on SetCurrentProcessExplicitAppUserModelID.
// A Start Menu shortcut with the AUMID (separate from the taskbar pin) is also
// required for toast routing; see
// https://learn.microsoft.com/windows/apps/develop/notifications/app-notifications/send-local-toast-other-apps
void WindowEmperor::_setupAumid(const std::wstring& aumid)
{
const auto ourExePath = wil::GetModuleFileNameW<std::wstring>(nullptr);
bool needsDeferredStamping = false;
std::wstring pinnedLnkPath;
const auto taskbarGlob = wil::ExpandEnvironmentStringsW<std::wstring>(
LR"(%APPDATA%\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\*.lnk)");
WIN32_FIND_DATAW findData{};
const wil::unique_hfind findHandle{ FindFirstFileExW(taskbarGlob.c_str(), FindExInfoBasic, &findData, FindExSearchNameMatch, nullptr, FIND_FIRST_EX_LARGE_FETCH) };
if (findHandle)
{
const auto lastSlash = taskbarGlob.rfind(L'\\');
const auto taskbarDir = taskbarGlob.substr(0, lastSlash + 1);
do
{
const auto lnkPath = taskbarDir + findData.cFileName;
wil::com_ptr<IShellLinkW> shellLink;
if (FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink))))
{
continue;
}
const auto persistFile = shellLink.try_query<IPersistFile>();
if (!persistFile || FAILED(persistFile->Load(lnkPath.c_str(), STGM_READ)))
{
continue;
}
wchar_t targetPath[MAX_PATH]{};
if (FAILED(shellLink->GetPath(targetPath, MAX_PATH, nullptr, SLGP_RAWPATH)))
{
continue;
}
if (til::compare_ordinal_insensitive(targetPath, ourExePath) != 0)
{
continue;
}
// Found a pin pointing to us. Assume it needs stamping unless
// we confirm it already has our AUMID.
pinnedLnkPath = lnkPath;
needsDeferredStamping = true;
if (const auto propertyStore = shellLink.try_query<IPropertyStore>())
{
wil::unique_prop_variant pv;
if (SUCCEEDED(propertyStore->GetValue(PKEY_AppUserModel_ID, &pv)) &&
pv.vt == VT_LPWSTR && pv.pwszVal &&
aumid == pv.pwszVal)
{
needsDeferredStamping = false;
}
}
break;
} while (FindNextFileW(findHandle.get(), &findData));
}
if (needsDeferredStamping)
{
// The pin exists but doesn't have our AUMID yet. Don't set the process
// AUMID or stamp the shortcut now. Writing the shortcut causes the
// shell to re-read it immediately, changing the pin's cached identity
// mid-launch and creating a mismatch in the opposite direction. Instead,
// stamp it at shutdown when the taskbar association no longer matters.
_pendingAumidLnkPath = std::move(pinnedLnkPath);
_pendingAumid = aumid;
}
else
{
LOG_IF_FAILED(SetCurrentProcessExplicitAppUserModelID(aumid.c_str()));
}
}
void WindowEmperor::HandleCommandlineArgs(int nCmdShow)
{
// When running without package identity, set an explicit AppUserModelID so
@@ -373,7 +476,7 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow)
#else
fmt::format_to(std::back_inserter(unpackagedAumid), FMT_COMPILE(L".{:08x}"), hash);
#endif
LOG_IF_FAILED(SetCurrentProcessExplicitAppUserModelID(unpackagedAumid.c_str()));
_setupAumid(unpackagedAumid);
}
}
@@ -553,6 +656,29 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow)
Shell_NotifyIconW(NIM_DELETE, &_notificationIcon);
}
// GH#20053: Deferred shortcut stamping. See _setupAumid() for context.
if (!_pendingAumidLnkPath.empty())
{
wil::com_ptr<IShellLinkW> shellLink;
if (SUCCEEDED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink))))
{
if (const auto persistFile = shellLink.try_query<IPersistFile>();
persistFile && SUCCEEDED(persistFile->Load(_pendingAumidLnkPath.c_str(), STGM_READWRITE)))
{
if (const auto propertyStore = shellLink.try_query<IPropertyStore>())
{
wil::unique_prop_variant pv;
if (SUCCEEDED(InitPropVariantFromString(_pendingAumid.c_str(), &pv)) &&
SUCCEEDED(propertyStore->SetValue(PKEY_AppUserModel_ID, pv)) &&
SUCCEEDED(propertyStore->Commit()))
{
persistFile->Save(_pendingAumidLnkPath.c_str(), TRUE);
}
}
}
}
}
// There's a mysterious crash in XAML on Windows 10 if you just let _app get destroyed (GH#15410).
// We also need to ensure that all UI threads exit before WindowEmperor leaves the scope on the main thread (MSFT:46744208).
// Both problems can be solved and the shutdown accelerated by using TerminateProcess.

View File

@@ -69,6 +69,7 @@ private:
void _persistState(const winrt::Microsoft::Terminal::Settings::Model::ApplicationState& state) const;
void _finalizeSessionPersistence() const;
void _checkWindowsForNotificationIcon();
void _setupAumid(const std::wstring& aumid);
wil::unique_hwnd _window;
winrt::TerminalApp::App _app{ nullptr };
@@ -84,6 +85,8 @@ private:
std::optional<bool> _currentSystemThemeIsDark;
int32_t _windowCount = 0;
int32_t _messageBoxCount = 0;
std::wstring _pendingAumidLnkPath;
std::wstring _pendingAumid;
#if 0 // #ifdef NDEBUG
static constexpr void _assertIsMainThread() noexcept