Skip to content

fix(windows): heal leftover taskbar pins after MSI upgrades - #142

Merged
danielss-dev merged 8 commits into
mainfrom
developements/dan-81-taskbar-pin-heal-486a
Oct 8, 2026
Merged

danielss-dev merged 8 commits into
mainfrom
developements/dan-81-taskbar-pin-heal-486a

Conversation

@danielss-dev

@danielss-dev danielss-dev commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Linear: DAN-81

Follow-up to #135 / #139. Daniels’ pin was created before 1.7.3 and still shows Windows’ blank-page icon on 1.7.4, even while Strand is running.

Confirmed cause (hypothesis 1)

Inspected the published GitHub Release MSIs with msitools msiinfo export (not just CI logs):

MSI ApplicationStartMenuShortcut.Icon_ ApplicationDesktopShortcut.Icon_ System.AppUserModel.ID
Strand_1.7.2_x64_en-US.msi ProductIcon (empty) dev.danielss.strand
Strand_1.7.3_x64_en-US.msi (empty) (empty) dev.danielss.strand
Strand_1.7.4_x64_en-US.msi (empty) (empty) dev.danielss.strand

Each build has a new ProductCode ({43173C17-…} / {81DD7077-…} / {1ADF3E0D-…}) and the same UpgradeCode. A major upgrade uninstalls the previous product and deletes %WINDIR%\Installer\{old ProductCode}\….

Hypothesis 4 is false: 1.7.3 and 1.7.4 did ship the #139 template. The WiX change is not missing from the published artifacts.

Hypothesis 1 is true for Daniels: a pre-1.7.3 pin is a copy of the Start Menu shortcut (Explorer also copies that shortcut when pinning a running window by AUMID). Its IconLocation still points at the 1.7.2 ProductIcon cache file that 1.7.3/1.7.4 deleted. HWND WM_SETICON cannot win over that pinned .lnk — matching the screenshot of Strand running with a blank taskbar button.

Hypothesis 2: a running-window pin is expected to copy the Start Menu icon when the AUMID matches. The rehearsal creates both a Start Menu copy and a WScript .lnk whose IconLocation is copied from Start, which is how that shell path stores the icon.

Hypothesis 3 (Explorer icon cache): still a live-machine check. This PR rewrites only the .lnk and sends SHChangeNotify(UPDATEITEM|FLUSH). It does not delete IconCache databases or Taskband blobs.

Fix

Smallest diff on top of the already-correct WiX template:

  • windows_pin::heal_broken_taskbar_pins at RunEvent::Ready (after the existing HWND icon contract).
  • Selection (should_heal_pin): target is the installed strand.exe and icon path is missing or under %WINDIR%\Installer. Healthy exe-icon pins, other apps, and working custom icons are left alone. 8.3 short paths (RUNNER~1, INSTALL~1) are expanded with GetLongPathNameW before the prefix check.
  • Then IShellLinkW::SetIconLocation(strand.exe, 0) + IPersistFile::Save (loaded with STGM_READWRITE) + SHChangeNotify.
  • No MSI custom action (would run as the installer identity and walk other users’ profiles). Per-user pins are healed when that user launches Strand.
  • Add/Remove Programs ProductIcon, AUMID dev.danielss.strand, upgrade sequence, and MSIX/Store path are unchanged.

If Explorer still shows a blank bitmap after the .lnk is repaired, unpin + re-pin once remains the fallback (user guide + README).

Heal contract

  • Once per launch. RunEvent::Ready fires once per process, in the same run callback as HWND WM_SETICON. There is no timer, watcher, or second pass.
  • Cheap no-op when nothing matches. Empty User Pinned dirs return Ok(0) without writing. Each remaining .lnk is read; should_heal_pin false skips SetIconLocation/Save. Typical pin counts are a handful of files.
  • Never deletes or re-creates .lnk files. The existing file is loaded with STGM_READWRITE, SetIconLocation is applied, and IPersistFile::Save writes that same path. No DeleteFile, no new shortcut, no Taskband/IconCache edits. (STGM_READ + Save is access-denied and would leave pins untouched.) Rehearsal fingerprints: healed pins keep the same CreationTime; 1.7.3-era and unrelated pins keep the same sha256.
  • Errors never block startup. The Ready handler matches heal_broken_taskbar_pins(): Ok(0) is silent, Ok(n) is tracing::info, Err is tracing::warn. No unwrap/expect on the launch path. Per-pin COM failures are skipped with a warn and the rest still run.

Proof

Latest green head: 73e6cd1. All 6 PR checks succeeded, including Native agent review (Windows) and MSI upgrade pin rehearsal (run 37752839523).

  • cargo test -p strand-tauri windows_pin on windows-latest (rehearsal + native-review): COM round-trip + selection, including 8.3 expansion.

  • cargo clippy -p strand-tauri -- -D warnings on windows-latest in windows-msi-pin-rehearsal.yml.

  • After msiexec 1.7.2 → 1.7.3 → 1.7.4, heal_pins_in ran against the real User Pinned dir (STRAND_PIN_HEAL healed=2). Artifact: msi-pin-rehearsal.

    Before heal:

    • 1.7.2 start/running pins: IconLocation=C:\Windows\Installer\{43173C17-8D6E-4182-AD64-FD38EC263504}\ProductIcon,0 iconFileExists=False
    • 1.7.3 start/running pins: IconLocation=,0
    • unrelated notepad: IconLocation=C:\Windows\Installer\{DEADBEEF-…}\ProductIcon,0

    After heal:

    • 1.7.2 start/running pins: IconLocation=C:\Program Files\Strand\strand.exe,0 iconFileExists=True (CreationTime unchanged, sha256 changed)
    • 1.7.3 pins and notepad: IconLocation and sha256 unchanged
  • scripts/check-release-security.mjs still requires the HWND contract, empty shortcut Icon attrs, AUMID, and the heal symbols.

  • scripts/check-msi-shortcuts.ps1 is unchanged and still runs in release.yml and microsoft-store.yml.

A windows-latest runner cannot prove a live taskbar bitmap, pins created from a visible HWND, or icon-cache refresh with Strand running vs closed. It can prove IconLocation staleness, that 1.7.3+ pins keep a valid icon reference across 1.7.3 → 1.7.4, and that this PR’s heal rewrites only the broken Strand pins.

This PR does not rebuild an MSI: packaging/windows/main.wxs is unchanged from 1.7.4, so the PR MSI Shortcut table would match the published 1.7.4 table already dumped above. The new behavior is in the exe.

Remaining live checks on Daniels’ machine

CI cannot see Explorer’s pinned bitmap. After this build is launched once with the current blank pin still on the taskbar:

  1. Confirm the pin’s .lnk IconLocation is C:\Program Files\Strand\strand.exe,0 (or empty / exe icon) and that file exists.
  2. Confirm the visible Strand artwork on the pinned button while Strand is running, then after Strand is closed.
  3. If the .lnk is repaired but the bitmap stays blank, try an Explorer restart / sign-out — SHChangeNotify is best-effort and may not be enough for the live Taskbar cache. Unpin + pin from the running window once remains the documented fallback.

Do not merge, tag, or publish — Daniels merges.

Open in Web Open in Cursor 

cursoragent and others added 8 commits October 8, 2026 07:55
Pre-1.7.3 Start Menu shortcuts stored ProductIcon in the Windows Installer
cache, so pinned copies keep that path after a major upgrade deletes the file.

Rewrite only User Pinned .lnk files whose target is the installed strand.exe
and whose icon path is missing or under %WINDIR%\Installer. New 1.7.3+ pins
already use the executable icon and are left alone.

Co-authored-by: Daniels <danielss-dev@users.noreply.github.com>
Compile and clippy the COM heal in the MSI pin rehearsal job, then run
heal_pins_in against the real User Pinned directory after 1.7.2 → 1.7.3 →
1.7.4 so leftover ProductIcon pins are rewritten in place.

Co-authored-by: Daniels <danielss-dev@users.noreply.github.com>
IPersistFile::Save after a STGM_READ load is access-denied, so the COM
disk test rewrote zero pins. Load writable shortcuts before SetIconLocation.

Co-authored-by: Daniels <danielss-dev@users.noreply.github.com>
GetPath's signature needs WIN32_FIND_DATAW, which is feature-gated. Keep
STGM_READWRITE for in-place Save.

Co-authored-by: Daniels <danielss-dev@users.noreply.github.com>
IShellLink GetIconLocation returns RUNNER~1-style short paths, so a long
Installer cache root never matched. Expand existing parents with
GetLongPathNameW before the prefix check.

Co-authored-by: Daniels <danielss-dev@users.noreply.github.com>
Windows clippy -D warnings compiles decode_wsl_output; Linux CI does not.

Co-authored-by: Daniels <danielss-dev@users.noreply.github.com>
IShellLink keeps environment-variable icon paths (e.g.
%SystemRoot%\System32\shell32.dll) unexpanded, so the existence check
treated working custom icons as missing and overwrote them. Expand icon
and target paths with ExpandEnvironmentStringsW before selection, and add
COM regressions for an env-var custom icon (kept) and an env-var Installer
cache icon (healed).
@danielss-dev
danielss-dev marked this pull request as ready for review October 8, 2026 09:37
@danielss-dev
danielss-dev merged commit 5576f8d into main Oct 8, 2026
6 checks passed
@danielss-dev
danielss-dev deleted the developements/dan-81-taskbar-pin-heal-486a branch October 8, 2026 09:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants