Repository: electron/electron
Pull request: fix: outdated execution path for COM activation

#### Description of Change

I identified a problem with missing icons on Windows. Electron PR [/pull/48132](https://github.com/electron/electron/pull/48132) introduced COM activation. COM activation requires the ToastCLSID to be stored in a shortcut in the start menu. The PR enforces that shortcut creation. see [EnsureShortcut()](https://github.com/electron/electron/blob/1d038bf279beae52ac0da5539b9adf7c8a8b18ed/shell/browser/notifications/win/windows_toast_activator.cc#L152) 
Unfortunately this is not compatible with Squirrel auto-updating. The shortcut in `C:\Users\<user>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs` is created with the current executable. E.g. `C:\Users\<user>\AppData\Local\my-app\app-1.0.0\myapp.exe` An update will kick in and the new version is `C:\Users\<user>\AppData\Local\my-app\app-2.0.0\mayapp.exe` The shortcut will never be updated but `C:\Users\<user>\AppData\Local\my-app\app-1.0.0\mayapp.exe`  will be deleted when the next update comes in. Now the shortcut is dead and eventually Explorers icon cashing catches up to the missing exe and the shortcut and the executable no longer shows an icon in the taskbar. 
<img width="92" height="47" alt="image" src="https://github.com/user-attachments/assets/4bceba90-fe0a-45bc-98e8-2dd100fd8148" />

This PR should fix this.
* detects changes in exe path and updates shortcut
* detects  Stub / launcher layout: if the running binary lives under a versioned folder but a stub exists one directory up with the same filename (Squirrel.Windows & and wix-msi), the shortcut targets the stub so the path stays stable across updates and pinned shortcuts don't break.
* detects app folder version changes and updates working directories 

 However there are still some fundamental problems that are not addressed but would need some rethinking and API changes.
1. Apps might want to manage there own shortcuts in start menu. Squirrel packager for example creates shortcuts. Now we have duplicated shortcuts.
2. A more rare edge case is two apps have the same name and would fight with each other over the same shortcut.



<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Contributors guide: https://github.com/electron/electron/blob/main/CONTRIBUTING.md

NOTE: PRS submitted without this template will be automatically closed.
-->

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [X] PR description included
- [X] I have built and tested this PR
- [ ] `npm test` passes
- [ ] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [ ] relevant API documentation, tutorials, and examples are updated and follow the [documentation style guide](https://github.com/electron/electron/blob/main/docs/development/style-guide.md)
- [ ] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Fixed an issue where an app shortcut may lose its icon after auto-updating on Windows.


Changed files:
filename: shell/browser/notifications/win/windows_toast_activator.cc
patch: @@ -133,6 +133,21 @@ std::wstring GetExecutablePath() {
   return std::wstring(path, len);
 }
 
+// Installers sometimes put the running app in a versioned subfolder and ship a
+// stub with the same filename one directory up. Point the Start Menu shortcut
+// at the stub when it exists so toast activation and updates keep a stable
+// launch path.
+std::wstring GetShortcutTargetPath(const std::wstring& exe_path) {
+  if (exe_path.empty())
+    return L"";
+  base::FilePath exe_fp(exe_path);
+  base::FilePath stub_candidate =
+      exe_fp.DirName().DirName().Append(exe_fp.BaseName());
+  if (base::PathExists(stub_candidate))
+    return stub_candidate.value();
+  return exe_path;
+}
+
 void EnsureCLSIDRegistry() {
   std::wstring exe = GetExecutablePath();
   if (exe.empty())
@@ -153,7 +168,10 @@ void EnsureCLSIDRegistry() {
   server_key.WriteValue(nullptr, exe.c_str());
 }
 
-bool ExistingShortcutValid(const base::FilePath& lnk_path, PCWSTR aumid) {
+bool ExistingShortcutValid(const base::FilePath& lnk_path,
+                           PCWSTR aumid,
+                           const std::wstring& expected_target_path,
+                           const std::wstring& expected_working_dir) {
   if (!base::PathExists(lnk_path))
     return false;
   Microsoft::WRL::ComPtr<IShellLink> existing;
@@ -165,6 +183,31 @@ bool ExistingShortcutValid(const base::FilePath& lnk_path, PCWSTR aumid) {
       FAILED(pf->Load(lnk_path.value().c_str(), STGM_READ))) {
     return false;
   }
+
+  // After an auto-update the .lnk may still have the correct AUMID/CLSID but
+  // point at an old install path; treat that as invalid so we rewrite it.
+  wchar_t target_path[MAX_PATH];
+  if (FAILED(existing->GetPath(target_path, MAX_PATH, nullptr, SLGP_RAWPATH)))
+    return false;
+  if (base::FilePath::CompareIgnoreCase(
+          base::FilePath(expected_target_path).value(),
+          base::FilePath(target_path).value()) != 0) {
+    return false;
+  }
+
+  wchar_t work_dir[MAX_PATH];
+  work_dir[0] = L'\0';
+  if (FAILED(existing->GetWorkingDirectory(work_dir, MAX_PATH)))
+    return false;
+  base::FilePath expected_cwd =
+      base::FilePath(expected_working_dir).NormalizePathSeparators();
+  base::FilePath actual_cwd =
+      base::FilePath(work_dir).NormalizePathSeparators();
+  if (base::FilePath::CompareIgnoreCase(expected_cwd.value(),
+                                        actual_cwd.value()) != 0) {
+    return false;
+  }
+
   Microsoft::WRL::ComPtr<IPropertyStore> store;
   if (FAILED(existing.As(&store)))
     return false;
@@ -194,6 +237,7 @@ void EnsureShortcut() {
   std::wstring exe = GetExecutablePath();
   if (exe.empty())
     return;
+  std::wstring shortcut_target = GetShortcutTargetPath(exe);
 
   PWSTR programs_path = nullptr;
   if (FAILED(
@@ -232,18 +276,20 @@ void EnsureShortcut() {
     }
   }
 
-  if (ExistingShortcutValid(lnk_path, aumid))
+  const std::wstring expected_working_dir =
+      base::FilePath(exe).DirName().value();
+  if (ExistingShortcutValid(lnk_path, aumid, shortcut_target,
+                            expected_working_dir))
     return;
 
   Microsoft::WRL::ComPtr<IShellLink> shell_link;
   if (FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
                               IID_PPV_ARGS(&shell_link))))
     return;
-  shell_link->SetPath(exe.c_str());
+  shell_link->SetPath(shortcut_target.c_str());
   shell_link->SetArguments(L"");
   shell_link->SetDescription(product_name.c_str());
-  shell_link->SetWorkingDirectory(
-      base::FilePath(exe).DirName().value().c_str());
+  shell_link->SetWorkingDirectory(expected_working_dir.c_str());
 
   Microsoft::WRL::ComPtr<IPropertyStore> prop_store;
   if (SUCCEEDED(shell_link.As(&prop_store))) {