Skip to content

Commit 7b2a5e1

Browse files
fix(nsis): use revertible+atomic rmdir on update and add user-confirmed retry loop (#6551)
* fix(nsis): use revertible+atomic rmdir on update and retry (w/ user confirmation) 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. * Retry uninstall
1 parent a138a86 commit 7b2a5e1

File tree

5 files changed

+186
-31
lines changed

5 files changed

+186
-31
lines changed

.changeset/chilly-trains-study.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"app-builder-lib": patch
3+
---
4+
5+
fix(nsis): use revertible rmdir on update

packages/app-builder-lib/templates/nsis/include/extractAppPackage.nsh

+12-16
Original file line numberDiff line numberDiff line change
@@ -104,28 +104,24 @@
104104
LoopExtract7za:
105105
IntOp $R1 $R1 + 1
106106

107+
# Attempt to copy files in atomic way
107108
CopyFiles /SILENT "$PLUGINSDIR\7z-out\*" $OUTDIR
108109
IfErrors 0 DoneExtract7za
109110

110-
${if} $R1 > 1
111-
DetailPrint `Can't modify "${PRODUCT_NAME}"'s files.`
112-
${if} $R1 < 5
113-
# Try copying a few times before giving up
114-
Goto LoopExtract7za
115-
${else}
116-
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeClosed)" /SD IDCANCEL IDRETRY RetryExtract7za
117-
${endIf}
118-
119-
# CopyFiles will remove all overwritten files when it encounters an
120-
# issue and make app non-launchable. Extract over from the archive
121-
# ignoring the failures so at least we will partially update and the
122-
# app would start.
123-
Nsis7z::Extract "${FILE}"
124-
Quit
111+
DetailPrint `Can't modify "${PRODUCT_NAME}"'s files.`
112+
${if} $R1 < 5
113+
# Try copying a few times before asking for a user action.
114+
Goto RetryExtract7za
125115
${else}
126-
Goto LoopExtract7za
116+
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeClosed)" /SD IDCANCEL IDRETRY RetryExtract7za
127117
${endIf}
128118

119+
# As an absolutely last resort after a few automatic attempts and user
120+
# intervention - we will just overwrite everything with `Nsis7z::Extract`
121+
# even though it is not atomic and will ignore errors.
122+
Nsis7z::Extract "${FILE}"
123+
Quit
124+
129125
RetryExtract7za:
130126
Sleep 1000
131127
Goto LoopExtract7za

packages/app-builder-lib/templates/nsis/include/installUtil.nsh

+36-8
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,10 @@ Function handleUninstallResult
126126
Return
127127

128128
${if} $R0 != 0
129+
MessageBox MB_OK|MB_ICONEXCLAMATION "$(uninstallFailed): $R0"
129130
DetailPrint `Uninstall was not successful. Uninstaller error code: $R0.`
131+
SetErrorLevel 2
132+
Quit
130133
${endif}
131134
FunctionEnd
132135

@@ -156,7 +159,7 @@ Function uninstallOldVersion
156159
!endif
157160
${if} $uninstallString == ""
158161
ClearErrors
159-
Goto Done
162+
Return
160163
${endif}
161164
${endif}
162165

@@ -175,7 +178,7 @@ Function uninstallOldVersion
175178
${if} $installationDir == ""
176179
${andIf} $uninstallerFileName == ""
177180
ClearErrors
178-
Goto Done
181+
Return
179182
${endif}
180183

181184
${if} $installMode == "CurrentUser"
@@ -206,12 +209,37 @@ Function uninstallOldVersion
206209
StrCpy $uninstallerFileNameTemp "$PLUGINSDIR\old-uninstaller.exe"
207210
!insertmacro copyFile "$uninstallerFileName" "$uninstallerFileNameTemp"
208211

209-
ExecWait '"$uninstallerFileNameTemp" /S /KEEP_APP_DATA $0 _?=$installationDir' $R0
210-
ifErrors 0 Done
211-
# the execution failed - might have been caused by some group policy restrictions
212-
# we try to execute the uninstaller in place
213-
ExecWait '"$uninstallerFileName" /S /KEEP_APP_DATA $0 _?=$installationDir' $R0
214-
Done:
212+
# Retry counter
213+
StrCpy $R5 0
214+
215+
UninstallLoop:
216+
IntOp $R5 $R5 + 1
217+
218+
${if} $R5 > 5
219+
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeClosed)" /SD IDCANCEL IDRETRY OneMoreAttempt
220+
Return
221+
${endIf}
222+
223+
OneMoreAttempt:
224+
ExecWait '"$uninstallerFileNameTemp" /S /KEEP_APP_DATA $0 _?=$installationDir' $R0
225+
ifErrors TryInPlace CheckResult
226+
227+
TryInPlace:
228+
# the execution failed - might have been caused by some group policy restrictions
229+
# we try to execute the uninstaller in place
230+
ExecWait '"$uninstallerFileName" /S /KEEP_APP_DATA $0 _?=$installationDir' $R0
231+
ifErrors DoesNotExist
232+
233+
CheckResult:
234+
${if} $R0 == 0
235+
Return
236+
${endIf}
237+
238+
Sleep 1000
239+
Goto UninstallLoop
240+
241+
DoesNotExist:
242+
SetErrors
215243
FunctionEnd
216244

217245
!macro uninstallOldVersion ROOT_KEY

packages/app-builder-lib/templates/nsis/messages.yml

+2
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,5 @@ areYouSureToUninstall:
144144
da: Er du sikker på, at du vil afinstallere ${PRODUCT_NAME}?
145145
decompressionFailed:
146146
en: Failed to decompress files. Please try running the installer again.
147+
uninstallFailed:
148+
en: Failed to uninstall old application files. Please try running the installer again.

packages/app-builder-lib/templates/nsis/uninstaller.nsh

+131-7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,109 @@ Function un.onInit
2828
!endif
2929
FunctionEnd
3030

31+
Function un.atomicRMDir
32+
Exch $R0
33+
Push $R1
34+
Push $R2
35+
Push $R3
36+
37+
StrCpy $R3 "$INSTDIR$R0\*.*"
38+
FindFirst $R1 $R2 $R3
39+
40+
loop:
41+
StrCmp $R2 "" break
42+
43+
StrCmp $R2 "." continue
44+
StrCmp $R2 ".." continue
45+
46+
IfFileExists "$INSTDIR$R0\$R2\*.*" isDir isNotDir
47+
48+
isDir:
49+
CreateDirectory "$PLUGINSDIR\old-install$R0\$R2"
50+
51+
Push "$R0\$R2"
52+
Call un.atomicRMDir
53+
Pop $R3
54+
55+
${if} $R3 != 0
56+
Goto done
57+
${endIf}
58+
59+
Goto continue
60+
61+
isNotDir:
62+
ClearErrors
63+
Rename "$INSTDIR$R0\$R2" "$PLUGINSDIR\old-install$R0\$R2"
64+
65+
# Ignore errors when renaming ourselves.
66+
StrCmp "$R0\$R2" "${UNINSTALL_FILENAME}" 0 +2
67+
ClearErrors
68+
69+
IfErrors 0 +3
70+
StrCpy $R3 "$INSTDIR$R0\$R2"
71+
Goto done
72+
73+
continue:
74+
FindNext $R1 $R2
75+
Goto loop
76+
77+
break:
78+
StrCpy $R3 0
79+
80+
done:
81+
FindClose $R1
82+
83+
StrCpy $R0 $R3
84+
85+
Pop $R3
86+
Pop $R2
87+
Pop $R1
88+
Exch $R0
89+
FunctionEnd
90+
91+
Function un.restoreFiles
92+
Exch $R0
93+
Push $R1
94+
Push $R2
95+
Push $R3
96+
97+
StrCpy $R3 "$PLUGINSDIR\old-install$R0\*.*"
98+
FindFirst $R1 $R2 $R3
99+
100+
loop:
101+
StrCmp $R2 "" break
102+
103+
StrCmp $R2 "." continue
104+
StrCmp $R2 ".." continue
105+
106+
IfFileExists "$INSTDIR$R0\$R2\*.*" isDir isNotDir
107+
108+
isDir:
109+
CreateDirectory "$INSTDIR$R0\$R2"
110+
111+
Push "$R0\$R2"
112+
Call un.restoreFiles
113+
Pop $R3
114+
115+
Goto continue
116+
117+
isNotDir:
118+
Rename $PLUGINSDIR\old-install$R0\$R2" "$INSTDIR$R0\$R2"
119+
120+
continue:
121+
FindNext $R1 $R2
122+
Goto loop
123+
124+
break:
125+
StrCpy $R0 0
126+
FindClose $R1
127+
128+
Pop $R3
129+
Pop $R2
130+
Pop $R1
131+
Exch $R0
132+
FunctionEnd
133+
31134
Section "un.install"
32135
# for assisted installer we check it here to show progress
33136
!ifndef ONE_CLICK
@@ -38,6 +141,34 @@ Section "un.install"
38141
39142
!insertmacro setLinkVars
40143
144+
# delete the installed files
145+
!ifmacrodef customRemoveFiles
146+
!insertmacro customRemoveFiles
147+
!else
148+
${if} ${isUpdated}
149+
CreateDirectory "$PLUGINSDIR\old-install"
150+
151+
Push ""
152+
Call un.atomicRMDir
153+
Pop $R0
154+
155+
${if} $R0 != 0
156+
DetailPrint "File is busy, aborting: $R0"
157+
158+
# Attempt to restore previous directory
159+
Push ""
160+
Call un.restoreFiles
161+
Pop $R0
162+
163+
Abort `Can't rename "$INSTDIR" to "$PLUGINSDIR\old-install".`
164+
${endif}
165+
166+
${endif}
167+
168+
# Remove all files (or remaining shallow directories from the block above)
169+
RMDir /r $INSTDIR
170+
!endif
171+
41172
${ifNot} ${isKeepShortcuts}
42173
WinShell::UninstAppUserModelId "${APP_ID}"
43174
@@ -64,13 +195,6 @@ Section "un.install"
64195
!insertmacro unregisterFileAssociations
65196
!endif
66197
67-
# delete the installed files
68-
!ifmacrodef customRemoveFiles
69-
!insertmacro customRemoveFiles
70-
!else
71-
RMDir /r $INSTDIR
72-
!endif
73-
74198
Var /GLOBAL isDeleteAppData
75199
StrCpy $isDeleteAppData "0"
76200

0 commit comments

Comments
 (0)