Repository navigation
fix(win): preserve shared associations and restore failed upgrades - #627
Merged
Merged
Conversation
Upgrading deleted the previous installation in place before checking whether the deletion succeeded, so a locked file (e.g. because the app was still running) left the old version half-deleted and unusable. The installer now makes sure no instance is running before touching the disk, moves the previous installation aside with a rename - which fails harmlessly while files are locked - and renames it back if writing the new files fails. The moved-aside copy is only removed once the new installation is complete. The uninstaller deleted whole file-extension registry keys that are shared with other applications (.png, .bmp, .jpeg, .jpg, .svg, .svgz, .dxf), discarding associations owned by other software. It now removes only the Rayforge ProgID keys, remembers the default association that the installer replaces, and restores it if the association still belongs to Rayforge.
The uninstaller previously remembered the default file association it had replaced and restored it on uninstall if the association still belonged to Rayforge. While the ownership guard handles extensions claimed by later installations, snapshotting and restoring shared state is fragile and unnecessary: shared extensions are now never claimed as default at all. Rayforge only registers its ProgIDs and lists them under each extension's OpenWithProgids subkey, which adds it to the "Open with" menu, and uninstalling removes exactly those registrations. The default value is only cleared when it still points at a Rayforge ProgID, which would dangle after removal. Rayforge-owned formats (.ryp, .rfs, .rd) keep their default registration.
🤖 Estimated LLM implementation costReplayed each of the 2 commits in this pull request and counted the code lines each commit added or rewrote:
Full estimate
Cost model: ≈10 tokens per code line at $15/Mtok output and $3/Mtok input (assuming heavy prompt caching); input ≈ 15× output for conversation replay; a review round ≈ 42,000 tokens; a failed CI run ≈ 21,000 tokens; file reads capped at 20 kB each. Active window from first commit to merge: <1 day.
|
This branch was previously deployed
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.
What does this PR change?
Fixes two defects in the Windows installer/uninstaller. Both were found during
the review of the winget-pkgs package submission
(microsoft/winget-pkgs#460163),
which was closed until these are fixed upstream:
Uninstaller destroyed shared file associations. The uninstaller called
DeleteRegKey HKCR ".png"(likewise.bmp,.jpeg,.jpg,.svg,.svgz,.dxf), deleting whole shared extension keys together with valuesand subkeys owned by other applications. Rayforge no longer claims shared
extensions as default at all: it registers its ProgIDs and lists them under
each extension's
OpenWithProgidssubkey, which adds Rayforge to the"Open with" menu. Uninstalling removes exactly those registrations, plus
the ProgID keys. The only time a shared extension's default value is
touched is when it still points at a Rayforge ProgID (which would dangle
after removal); it is cleared, never replaced with a guessed previous
state. Rayforge-owned types (
.ryp,.rfs,.rd) keep full defaultregistration and whole-key cleanup.
A failed upgrade could leave the old installation half-deleted. The
installer removed the previous installation with
RMDir /rbeforechecking whether the deletion succeeded, so a single locked file (e.g. the
app still running) aborted the upgrade with the old version already
partially destroyed. The installer now:
(in silent installs, e.g. winget, it aborts cleanly before deleting
anything),
Renameto<dir>.old-which fails harmlessly while any file in it is locked, leaving the old
version fully intact,
moved-aside copy once the installation is complete,
.onInstFailedcallback(deletes partial new files, renames the old copy back) if the upgrade
fails half-way, e.g. on a full disk.
Why did you choose this specific architectural approach?
association and restored it on uninstall behind an ownership guard. That is
correct in the "another app claimed it later" case, but snapshotting and
restoring mutable external state is fragile by design, so the final
approach mutates no shared state at all:
OpenWithProgidsis additive,removal is exactly scoped to what Rayforge wrote, and there is no previous
state to track. This follows the registration model documented by
Microsoft for the "Open with" dialog.
Renameof the old install directory is atomic on the same volume andfails without side effects while a file in it is locked. That covers the
reviewer's regression case (upgrade while the old app is running) without
a full backup/restore of the installation, which would double the disk
footprint and still risk partial state.
tasklistpiped throughfind, excluding the installer process itself, which shares theRayforge*name prefix). The bundled executable name contains the version, so matching
a fixed file name would miss the old version's process during an upgrade.
No third-party NSIS plugins are required, so the MSYS2
makensisbuildstays unchanged.
How did you manually test this locally?
scripts/win/win_installer.nsiwith makensis 3.12 (x86-unicode)against a stub icon and PyInstaller dist tree: zero warnings, valid
installer produced.
-PPO): macros expand with the correctregistry paths, the process check excludes the installer itself, and the
LogicLib branches (
IntCmp/StrCmp) match the intended logic.a running instance (interactive retry / silent abort), rename failure with
locked files, failure during file copy (rollback via
.onInstFailed),and uninstall after another application or the user changed a shared
association.
Not yet exercised on a real Windows machine: before packaging the next
release, run an upgrade while the old version is still running and confirm
the old installation remains usable when the upgrade is aborted.
Checklist
this code.