|
11 | 11 | #include <wil/token_helpers.h> |
12 | 12 | #include <winrt/TerminalApp.h> |
13 | 13 | #include <sddl.h> |
| 14 | +#include <propkey.h> |
| 15 | +#include <propvarutil.h> |
14 | 16 |
|
15 | 17 | #include "AppHost.h" |
16 | 18 | #include "resource.h" |
@@ -313,6 +315,107 @@ AppHost* WindowEmperor::_mostRecentWindow() const noexcept |
313 | 315 | return mostRecent; |
314 | 316 | } |
315 | 317 |
|
| 318 | +// GH#20053: The shell resolves taskbar grouping identity as: per-window AUMID > |
| 319 | +// per-process AUMID > auto-derived from exe path. Before we started setting a |
| 320 | +// process AUMID, both the pinned .lnk and the process used auto-derived |
| 321 | +// identity, so they matched. Now that we set an explicit AUMID, a pinned .lnk |
| 322 | +// that predates the AUMID change has no AUMID and still uses auto-derived |
| 323 | +// identity, causing a mismatch and a duplicate taskbar button. |
| 324 | +// |
| 325 | +// To fix this, we check if a pinned taskbar shortcut (.lnk) points to our exe. |
| 326 | +// If it already carries our AUMID (or no pin exists), we set the process AUMID |
| 327 | +// normally. If a pin exists WITHOUT our AUMID, we skip setting the process |
| 328 | +// AUMID for THIS launch (both sides use auto-derived identity, so they match) |
| 329 | +// and defer stamping the shortcut to process exit. On the next launch, the pin |
| 330 | +// has our AUMID, so we set the process AUMID to match, and both agree. |
| 331 | +// |
| 332 | +// NOTE: On the first launch after pinning, the process AUMID is not set. If |
| 333 | +// toast notifications are needed in the future, use |
| 334 | +// ToastNotificationManager::CreateToastNotifier(aumid) with the AUMID string |
| 335 | +// directly. That API does not depend on SetCurrentProcessExplicitAppUserModelID. |
| 336 | +// A Start Menu shortcut with the AUMID (separate from the taskbar pin) is also |
| 337 | +// required for toast routing; see |
| 338 | +// https://learn.microsoft.com/windows/apps/develop/notifications/app-notifications/send-local-toast-other-apps |
| 339 | +void WindowEmperor::_setupAumid(const std::wstring& aumid) |
| 340 | +{ |
| 341 | + const auto ourExePath = wil::GetModuleFileNameW<std::wstring>(nullptr); |
| 342 | + |
| 343 | + bool needsDeferredStamping = false; |
| 344 | + std::wstring pinnedLnkPath; |
| 345 | + |
| 346 | + const auto taskbarGlob = wil::ExpandEnvironmentStringsW<std::wstring>( |
| 347 | + LR"(%APPDATA%\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\*.lnk)"); |
| 348 | + |
| 349 | + WIN32_FIND_DATAW findData{}; |
| 350 | + const wil::unique_hfind findHandle{ FindFirstFileW(taskbarGlob.c_str(), &findData) }; |
| 351 | + if (findHandle) |
| 352 | + { |
| 353 | + const auto lastSlash = taskbarGlob.rfind(L'\\'); |
| 354 | + const auto taskbarDir = taskbarGlob.substr(0, lastSlash + 1); |
| 355 | + |
| 356 | + do |
| 357 | + { |
| 358 | + const auto lnkPath = taskbarDir + findData.cFileName; |
| 359 | + |
| 360 | + wil::com_ptr<IShellLinkW> shellLink; |
| 361 | + if (FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)))) |
| 362 | + { |
| 363 | + continue; |
| 364 | + } |
| 365 | + |
| 366 | + const auto persistFile = shellLink.try_query<IPersistFile>(); |
| 367 | + if (!persistFile || FAILED(persistFile->Load(lnkPath.c_str(), STGM_READ))) |
| 368 | + { |
| 369 | + continue; |
| 370 | + } |
| 371 | + |
| 372 | + wchar_t targetPath[MAX_PATH]{}; |
| 373 | + if (FAILED(shellLink->GetPath(targetPath, MAX_PATH, nullptr, SLGP_RAWPATH))) |
| 374 | + { |
| 375 | + continue; |
| 376 | + } |
| 377 | + |
| 378 | + if (_wcsicmp(targetPath, ourExePath.c_str()) != 0) |
| 379 | + { |
| 380 | + continue; |
| 381 | + } |
| 382 | + |
| 383 | + // Found a pin pointing to us. Assume it needs stamping unless |
| 384 | + // we confirm it already has our AUMID. |
| 385 | + pinnedLnkPath = lnkPath; |
| 386 | + needsDeferredStamping = true; |
| 387 | + |
| 388 | + if (const auto propertyStore = shellLink.try_query<IPropertyStore>()) |
| 389 | + { |
| 390 | + wil::unique_prop_variant pv; |
| 391 | + if (SUCCEEDED(propertyStore->GetValue(PKEY_AppUserModel_ID, &pv)) && |
| 392 | + pv.vt == VT_LPWSTR && pv.pwszVal && |
| 393 | + aumid == pv.pwszVal) |
| 394 | + { |
| 395 | + needsDeferredStamping = false; |
| 396 | + } |
| 397 | + } |
| 398 | + |
| 399 | + break; |
| 400 | + } while (FindNextFileW(findHandle.get(), &findData)); |
| 401 | + } |
| 402 | + |
| 403 | + if (needsDeferredStamping) |
| 404 | + { |
| 405 | + // The pin exists but doesn't have our AUMID yet. Don't set the process |
| 406 | + // AUMID or stamp the shortcut now. Writing the shortcut causes the |
| 407 | + // shell to re-read it immediately, changing the pin's cached identity |
| 408 | + // mid-launch and creating a mismatch in the opposite direction. Instead, |
| 409 | + // stamp it at shutdown when the taskbar association no longer matters. |
| 410 | + _pendingAumidLnkPath = std::move(pinnedLnkPath); |
| 411 | + _pendingAumid = aumid; |
| 412 | + } |
| 413 | + else |
| 414 | + { |
| 415 | + LOG_IF_FAILED(SetCurrentProcessExplicitAppUserModelID(aumid.c_str())); |
| 416 | + } |
| 417 | +} |
| 418 | + |
316 | 419 | void WindowEmperor::HandleCommandlineArgs(int nCmdShow) |
317 | 420 | { |
318 | 421 | // When running without package identity, set an explicit AppUserModelID so |
@@ -373,7 +476,7 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow) |
373 | 476 | #else |
374 | 477 | fmt::format_to(std::back_inserter(unpackagedAumid), FMT_COMPILE(L".{:08x}"), hash); |
375 | 478 | #endif |
376 | | - LOG_IF_FAILED(SetCurrentProcessExplicitAppUserModelID(unpackagedAumid.c_str())); |
| 479 | + _setupAumid(unpackagedAumid); |
377 | 480 | } |
378 | 481 | } |
379 | 482 |
|
@@ -553,6 +656,29 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow) |
553 | 656 | Shell_NotifyIconW(NIM_DELETE, &_notificationIcon); |
554 | 657 | } |
555 | 658 |
|
| 659 | + // GH#20053: Deferred shortcut stamping. See _setupAumid() for context. |
| 660 | + if (!_pendingAumidLnkPath.empty()) |
| 661 | + { |
| 662 | + wil::com_ptr<IShellLinkW> shellLink; |
| 663 | + if (SUCCEEDED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)))) |
| 664 | + { |
| 665 | + if (const auto persistFile = shellLink.try_query<IPersistFile>(); |
| 666 | + persistFile && SUCCEEDED(persistFile->Load(_pendingAumidLnkPath.c_str(), STGM_READWRITE))) |
| 667 | + { |
| 668 | + if (const auto propertyStore = shellLink.try_query<IPropertyStore>()) |
| 669 | + { |
| 670 | + wil::unique_prop_variant pv; |
| 671 | + if (SUCCEEDED(InitPropVariantFromString(_pendingAumid.c_str(), &pv)) && |
| 672 | + SUCCEEDED(propertyStore->SetValue(PKEY_AppUserModel_ID, pv)) && |
| 673 | + SUCCEEDED(propertyStore->Commit())) |
| 674 | + { |
| 675 | + persistFile->Save(_pendingAumidLnkPath.c_str(), TRUE); |
| 676 | + } |
| 677 | + } |
| 678 | + } |
| 679 | + } |
| 680 | + } |
| 681 | + |
556 | 682 | // There's a mysterious crash in XAML on Windows 10 if you just let _app get destroyed (GH#15410). |
557 | 683 | // We also need to ensure that all UI threads exit before WindowEmperor leaves the scope on the main thread (MSFT:46744208). |
558 | 684 | // Both problems can be solved and the shutdown accelerated by using TerminateProcess. |
|
0 commit comments