diff --git a/.changeset/chilly-trains-study.md b/.changeset/chilly-trains-study.md new file mode 100644 index 0000000000..1e9e8381b8 --- /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 dff19261c0..d96a655493 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 0d6f2938fd..4736774163 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 @@ -156,7 +159,7 @@ Function uninstallOldVersion !endif ${if} $uninstallString == "" ClearErrors - Goto Done + Return ${endif} ${endif} @@ -175,7 +178,7 @@ Function uninstallOldVersion ${if} $installationDir == "" ${andIf} $uninstallerFileName == "" ClearErrors - Goto Done + Return ${endif} ${if} $installMode == "CurrentUser" @@ -206,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 diff --git a/packages/app-builder-lib/templates/nsis/messages.yml b/packages/app-builder-lib/templates/nsis/messages.yml index ce95e3f138..6527c99cc6 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 8db1f9acc2..d400ec452d 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"