From dd8770fe67af75b110cf5bc412d2b4fea4be9611 Mon Sep 17 00:00:00 2001 From: Fedor Indutnyy Date: Fri, 14 Jan 2022 16:24:44 -0800 Subject: [PATCH 1/2] fix(nsis): use revertible rmdir on update 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/chilly-trains-study.md | 5 + .../nsis/include/extractAppPackage.nsh | 28 ++-- .../templates/nsis/include/installUtil.nsh | 3 + .../templates/nsis/messages.yml | 2 + .../templates/nsis/uninstaller.nsh | 138 +++++++++++++++++- 5 files changed, 153 insertions(+), 23 deletions(-) create mode 100644 .changeset/chilly-trains-study.md diff --git a/.changeset/chilly-trains-study.md b/.changeset/chilly-trains-study.md new file mode 100644 index 00000000000..1e9e8381b8d --- /dev/null +++ b/.changeset/chilly-trains-study.md @@ -0,0 +1,5 @@ +--- +"app-builder-lib": patch +--- + +fix(nsis): use revertible rmdir on update diff --git a/packages/app-builder-lib/templates/nsis/include/extractAppPackage.nsh b/packages/app-builder-lib/templates/nsis/include/extractAppPackage.nsh index dff19261c0d..d96a6554938 100644 --- a/packages/app-builder-lib/templates/nsis/include/extractAppPackage.nsh +++ b/packages/app-builder-lib/templates/nsis/include/extractAppPackage.nsh @@ -104,28 +104,24 @@ LoopExtract7za: IntOp $R1 $R1 + 1 + # Attempt to copy files in atomic way CopyFiles /SILENT "$PLUGINSDIR\7z-out\*" $OUTDIR IfErrors 0 DoneExtract7za - ${if} $R1 > 1 - DetailPrint `Can't modify "${PRODUCT_NAME}"'s files.` - ${if} $R1 < 5 - # Try copying a few times before giving up - Goto LoopExtract7za - ${else} - MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeClosed)" /SD IDCANCEL IDRETRY RetryExtract7za - ${endIf} - - # CopyFiles will remove all overwritten files when it encounters an - # issue and make app non-launchable. Extract over from the archive - # ignoring the failures so at least we will partially update and the - # app would start. - Nsis7z::Extract "${FILE}" - Quit + DetailPrint `Can't modify "${PRODUCT_NAME}"'s files.` + ${if} $R1 < 5 + # Try copying a few times before asking for a user action. + Goto RetryExtract7za ${else} - Goto LoopExtract7za + MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeClosed)" /SD IDCANCEL IDRETRY RetryExtract7za ${endIf} + # As an absolutely last resort after a few automatic attempts and user + # intervention - we will just overwrite everything with `Nsis7z::Extract` + # even though it is not atomic and will ignore errors. + Nsis7z::Extract "${FILE}" + Quit + RetryExtract7za: Sleep 1000 Goto LoopExtract7za diff --git a/packages/app-builder-lib/templates/nsis/include/installUtil.nsh b/packages/app-builder-lib/templates/nsis/include/installUtil.nsh index 0d6f2938fd6..c338abfd19b 100644 --- a/packages/app-builder-lib/templates/nsis/include/installUtil.nsh +++ b/packages/app-builder-lib/templates/nsis/include/installUtil.nsh @@ -126,7 +126,10 @@ Function handleUninstallResult Return ${if} $R0 != 0 + MessageBox MB_OK|MB_ICONEXCLAMATION "$(uninstallFailed): $R0" DetailPrint `Uninstall was not successful. Uninstaller error code: $R0.` + SetErrorLevel 2 + Quit ${endif} FunctionEnd diff --git a/packages/app-builder-lib/templates/nsis/messages.yml b/packages/app-builder-lib/templates/nsis/messages.yml index ce95e3f1381..6527c99cc6a 100644 --- a/packages/app-builder-lib/templates/nsis/messages.yml +++ b/packages/app-builder-lib/templates/nsis/messages.yml @@ -144,3 +144,5 @@ areYouSureToUninstall: da: Er du sikker på, at du vil afinstallere ${PRODUCT_NAME}? decompressionFailed: en: Failed to decompress files. Please try running the installer again. +uninstallFailed: + en: Failed to uninstall old application files. Please try running the installer again. diff --git a/packages/app-builder-lib/templates/nsis/uninstaller.nsh b/packages/app-builder-lib/templates/nsis/uninstaller.nsh index 8db1f9acc2a..d400ec452df 100644 --- a/packages/app-builder-lib/templates/nsis/uninstaller.nsh +++ b/packages/app-builder-lib/templates/nsis/uninstaller.nsh @@ -28,6 +28,109 @@ Function un.onInit !endif FunctionEnd +Function un.atomicRMDir + Exch $R0 + Push $R1 + Push $R2 + Push $R3 + + StrCpy $R3 "$INSTDIR$R0\*.*" + FindFirst $R1 $R2 $R3 + + loop: + StrCmp $R2 "" break + + StrCmp $R2 "." continue + StrCmp $R2 ".." continue + + IfFileExists "$INSTDIR$R0\$R2\*.*" isDir isNotDir + + isDir: + CreateDirectory "$PLUGINSDIR\old-install$R0\$R2" + + Push "$R0\$R2" + Call un.atomicRMDir + Pop $R3 + + ${if} $R3 != 0 + Goto done + ${endIf} + + Goto continue + + isNotDir: + ClearErrors + Rename "$INSTDIR$R0\$R2" "$PLUGINSDIR\old-install$R0\$R2" + + # Ignore errors when renaming ourselves. + StrCmp "$R0\$R2" "${UNINSTALL_FILENAME}" 0 +2 + ClearErrors + + IfErrors 0 +3 + StrCpy $R3 "$INSTDIR$R0\$R2" + Goto done + + continue: + FindNext $R1 $R2 + Goto loop + + break: + StrCpy $R3 0 + + done: + FindClose $R1 + + StrCpy $R0 $R3 + + Pop $R3 + Pop $R2 + Pop $R1 + Exch $R0 +FunctionEnd + +Function un.restoreFiles + Exch $R0 + Push $R1 + Push $R2 + Push $R3 + + StrCpy $R3 "$PLUGINSDIR\old-install$R0\*.*" + FindFirst $R1 $R2 $R3 + + loop: + StrCmp $R2 "" break + + StrCmp $R2 "." continue + StrCmp $R2 ".." continue + + IfFileExists "$INSTDIR$R0\$R2\*.*" isDir isNotDir + + isDir: + CreateDirectory "$INSTDIR$R0\$R2" + + Push "$R0\$R2" + Call un.restoreFiles + Pop $R3 + + Goto continue + + isNotDir: + Rename $PLUGINSDIR\old-install$R0\$R2" "$INSTDIR$R0\$R2" + + continue: + FindNext $R1 $R2 + Goto loop + + break: + StrCpy $R0 0 + FindClose $R1 + + Pop $R3 + Pop $R2 + Pop $R1 + Exch $R0 +FunctionEnd + Section "un.install" # for assisted installer we check it here to show progress !ifndef ONE_CLICK @@ -38,6 +141,34 @@ Section "un.install" !insertmacro setLinkVars + # delete the installed files + !ifmacrodef customRemoveFiles + !insertmacro customRemoveFiles + !else + ${if} ${isUpdated} + CreateDirectory "$PLUGINSDIR\old-install" + + Push "" + Call un.atomicRMDir + Pop $R0 + + ${if} $R0 != 0 + DetailPrint "File is busy, aborting: $R0" + + # Attempt to restore previous directory + Push "" + Call un.restoreFiles + Pop $R0 + + Abort `Can't rename "$INSTDIR" to "$PLUGINSDIR\old-install".` + ${endif} + + ${endif} + + # Remove all files (or remaining shallow directories from the block above) + RMDir /r $INSTDIR + !endif + ${ifNot} ${isKeepShortcuts} WinShell::UninstAppUserModelId "${APP_ID}" @@ -64,13 +195,6 @@ Section "un.install" !insertmacro unregisterFileAssociations !endif - # delete the installed files - !ifmacrodef customRemoveFiles - !insertmacro customRemoveFiles - !else - RMDir /r $INSTDIR - !endif - Var /GLOBAL isDeleteAppData StrCpy $isDeleteAppData "0" From f8fe5fff1a788a8719b1b7991652c1da54c594f1 Mon Sep 17 00:00:00 2001 From: Fedor Indutnyy Date: Fri, 14 Jan 2022 20:35:28 -0800 Subject: [PATCH 2/2] Retry uninstall --- .../templates/nsis/include/installUtil.nsh | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/app-builder-lib/templates/nsis/include/installUtil.nsh b/packages/app-builder-lib/templates/nsis/include/installUtil.nsh index c338abfd19b..47367741632 100644 --- a/packages/app-builder-lib/templates/nsis/include/installUtil.nsh +++ b/packages/app-builder-lib/templates/nsis/include/installUtil.nsh @@ -159,7 +159,7 @@ Function uninstallOldVersion !endif ${if} $uninstallString == "" ClearErrors - Goto Done + Return ${endif} ${endif} @@ -178,7 +178,7 @@ Function uninstallOldVersion ${if} $installationDir == "" ${andIf} $uninstallerFileName == "" ClearErrors - Goto Done + Return ${endif} ${if} $installMode == "CurrentUser" @@ -209,12 +209,37 @@ Function uninstallOldVersion StrCpy $uninstallerFileNameTemp "$PLUGINSDIR\old-uninstaller.exe" !insertmacro copyFile "$uninstallerFileName" "$uninstallerFileNameTemp" - ExecWait '"$uninstallerFileNameTemp" /S /KEEP_APP_DATA $0 _?=$installationDir' $R0 - ifErrors 0 Done - # the execution failed - might have been caused by some group policy restrictions - # we try to execute the uninstaller in place - ExecWait '"$uninstallerFileName" /S /KEEP_APP_DATA $0 _?=$installationDir' $R0 - Done: + # Retry counter + StrCpy $R5 0 + + UninstallLoop: + IntOp $R5 $R5 + 1 + + ${if} $R5 > 5 + MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeClosed)" /SD IDCANCEL IDRETRY OneMoreAttempt + Return + ${endIf} + + OneMoreAttempt: + ExecWait '"$uninstallerFileNameTemp" /S /KEEP_APP_DATA $0 _?=$installationDir' $R0 + ifErrors TryInPlace CheckResult + + TryInPlace: + # the execution failed - might have been caused by some group policy restrictions + # we try to execute the uninstaller in place + ExecWait '"$uninstallerFileName" /S /KEEP_APP_DATA $0 _?=$installationDir' $R0 + ifErrors DoesNotExist + + CheckResult: + ${if} $R0 == 0 + Return + ${endIf} + + Sleep 1000 + Goto UninstallLoop + + DoesNotExist: + SetErrors FunctionEnd !macro uninstallOldVersion ROOT_KEY