Single-instance upgrade handoff: close the stale instance instead of surfacing it#1148
Merged
Merged
Conversation
…surfacing it Upgrading the app appeared not to take effect: the apps are single-instance (constant mutex) and minimize to tray, so an old build kept running after the user "closed" it, and launching the new build just surfaced the old in-memory version via the mutex. Fix: a version-aware handoff at startup — a newer build closes an older tray-resident one and takes over, instead of being handed back the stale version. Shared PerformanceMonitor.Ui: - SingleInstanceDecision: pure, unit-tested decision (older->take over; same/newer->surface; older-but-higher-integrity->actionable error). - ProcessInspector: Win32 — read the other instance's release version from its on-disk exe (QueryFullProcessImageNameW, cross-integrity for same user), measure integrity level directly, detect split-token admin. Fails closed. - SingleInstanceCoordinator: acquire-or-handoff; prompt; graceful exit signal (old runs its real shutdown), bounded wait, force-kill last resort; mutex take-over; elevated relaunch (--upgrade-takeover) for the elevated-old case. - MessageBoxHandoffPrompts: shared dialogs (both apps, parity). Both apps: OnStartup runs the coordinator (replacing the inline mutex/surface block) synchronously before any window/DB/port init; OnExit disposes it; MainWindow opens the exit-for-upgrade channel only after init (so a newer build won't disturb a mid-initializing instance). Scoped by exe name so Lite never targets Dashboard and vice-versa. Local\ session scoping kept intentionally. Two adversarial plan reviews + one implementation review folded in (version field = ProductVersion not FileVersion; mutex-throw vs an elevated instance -> integrity-error path not crash; deferred exit listener; direct IL measure; runas gated to split-token admins; UAC-cancel handled; handles disposed). Tests: Lite 524 + Dashboard 487 green; 0 new warnings. Manual smoke testing of the upgrade/elevation paths still required before merge (see plans/single-instance-upgrade-handoff.md). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The apps are single-instance (constant mutex) and minimize to tray, so an old build keeps running after the user "closes" it. Launching the new build just surfaced the old in-memory version via the mutex — so an upgrade looked like it "didn't take." (Tester: ran setup, reopened, got the old version; only a full tray-exit + uninstall/reinstall worked.)
Fix
Version-aware single-instance handoff at startup, in a shared
SingleInstanceCoordinator(PerformanceMonitor.Ui) used by both apps:QueryFullProcessImageNameW(cross-integrity, same user); integrity measured directly (token IL). Compares the release version (<Version>→ ProductVersion), not FileVersion/AssemblyVersion.Local\session scoping kept intentionally.OnStartupbefore any window/DB/port init; the exit-for-upgrade channel opens only after init so a newer build won't disturb a mid-initializing instance.Covers all delivery paths (Velopack Setup relaunch, in-app update, portable ZIP); the in-app
ApplyUpdatesAndRestartpath already hard-exits the old process, so it's unaffected.Review
Two adversarial plan passes + one implementation pass; all findings folded in (version-field critical; mutex-throw-vs-elevated → integrity-error path not crash; deferred exit listener; direct IL measure; runas gated; UAC-cancel handled; handles disposed).
Tests
Lite 524 + Dashboard 487 green; 0 new warnings. 19 new decision unit tests. Manual upgrade/elevation smoke testing to be done on the build.
Design:
plans/single-instance-upgrade-handoff.md.🤖 Generated with Claude Code