Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(nsis): use atomic rmdir on update #6551

Conversation

indutny-signal
Copy link
Contributor

@indutny-signal indutny-signal commented Jan 15, 2022

Previously uninstaller would run RMDir /r $INSTDIR to remove the
currently installed version, but this operation is not atomic and if
some of the files will fail to delete it will leave them in the
directory while erasing the rest. If we are updating, however, this
leads us to a tricky situation where we cannot update these files, but
cannot also cancel the installation. Because of the erased files the app
won't be able to start if the installation won't completely at least
partially. However, the downside of that is that the app can have new
asar.unpacked files along with the old asar, executable, and bindings.

The approach of this change is to recursive use Rename instead of a
single RMDir /r in uninstaller (when isUpdated is true) to move the
whole app directory file by file to a temporary folder. If this
operation fails due to busy files - we use CopyFiles to restore all
files that we managed to move so far. Because the whole uninstallation
process becomes interrupted - the app shortcut and file associations
have to be removed only after the successful recursive Rename.

This error is caught by installer running in an update mode (see
installUtil.nsh) and presented to user in a dialog. If this erro
happen the installation does not proceed normally.

In addition to all of the above, this patch simplifies the last resort
measure in extractAppPackage which should now only run when old
uninstaller (that still uses RMDir /r) leaves busy files behind.

Testing

Tested by doing normal updates and keeping tmp.exe open as in #6547

electron-builder-demo.mp4

Previously uninstaller would run `RMDir /r $INSTDIR` to remove the
currently installed version, but this operation is not atomic and if
some of the files will fail to delete it will leave them in the
directory while erasing the rest. If we are updating, however, this
leads us to a tricky situation where we cannot update these files, but
cannot also cancel the installation. Because of the erased files the app
won't be able to start if the installation won't completely at least
partially. However, the downside of that is that the app can have new
asar.unpacked files along with the old asar, executable, and bindings.

The approach of this change is to recursive use `Rename` instead of a
single `RMDir /r` in uninstaller (when `isUpdated` is true) to move the
whole app directory file by file to a temporary folder. If this
operation fails due to busy files - we use `CopyFiles` to restore all
files that we managed to move so far. Because the whole uninstallation
process becomes interrupted - the app shortcut and file associations
have to be removed only *after* the successful recursive `Rename`.

This error is caught by installer running in an update mode (see
`installUtil.nsh`) and presented to user in a dialog. If this erro
happen the installation does not proceed normally.

In addition to all of the above, this patch simplifies the last resort
measure in `extractAppPackage` which should now only run when old
uninstaller (that still uses `RMDir /r`) leaves busy files behind.
@changeset-bot
Copy link

changeset-bot bot commented Jan 15, 2022

🦋 Changeset detected

Latest commit: f8fe5ff

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
app-builder-lib Patch
dmg-builder Patch
electron-builder-squirrel-windows Patch
electron-builder Patch
electron-forge-maker-appimage Patch
electron-forge-maker-nsis-web Patch
electron-forge-maker-nsis Patch
electron-forge-maker-snap Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify
Copy link

netlify bot commented Jan 15, 2022

✔️ Deploy Preview for car-park-attendant-cleat-11576 ready!

🔨 Explore the source changes: f8fe5ff

🔍 Inspect the deploy log: https://app.netlify.com/sites/car-park-attendant-cleat-11576/deploys/61e25152115705000774080c

😎 Browse the preview: https://deploy-preview-6551--car-park-attendant-cleat-11576.netlify.app

DetailPrint `Uninstall was not successful. Uninstaller error code: $R0.`
SetErrorLevel 2
Quit
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think ideally we should retry running uninstaller (with user's confirmation) in such case instead of failing whole installation immediately. Let me know if you want me to add this to the current patch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, and btw. Abort doesn't quite work here for some reason (the installer just hangs), so I had to resort to Quit.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@indutny-signal This sounds like a great idea. Is it complex to add?

we should retry running uninstaller (with user's confirmation) in such case instead of failing whole installation immediately

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually already pushed it 😉

@@ -28,6 +28,109 @@ Function un.onInit
!endif
FunctionEnd

Function un.atomicRMDir
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely proud of this, but it implements RMDir /r that preserves backups of all files and directories that it removed in $PLUGINSDIR\old-install.

@@ -38,6 +141,34 @@ Section "un.install"

!insertmacro setLinkVars

# delete the installed files
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order of operations changed as stated in the commit message. We try to keep as much state as possible in case we will need to restore the files on failure. The users would be really frustrated to see their shortcuts and file associations go if uninstall.exe --updated fails while restoring all app's files.

Copy link
Contributor Author

@indutny-signal indutny-signal Jan 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just went ahead and did it. Makes for a much better user experience!

@mmaietta mmaietta merged commit 7b2a5e1 into electron-userland:master Jan 16, 2022
@indutny
Copy link
Contributor

indutny commented Jan 16, 2022

Awesome! Thanks for merging it!

@mmaietta
Copy link
Collaborator

Thanks for the contribution @indutny! It'll make it in our upcoming v23 alpha release :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants