mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
[Installer] Recover leftover payload files when previous uninstaller leaves them behind (#7676)
* [Installer] Recover leftover payload files when previous uninstaller leaves them behind * [Installer] Fix leftover recovery on the 64-bit auto-uninstall path - Record the 64-bit uninstaller exit code after ExecWait returns. It was captured before the call (always empty), so WaitForOldPayloadToDisappear always aborted before RemoveOldPayloadLeftovers could run. - Restore the pre-uninstall user-data backup before aborting out of the wait, including the guard's cancel and probe-failure prompts, so the only copy cannot be stranded in %TEMP% for the next run to wipe. - Report the leftover removal as done only when every payload executable is gone and RetryDllPurge reports no stale DLLs, instead of checking cockatrice.exe alone. - Return directly after a successful recovery instead of reusing the poll counter, so the log stops claiming the directory "was already clear" and the normal settle delay is kept. * [Installer] Delete the old uninstaller only after recovery succeeds RemoveOldPayloadLeftovers cleared the attribute and deleted $INSTDIR\uninstall.exe on every attempt, before the attempt was known to work. On failure the installer aborts telling the user to run it again, but UninstallString then points at a missing uninstaller: ExecWait leaves the exit code empty, WaitForOldPayloadToDisappear takes the "cancelled or failed" branch, and RemoveOldPayloadLeftovers is gated on exit code 0, so no later run can ever get past it - a half-removed old install the previous revision never produced. Delete it in rop_done instead, after $OldPayloadRemoved is set. WriteUninstaller recreates it on success anyway, and nothing in the success verdict depends on it being gone. --------- Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
bf3562a85a
commit
a83d82b5e8
1 changed files with 154 additions and 4 deletions
|
|
@ -15,6 +15,17 @@ Var ReinstallMode
|
||||||
Var GuardDir
|
Var GuardDir
|
||||||
Var OldUninstallString
|
Var OldUninstallString
|
||||||
Var OldInstallDir
|
Var OldInstallDir
|
||||||
|
; What ExecWait reported for the previous version's uninstaller (empty when it could not be
|
||||||
|
; started at all). Read by WaitForOldPayloadToDisappear: an uninstaller that ran in place
|
||||||
|
; (_?=) and failed or was cancelled will not remove anything more, however long we wait.
|
||||||
|
Var OldUninstallExit
|
||||||
|
; Set to 1 by RemoveOldPayloadLeftovers only when every leftover it is responsible for is
|
||||||
|
; gone; WaitForOldPayloadToDisappear aborts while it is still 0.
|
||||||
|
Var OldPayloadRemoved
|
||||||
|
; Scratch state of AppendUninstallerLog.
|
||||||
|
Var UninstLogHandle
|
||||||
|
Var UninstLogLine
|
||||||
|
Var UninstLogChar
|
||||||
Var DataBackupDir
|
Var DataBackupDir
|
||||||
Var DataBackupDone
|
Var DataBackupDone
|
||||||
Var DataBackupForDir
|
Var DataBackupForDir
|
||||||
|
|
@ -198,6 +209,7 @@ StrCpy $LooseDeckNoticeShown ""
|
||||||
; No old install location resolved yet; Backup/Restore fall back to $INSTDIR
|
; No old install location resolved yet; Backup/Restore fall back to $INSTDIR
|
||||||
; when this is empty (the /R path where $INSTDIR already is the old dir).
|
; when this is empty (the /R path where $INSTDIR already is the old dir).
|
||||||
StrCpy $OldInstallDir ""
|
StrCpy $OldInstallDir ""
|
||||||
|
StrCpy $OldUninstallExit ""
|
||||||
; No purge has run and nothing has been measured yet. The section sets both
|
; No purge has run and nothing has been measured yet. The section sets both
|
||||||
; after a purge, or measures the directory itself when neither purge applies.
|
; after a purge, or measures the directory itself when neither purge applies.
|
||||||
StrCpy $StaleRuntimeAfterPurge 0
|
StrCpy $StaleRuntimeAfterPurge 0
|
||||||
|
|
@ -404,7 +416,9 @@ StrCpy $OldInstallDir $INSTDIR
|
||||||
StrCmp $R0 "" done32
|
StrCmp $R0 "" done32
|
||||||
DetailPrint "Removing previous version (32-bit)..."
|
DetailPrint "Removing previous version (32-bit)..."
|
||||||
Call BackupUserDataBeforeUninstall
|
Call BackupUserDataBeforeUninstall
|
||||||
|
StrCpy $R5 ""
|
||||||
ExecWait '$R0 _?=$OldInstallDir' $R5
|
ExecWait '$R0 _?=$OldInstallDir' $R5
|
||||||
|
StrCpy $OldUninstallExit $R5
|
||||||
!insertmacro CockLog "uninstall (32-bit): exit=$R5"
|
!insertmacro CockLog "uninstall (32-bit): exit=$R5"
|
||||||
Call WaitForOldPayloadToDisappear
|
Call WaitForOldPayloadToDisappear
|
||||||
Call RestoreUserDataAfterUninstall
|
Call RestoreUserDataAfterUninstall
|
||||||
|
|
@ -419,9 +433,11 @@ ${If} ${RunningX64}
|
||||||
StrCpy $OldInstallDir $INSTDIR
|
StrCpy $OldInstallDir $INSTDIR
|
||||||
!insertmacro CockLog "uninstall (64-bit): '$R0 _?=$OldInstallDir'"
|
!insertmacro CockLog "uninstall (64-bit): '$R0 _?=$OldInstallDir'"
|
||||||
StrCmp $R0 "" done64
|
StrCmp $R0 "" done64
|
||||||
|
StrCpy $R5 ""
|
||||||
DetailPrint "Removing previous version (64-bit)..."
|
DetailPrint "Removing previous version (64-bit)..."
|
||||||
Call BackupUserDataBeforeUninstall
|
Call BackupUserDataBeforeUninstall
|
||||||
ExecWait '$R0 _?=$OldInstallDir' $R5
|
ExecWait '$R0 _?=$OldInstallDir' $R5
|
||||||
|
StrCpy $OldUninstallExit $R5
|
||||||
!insertmacro CockLog "uninstall (64-bit): exit=$R5"
|
!insertmacro CockLog "uninstall (64-bit): exit=$R5"
|
||||||
Call WaitForOldPayloadToDisappear
|
Call WaitForOldPayloadToDisappear
|
||||||
Call RestoreUserDataAfterUninstall
|
Call RestoreUserDataAfterUninstall
|
||||||
|
|
@ -439,6 +455,12 @@ FunctionEnd
|
||||||
; ExecWait returns at once while the copy keeps deleting - and it finishes after this
|
; ExecWait returns at once while the copy keeps deleting - and it finishes after this
|
||||||
; window has closed, removing the version that was just installed. Waiting for
|
; window has closed, removing the version that was just installed. Waiting for
|
||||||
; cockatrice.exe to disappear covers both cases, and stops with a message instead of
|
; cockatrice.exe to disappear covers both cases, and stops with a message instead of
|
||||||
|
;
|
||||||
|
; When the payload is still there after the wait, $OldUninstallExit decides: a non-zero
|
||||||
|
; code means the uninstaller was cancelled or failed, and the install aborts saying so.
|
||||||
|
; A zero code means it finished but skipped files someone was holding (NSIS Delete does
|
||||||
|
; that silently), so RemoveOldPayloadLeftovers frees and removes them, and the install
|
||||||
|
; only aborts - naming the holders - when even that fails.
|
||||||
; installing over a directory that is still being emptied.
|
; installing over a directory that is still being emptied.
|
||||||
Function WaitForOldPayloadToDisappear
|
Function WaitForOldPayloadToDisappear
|
||||||
${If} $OldInstallDir == ""
|
${If} $OldInstallDir == ""
|
||||||
|
|
@ -449,10 +471,37 @@ old_uninstall_wait:
|
||||||
IfFileExists "$OldInstallDir\cockatrice.exe" 0 old_uninstall_gone
|
IfFileExists "$OldInstallDir\cockatrice.exe" 0 old_uninstall_gone
|
||||||
IntOp $R2 $R2 + 1
|
IntOp $R2 $R2 + 1
|
||||||
${If} $R2 > 60
|
${If} $R2 > 60
|
||||||
!insertmacro CockLog "old uninstall: $OldInstallDir still holds cockatrice.exe after 30 s"
|
!insertmacro CockLog "old uninstall: $OldInstallDir still holds cockatrice.exe after 30 s (uninstaller exit=$OldUninstallExit)"
|
||||||
StrCpy $AbortReason \
|
; The uninstaller's own account of what it skipped or why it stopped lives in its
|
||||||
"The previous version in $OldInstallDir is still there after its uninstaller ran.$\r$\n$\r$\nIf that uninstaller was cancelled or reported an error, close anything still using those files and run this installer again."
|
; separate log; copy it here so the report sent from AbortInstall carries it.
|
||||||
Call AbortInstall
|
Call AppendUninstallerLog
|
||||||
|
${If} $OldUninstallExit != "0"
|
||||||
|
; Cancelled, failed, or could not be started at all (empty code): the old version
|
||||||
|
; was not meant to be removed, so nothing is deleted on its behalf.
|
||||||
|
StrCpy $AbortReason \
|
||||||
|
"The previous version in $OldInstallDir is still there because its uninstaller was cancelled or failed (exit code '$OldUninstallExit').$\r$\n$\r$\nClose anything still using those files and run this installer again."
|
||||||
|
; The backup was taken before the uninstaller ran; an abort must not leave the
|
||||||
|
; only copy of the user data in %TEMP%, where the next run clears it.
|
||||||
|
Call RestoreUserDataAfterUninstall
|
||||||
|
Call AbortInstall
|
||||||
|
${EndIf}
|
||||||
|
; The uninstaller finished but skipped files - a running instance or a scanner holding
|
||||||
|
; them, which NSIS Delete passes over silently. Free them and remove what is left.
|
||||||
|
Call RemoveOldPayloadLeftovers
|
||||||
|
; The removal reports success only when every payload executable is gone and the
|
||||||
|
; DLL/Plugins purge came back clean; cockatrice.exe alone would hide a locked
|
||||||
|
; oracle.exe or a stale DLL the new payload is about to be written over.
|
||||||
|
${If} $OldPayloadRemoved != "1"
|
||||||
|
StrCpy $AbortReason \
|
||||||
|
"The previous version in $OldInstallDir could not be fully removed after its uninstaller ran.$\r$\n$\r$\nStill in use by: $HolderOutput$\r$\n$\r$\nClose anything still using those files and run this installer again."
|
||||||
|
Call RestoreUserDataAfterUninstall
|
||||||
|
Call AbortInstall
|
||||||
|
${EndIf}
|
||||||
|
; The payload is gone and ExecWait already waited for the uninstaller; keep the
|
||||||
|
; short settle the other paths use, then hand back to the caller's restore.
|
||||||
|
Sleep 1000
|
||||||
|
!insertmacro CockLog "old uninstall: continuing after removing the leftovers the uninstaller skipped"
|
||||||
|
Return
|
||||||
${EndIf}
|
${EndIf}
|
||||||
Sleep 500
|
Sleep 500
|
||||||
Goto old_uninstall_wait
|
Goto old_uninstall_wait
|
||||||
|
|
@ -467,6 +516,97 @@ ${If} $R2 > 0
|
||||||
${Else}
|
${Else}
|
||||||
!insertmacro CockLog "old uninstall: $OldInstallDir was already clear when ExecWait returned"
|
!insertmacro CockLog "old uninstall: $OldInstallDir was already clear when ExecWait returned"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
Return
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; Called when the old uninstaller exited successfully but left its payload behind.
|
||||||
|
; Frees the files the same way the guard does (close, then offer to end), deletes what
|
||||||
|
; the uninstaller skipped, and on failure records which files survived and who holds
|
||||||
|
; them in $HolderOutput, for the abort message. Sets $OldPayloadRemoved to 1 only when
|
||||||
|
; every payload executable is gone and the DLL/Plugins purge is clean.
|
||||||
|
Function RemoveOldPayloadLeftovers
|
||||||
|
!insertmacro CockLog "old uninstall: removing leftovers of $OldInstallDir"
|
||||||
|
StrCpy $OldPayloadRemoved 0
|
||||||
|
StrCpy $HolderOutput "nothing could be named"
|
||||||
|
StrCpy $GuardDir $OldInstallDir
|
||||||
|
Call GuardAppsNotRunning
|
||||||
|
; The purge helpers work on $INSTDIR; point them at the old directory for the
|
||||||
|
; duration (on /R the two are the same anyway).
|
||||||
|
Push $INSTDIR
|
||||||
|
StrCpy $INSTDIR $OldInstallDir
|
||||||
|
StrCpy $R2 0
|
||||||
|
rop_loop:
|
||||||
|
SetFileAttributes "$INSTDIR\cockatrice.exe" NORMAL
|
||||||
|
SetFileAttributes "$INSTDIR\oracle.exe" NORMAL
|
||||||
|
SetFileAttributes "$INSTDIR\servatrice.exe" NORMAL
|
||||||
|
Delete "$INSTDIR\cockatrice.exe"
|
||||||
|
Delete "$INSTDIR\oracle.exe"
|
||||||
|
Delete "$INSTDIR\servatrice.exe"
|
||||||
|
Delete "$INSTDIR\qt.conf"
|
||||||
|
Call RetryDllPurge
|
||||||
|
; $R0 is RetryDllPurge's verdict (1 = a DLL or the Plugins directory survived).
|
||||||
|
; Every payload executable has to be gone as well: a locked oracle.exe or
|
||||||
|
; servatrice.exe is just as fatal to the install that follows as cockatrice.exe.
|
||||||
|
IfFileExists "$INSTDIR\cockatrice.exe" rop_retry
|
||||||
|
IfFileExists "$INSTDIR\oracle.exe" rop_retry
|
||||||
|
IfFileExists "$INSTDIR\servatrice.exe" rop_retry
|
||||||
|
${If} $R0 == 0
|
||||||
|
Goto rop_done
|
||||||
|
${EndIf}
|
||||||
|
rop_retry:
|
||||||
|
IntOp $R2 $R2 + 1
|
||||||
|
${If} $R2 < 3
|
||||||
|
!insertmacro CockLog "old uninstall: leftovers of $OldInstallDir survived removal attempt $R2, retrying in 2 s"
|
||||||
|
Sleep 2000
|
||||||
|
Goto rop_loop
|
||||||
|
${EndIf}
|
||||||
|
!insertmacro CockLog "old uninstall: leftovers of $OldInstallDir still present after 3 removal attempts"
|
||||||
|
!insertmacro CockLogListing "old-leftovers" "*.*"
|
||||||
|
; Per-file Windows codes for the DLLs plus the holder probe's line ($HolderOutput).
|
||||||
|
Call LogStaleRuntimeDiagnostics
|
||||||
|
Pop $INSTDIR
|
||||||
|
Return
|
||||||
|
rop_done:
|
||||||
|
StrCpy $OldPayloadRemoved 1
|
||||||
|
; Only now that the recovery has succeeded: deleting the old uninstaller on a
|
||||||
|
; failed attempt would leave a directory whose UninstallString points at nothing,
|
||||||
|
; so no later run could remove it. WriteUninstaller recreates it on success anyway.
|
||||||
|
SetFileAttributes "$INSTDIR\uninstall.exe" NORMAL
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
!insertmacro CockLog "old uninstall: leftovers of $OldInstallDir removed"
|
||||||
|
!insertmacro CockLogListing "old-leftovers-after-removal" "*.*"
|
||||||
|
Pop $INSTDIR
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; Copies the old uninstaller's log ($TEMP\Cockatrice-uninstaller.log) into this
|
||||||
|
; installer's log, so the report AbortInstall sends shows what the uninstaller did.
|
||||||
|
; Uninstallers older than this log simply have no file; that is logged too.
|
||||||
|
Function AppendUninstallerLog
|
||||||
|
ClearErrors
|
||||||
|
FileOpen $UninstLogHandle "$TEMP\Cockatrice-uninstaller.log" r
|
||||||
|
${If} ${Errors}
|
||||||
|
${OrIf} $UninstLogHandle == ""
|
||||||
|
!insertmacro CockLog "uninstaller log: not found at $TEMP\Cockatrice-uninstaller.log"
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
!insertmacro CockLog "uninstaller log begin ----------------------------------------"
|
||||||
|
aul_loop:
|
||||||
|
ClearErrors
|
||||||
|
FileRead $UninstLogHandle $UninstLogLine
|
||||||
|
IfErrors aul_done
|
||||||
|
; Strip the line break FileRead keeps, so CockLog does not double it.
|
||||||
|
aul_trim:
|
||||||
|
StrCpy $R9 $UninstLogLine 1 -1
|
||||||
|
${If} $R9 == "$\n"
|
||||||
|
${OrIf} $R9 == "$\r"
|
||||||
|
StrCpy $UninstLogLine $UninstLogLine -1
|
||||||
|
Goto aul_trim
|
||||||
|
${EndIf}
|
||||||
|
!insertmacro CockLog "uninstaller: $UninstLogLine"
|
||||||
|
Goto aul_loop
|
||||||
|
aul_done:
|
||||||
|
FileClose $UninstLogHandle
|
||||||
|
!insertmacro CockLog "uninstaller log end ------------------------------------------"
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
; BackupUserDataBeforeUninstall / RestoreUserDataAfterUninstall
|
; BackupUserDataBeforeUninstall / RestoreUserDataAfterUninstall
|
||||||
|
|
@ -1000,6 +1140,9 @@ Function GuardAppsNotRunning
|
||||||
!insertmacro CockLog "guard: cancelled by user"
|
!insertmacro CockLog "guard: cancelled by user"
|
||||||
StrCpy $AbortReason \
|
StrCpy $AbortReason \
|
||||||
"The installation was cancelled because Cockatrice, Oracle or Servatrice is still running and holding files of the previous version.$\r$\n$\r$\nClose them and run the installer again."
|
"The installation was cancelled because Cockatrice, Oracle or Servatrice is still running and holding files of the previous version.$\r$\n$\r$\nClose them and run the installer again."
|
||||||
|
; RemoveOldPayloadLeftovers runs with the pre-uninstall backup already taken;
|
||||||
|
; restoring here keeps this abort from stranding the only copy in %TEMP%.
|
||||||
|
Call RestoreUserDataAfterUninstall
|
||||||
Call AbortInstall
|
Call AbortInstall
|
||||||
; Only reached through Yes: AbortInstall does not return.
|
; Only reached through Yes: AbortInstall does not return.
|
||||||
guard_end:
|
guard_end:
|
||||||
|
|
@ -1031,6 +1174,9 @@ Function GuardAppsNotRunning
|
||||||
guard_unknown_abort:
|
guard_unknown_abort:
|
||||||
StrCpy $AbortReason \
|
StrCpy $AbortReason \
|
||||||
"The installation was cancelled because it could not check whether Cockatrice, Oracle or Servatrice is still running.$\r$\n$\r$\nThe check reported: $GuardOutput$\r$\n$\r$\nPlease close them and run the installer again."
|
"The installation was cancelled because it could not check whether Cockatrice, Oracle or Servatrice is still running.$\r$\n$\r$\nThe check reported: $GuardOutput$\r$\n$\r$\nPlease close them and run the installer again."
|
||||||
|
; Same reason as the cancel path above: this abort can be reached from the
|
||||||
|
; leftover-removal guard, after the backup was taken.
|
||||||
|
Call RestoreUserDataAfterUninstall
|
||||||
Call AbortInstall
|
Call AbortInstall
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
|
|
@ -1818,7 +1964,9 @@ ${If} $PortableMode = 0
|
||||||
!insertmacro CockLog "componentsPagePre: old 32-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)"
|
!insertmacro CockLog "componentsPagePre: old 32-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)"
|
||||||
ClearErrors
|
ClearErrors
|
||||||
Call BackupUserDataBeforeUninstall
|
Call BackupUserDataBeforeUninstall
|
||||||
|
StrCpy $R5 ""
|
||||||
ExecWait "$OldUninstallString _?=$OldInstallDir" $R5
|
ExecWait "$OldUninstallString _?=$OldInstallDir" $R5
|
||||||
|
StrCpy $OldUninstallExit $R5
|
||||||
!insertmacro CockLog "componentsPagePre: old 32-bit uninstall exit=$R5"
|
!insertmacro CockLog "componentsPagePre: old 32-bit uninstall exit=$R5"
|
||||||
Call WaitForOldPayloadToDisappear
|
Call WaitForOldPayloadToDisappear
|
||||||
Call RestoreUserDataAfterUninstall
|
Call RestoreUserDataAfterUninstall
|
||||||
|
|
@ -1854,7 +2002,9 @@ ${If} $PortableMode = 0
|
||||||
!insertmacro CockLog "componentsPagePre: old 64-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)"
|
!insertmacro CockLog "componentsPagePre: old 64-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)"
|
||||||
ClearErrors
|
ClearErrors
|
||||||
Call BackupUserDataBeforeUninstall
|
Call BackupUserDataBeforeUninstall
|
||||||
|
StrCpy $R5 ""
|
||||||
ExecWait "$OldUninstallString _?=$OldInstallDir" $R5
|
ExecWait "$OldUninstallString _?=$OldInstallDir" $R5
|
||||||
|
StrCpy $OldUninstallExit $R5
|
||||||
!insertmacro CockLog "componentsPagePre: old 64-bit uninstall exit=$R5"
|
!insertmacro CockLog "componentsPagePre: old 64-bit uninstall exit=$R5"
|
||||||
Call WaitForOldPayloadToDisappear
|
Call WaitForOldPayloadToDisappear
|
||||||
Call RestoreUserDataAfterUninstall
|
Call RestoreUserDataAfterUninstall
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue