mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
2554 lines
127 KiB
Text
2554 lines
127 KiB
Text
!include ..\..\..\NSIS.definitions.nsh
|
|
Name "@CPACK_PACKAGE_NAME@"
|
|
BrandingText "@CPACK_PACKAGE_FILE_NAME@"
|
|
OutFile "@CPACK_TOPLEVEL_DIRECTORY@/@CPACK_OUTPUT_FILE_NAME@"
|
|
|
|
!define INST_DIR "@CPACK_TEMPORARY_DIRECTORY@"
|
|
|
|
RequestExecutionlevel admin
|
|
SetCompressor LZMA
|
|
|
|
Var NormalDestDir
|
|
Var PortableDestDir
|
|
Var PortableMode
|
|
Var ReinstallMode
|
|
Var GuardDir
|
|
Var OldUninstallString
|
|
Var OldInstallDir
|
|
; What ExecWait reported for the previous version's uninstaller, or "missing" /
|
|
; "launch-failed" when RunOldUninstaller could not start it. 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
|
|
; Executable path parsed out of $OldUninstallString by ResolveOldUninstallExe. Used to
|
|
; tell an uninstaller that is missing or cannot be started ("missing" / "launch-failed"
|
|
; in $OldUninstallExit) from one that ran and failed.
|
|
Var OldUninstallExe
|
|
; 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 DataBackupDone
|
|
Var DataBackupForDir
|
|
Var LooseDecksBackupDone
|
|
Var InstallSubdirsBackupDone
|
|
Var LooseDeckNoticeShown
|
|
Var StaleFindHandle
|
|
Var StaleFindName
|
|
Var DataBackupFindHandle
|
|
Var DataBackupFindName
|
|
Var IsUpgrade
|
|
; Set together by whichever purge block ran: the verdict RetryDllPurge measured
|
|
; and whether a purge ran at all - a portable install into a directory without
|
|
; portable.dat is neither purge block, and the abort below measures it itself.
|
|
Var StaleRuntimeAfterPurge
|
|
Var DllPurgeRan
|
|
Var AbortReason
|
|
; Diagnostics for a failed purge: the holder probe in LogProcessesUsingInstallDir writes
|
|
; $HolderOutput (one log line) and sets $HolderFound when it named anything.
|
|
Var HolderExit
|
|
Var HolderOutput
|
|
Var HolderFound
|
|
; What the last RunHolderProbe call answered: its exit code and its line. Kept
|
|
; apart from $HolderExit/$HolderOutput because the guard only wants to know
|
|
; whether the payload is free, while the abort dialog reports what survived.
|
|
Var GuardExit
|
|
Var GuardOutput
|
|
; Whether the guard has already asked Cockatrice to close by itself: 0 before the automatic
|
|
; close, 1 after it. The prompt that follows offers to end the instance rather than asking
|
|
; a second time (see GuardAppsNotRunning).
|
|
Var GuardCloseTried
|
|
; Set when the user answered Yes to ending the programs and the files were still in use
|
|
; afterwards, so the prompt that follows says the end did not help instead of reappearing
|
|
; word for word (see CockEndPrompt).
|
|
Var GuardEndFailed
|
|
; The question CockEndPrompt assembles: one sentence in it is conditional, and the result
|
|
; has to stay inside the characters one NSIS string holds.
|
|
Var GuardPrompt
|
|
Var WaitPid
|
|
Var LauncherExited
|
|
; How long one WaitForSingleObject on the launcher's handle waits before asking the user
|
|
; (see WaitForLauncherToExit). A variable because the first slice is short and the ones
|
|
; after the first question are not.
|
|
Var WaitSlice
|
|
; Whether that handle carries PROCESS_TERMINATE as well as SYNCHRONIZE, and can therefore
|
|
; be ended from the answer the dialog offers. Cleared when only the synchronize right could
|
|
; be had (see WaitForLauncherToExit).
|
|
Var WaitCanEnd
|
|
; How far the launcher wait got with a launcher that has no window to ask: 0 before the
|
|
; first round, 1 after the grace it was given, 2 after it was ended. A launcher whose
|
|
; window is gone has answered its own close and is stuck in shutdown, so the dialog cannot
|
|
; reach it and the wait may end it instead of asking the user (see WaitForLauncherToExit).
|
|
Var WaitWindowless
|
|
Var InstallLogPath
|
|
Var LogHandle
|
|
Var LogMessage
|
|
Var LogFindHandle
|
|
Var LogFindName
|
|
Var LogMatchLabel
|
|
Var LogMatchPattern
|
|
; Sentry installer-log hook state (see AbortInstall)
|
|
Var SentryResult
|
|
Var SentryOutput
|
|
|
|
!include LogicLib.nsh
|
|
!include FileFunc.nsh
|
|
!include MUI2.nsh
|
|
!include x64.nsh
|
|
|
|
!define MUI_ABORTWARNING
|
|
!define MUI_WELCOMEFINISHPAGE_BITMAP "${NSIS_SOURCE_PATH}\cmake\leftimage.bmp"
|
|
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "${NSIS_SOURCE_PATH}\cmake\leftimage.bmp"
|
|
!define MUI_HEADERIMAGE
|
|
!define MUI_HEADERIMAGE_BITMAP "${NSIS_SOURCE_PATH}\cmake\headerimage.bmp"
|
|
!define MUI_HEADERIMAGE_UNBITMAP "${NSIS_SOURCE_PATH}\cmake\headerimage.bmp"
|
|
!define MUI_WELCOMEPAGE_TEXT "This wizard will guide you through the installation of Cockatrice.$\r$\n$\r$\nClick Next to continue."
|
|
!define MUI_FINISHPAGE_RUN "$INSTDIR/cockatrice.exe"
|
|
!define MUI_FINISHPAGE_RUN_TEXT "Run 'Cockatrice' now"
|
|
!define MUI_ICON "${NSIS_SOURCE_PATH}\cockatrice\resources\appicon.ico"
|
|
|
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
|
|
!insertmacro MUI_PAGE_WELCOME
|
|
|
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
|
|
!insertmacro MUI_PAGE_LICENSE "${NSIS_SOURCE_PATH}\LICENSE"
|
|
|
|
Page Custom PortableModePageCreate PortableModePageLeave
|
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE componentsPagePre
|
|
!insertmacro MUI_PAGE_COMPONENTS
|
|
|
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
|
|
!insertmacro MUI_PAGE_DIRECTORY
|
|
|
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
|
|
!insertmacro MUI_PAGE_INSTFILES
|
|
|
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
|
|
!insertmacro MUI_PAGE_FINISH
|
|
|
|
!insertmacro MUI_UNPAGE_CONFIRM
|
|
!insertmacro MUI_UNPAGE_COMPONENTS
|
|
!insertmacro MUI_UNPAGE_INSTFILES
|
|
!insertmacro MUI_UNPAGE_FINISH
|
|
|
|
!insertmacro MUI_LANGUAGE English
|
|
|
|
; Installer log
|
|
; Every run appends a step-by-step trace to a file so a failed upgrade on a
|
|
; user's machine can be diagnosed after the fact. Kept in $TEMP rather than in
|
|
; $INSTDIR because the previous version's uninstaller removes the whole install
|
|
; directory part-way through an upgrade. Read it back with:
|
|
; notepad %TEMP%\Cockatrice-installer.log
|
|
!macro CockLog text
|
|
Push "${text}"
|
|
Call CockLogWrite
|
|
!macroend
|
|
|
|
; The body is shared because the uninstaller needs its own copy: un.* functions are
|
|
; compiled into a separate binary, and inside one a Call may only name an un. function,
|
|
; so CockLog's Call to CockLogWrite cannot be used there.
|
|
!macro CockLogWriteBody
|
|
Pop $LogMessage
|
|
FileOpen $LogHandle "$InstallLogPath" a
|
|
${If} $LogHandle == ""
|
|
Return
|
|
${EndIf}
|
|
; FileOpen places the pointer at the start of the file in every mode, so
|
|
; 'a' alone would overwrite the log from the beginning on each line. Seek
|
|
; past the existing content first to actually append.
|
|
FileSeek $LogHandle 0 END
|
|
FileWrite $LogHandle "$LogMessage$\r$\n"
|
|
FileClose $LogHandle
|
|
!macroend
|
|
|
|
Function CockLogWrite
|
|
!insertmacro CockLogWriteBody
|
|
FunctionEnd
|
|
|
|
; CockLog for un.* code: same lines, prefixed call target.
|
|
!macro CockLogUn text
|
|
Push "${text}"
|
|
Call un.CockLogWrite
|
|
!macroend
|
|
|
|
Function un.CockLogWrite
|
|
!insertmacro CockLogWriteBody
|
|
FunctionEnd
|
|
|
|
; Logs every file matching $INSTDIR\<mask>. Used to record the DLL inventory
|
|
; before and after the upgrade purge, which is what shows whether Qt6Gui.dll was
|
|
; on disk and what (if anything) survived the deletes.
|
|
!macro CockLogListing label mask
|
|
Push "${label}"
|
|
Push "$INSTDIR\${mask}"
|
|
Call CockLogListing
|
|
!macroend
|
|
|
|
Function CockLogListing
|
|
Pop $LogMatchPattern
|
|
Pop $LogMatchLabel
|
|
!insertmacro CockLog "listing [$LogMatchLabel] pattern=$LogMatchPattern"
|
|
FindFirst $LogFindHandle $LogFindName "$LogMatchPattern"
|
|
${If} $LogFindHandle == ""
|
|
!insertmacro CockLog "listing [$LogMatchLabel] (no matches)"
|
|
Return
|
|
${EndIf}
|
|
log_listing_loop:
|
|
!insertmacro CockLog "listing [$LogMatchLabel] $LogFindName"
|
|
ClearErrors
|
|
FindNext $LogFindHandle $LogFindName
|
|
IfErrors 0 log_listing_loop
|
|
FindClose $LogFindHandle
|
|
FunctionEnd
|
|
|
|
Function .onInit
|
|
|
|
StrCpy $InstallLogPath "$TEMP\Cockatrice-installer.log"
|
|
; No backup has been taken yet; RestoreUserDataAfterUninstall must be a no-op
|
|
; until BackupUserDataBeforeUninstall sets this after actually copying something.
|
|
StrCpy $DataBackupDir ""
|
|
StrCpy $DataBackupDone ""
|
|
StrCpy $DataBackupForDir ""
|
|
StrCpy $LooseDecksBackupDone ""
|
|
StrCpy $InstallSubdirsBackupDone ""
|
|
StrCpy $LooseDeckNoticeShown ""
|
|
; 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).
|
|
StrCpy $OldInstallDir ""
|
|
StrCpy $OldUninstallExit ""
|
|
; 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.
|
|
StrCpy $StaleRuntimeAfterPurge 0
|
|
StrCpy $DllPurgeRan 0
|
|
; Start each run from a clean file so the log that gets sent back is the last
|
|
; run, not every past run glued together.
|
|
FileOpen $LogHandle "$InstallLogPath" w
|
|
FileClose $LogHandle
|
|
!insertmacro CockLog "=================================================================="
|
|
!insertmacro CockLog "installer start: @CPACK_PACKAGE_NAME@ @CPACK_PACKAGE_VERSION@"
|
|
|
|
${If} ${NSIS_IS_64_BIT} == 1 #NSIS 64bit
|
|
${IfNot} ${RunningX64}
|
|
MessageBox MB_OK|MB_ICONSTOP "This version of Cockatrice requires a 64-bit Windows system."
|
|
Abort
|
|
${EndIf}
|
|
StrCpy $NormalDestDir "$ProgramFiles64\Cockatrice"
|
|
SetRegView 64
|
|
${Else} #NSIS 32bit
|
|
${If} ${RunningX64}
|
|
MessageBox MB_OK|MB_ICONEXCLAMATION \
|
|
"You are about to install a 32-bit version of Cockatrice on a 64-bit Windows system.$\n\
|
|
We advise you to use the correct 64-bit installer instead to get around potential issues.$\n$\n\
|
|
Download from our webpage: https://cockatrice.github.io"
|
|
${EndIf}
|
|
StrCpy $NormalDestDir "$ProgramFiles\Cockatrice"
|
|
${EndIf}
|
|
|
|
StrCpy $PortableDestDir "$Desktop\CockatricePortable"
|
|
|
|
${GetParameters} $9
|
|
|
|
ClearErrors
|
|
${GetOptions} $9 "/?" $8
|
|
${IfNot} ${Errors}
|
|
MessageBox MB_ICONINFORMATION|MB_SETFOREGROUND "\
|
|
/PORTABLE : Install in portable mode$\n\
|
|
/S : Silent install$\n\
|
|
/R : Silent upgrade$\n\
|
|
/PID=%pid% : Process to wait for before replacing files (used by the in-app updater)$\n\
|
|
/D=%directory% : Specify destination directory$\n"
|
|
Quit
|
|
${EndIf}
|
|
|
|
ClearErrors
|
|
${GetOptions} $9 "/PORTABLE" $8
|
|
${IfNot} ${Errors}
|
|
StrCpy $PortableMode 1
|
|
StrCpy $0 $PortableDestDir
|
|
${Else}
|
|
StrCpy $PortableMode 0
|
|
StrCpy $0 $NormalDestDir
|
|
${If} ${Silent}
|
|
Call RequireAdmin
|
|
${EndIf}
|
|
${EndIf}
|
|
|
|
ClearErrors
|
|
${GetOptions} $9 "/R" $8
|
|
${IfNot} ${Errors}
|
|
StrCpy $ReinstallMode 1
|
|
SetSilent silent
|
|
SetAutoClose true
|
|
${Else}
|
|
StrCpy $ReinstallMode 0
|
|
${EndIf}
|
|
|
|
; /PID= carries the process id of the application that started this installer.
|
|
; The in-app updater launches the installer and only then quits itself, so
|
|
; without it the installer has to guess whether the launching process is still
|
|
; alive from the files it still has open - a guess that can come back "nothing
|
|
; is holding them" while the client is very much alive and still has its Qt DLLs
|
|
; mapped. An older client does not pass /PID= and an older installer ignores it,
|
|
; so both sides of the pair have to keep working.
|
|
; Note that /D= must stay the last parameter on the command line, which is why
|
|
; the client puts it last and this one anywhere.
|
|
StrCpy $WaitPid ""
|
|
StrCpy $LauncherExited 0
|
|
ClearErrors
|
|
${GetOptions} $9 "/PID=" $8
|
|
${IfNot} ${Errors}
|
|
StrCpy $WaitPid $8
|
|
${EndIf}
|
|
|
|
${If} $InstDir == ""
|
|
; User did not use /D to specify a directory,
|
|
; we need to set a default based on the install mode
|
|
StrCpy $InstDir $0
|
|
${EndIf}
|
|
|
|
; --- Detect portable install when using /R (must come BEFORE SetModeDestinationFromInstdir) ---
|
|
${If} $ReinstallMode = 1
|
|
IfFileExists "$InstDir\portable.dat" 0 not_portable
|
|
StrCpy $PortableMode 1
|
|
Goto portable_done
|
|
not_portable:
|
|
StrCpy $PortableMode 0
|
|
portable_done:
|
|
${EndIf}
|
|
|
|
; Now that $PortableMode reflects reality, commit InstDir into the correct slot
|
|
Call SetModeDestinationFromInstdir
|
|
|
|
!insertmacro CockLog "options: InstDir=$InstDir PortableMode=$PortableMode ReinstallMode=$ReinstallMode WaitPid=$WaitPid"
|
|
${If} ${Silent}
|
|
!insertmacro CockLog "options: running silently"
|
|
${Else}
|
|
!insertmacro CockLog "options: interactive"
|
|
${EndIf}
|
|
|
|
; Wait for the process that launched this installer to exit before anything is
|
|
; replaced or deleted. It goes first because a real handle on that process is
|
|
; authority: the holder probe that follows asks the same question indirectly, by
|
|
; looking at which files are still open.
|
|
;
|
|
; Then make sure no *other* application instance is still running (and holding
|
|
; file locks) before the previous version is uninstalled. The probe names them
|
|
; and the user is asked to close them; nothing is force-stopped without that
|
|
; answer, because Cockatrice may hold an unsaved deck and Oracle writes the card
|
|
; database in place. On /R the old version is about to be removed, so an answer
|
|
; the probe cannot give cancels the upgrade before it starts rather than after it.
|
|
; Only needed this early for /R, where AutoUninstallIfNeeded runs below; for
|
|
; interactive installs the section re-checks after the directory page, when
|
|
; $INSTDIR is the directory the user actually picked.
|
|
${If} $ReinstallMode = 1
|
|
!insertmacro CockLog "reinstall: waiting for launcher and other instances"
|
|
Call WaitForLauncherToExit
|
|
Call EnsureAppsNotRunning
|
|
${EndIf}
|
|
|
|
${If} $ReinstallMode = 1
|
|
${AndIf} $PortableMode = 0
|
|
!insertmacro CockLog "reinstall: auto-uninstalling previous version"
|
|
Call AutoUninstallIfNeeded
|
|
${EndIf}
|
|
|
|
FunctionEnd
|
|
|
|
Function un.onInit
|
|
|
|
${If} ${NSIS_IS_64_BIT} == 1
|
|
SetRegView 64
|
|
${EndIf}
|
|
|
|
; The uninstaller logs to its own file: the installer truncates its log on every run, so
|
|
; sharing one would mean either side erasing the other's account of a failed upgrade.
|
|
StrCpy $InstallLogPath "$TEMP\Cockatrice-uninstaller.log"
|
|
FileOpen $LogHandle "$InstallLogPath" w
|
|
FileClose $LogHandle
|
|
!insertmacro CockLogUn "=================================================================="
|
|
!insertmacro CockLogUn "uninstaller start: @CPACK_PACKAGE_NAME@ @CPACK_PACKAGE_VERSION@ dir=$INSTDIR"
|
|
|
|
; Ensure no application instance is still running before removing files.
|
|
Call un.EnsureAppsNotRunning
|
|
|
|
FunctionEnd
|
|
|
|
Function RequireAdmin
|
|
UserInfo::GetAccountType
|
|
Pop $8
|
|
${If} $8 != "admin"
|
|
MessageBox MB_ICONSTOP "You need administrator rights to install Cockatrice"
|
|
SetErrorLevel 740 ;ERROR_ELEVATION_REQUIRED
|
|
Abort
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
|
|
Function SetModeDestinationFromInstdir
|
|
${If} $PortableMode = 0
|
|
StrCpy $NormalDestDir $InstDir
|
|
${Else}
|
|
StrCpy $PortableDestDir $InstDir
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
Function SkipIfReinstall
|
|
${If} $ReinstallMode = 1
|
|
Abort
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
; Sets $OldUninstallExe to the executable named in $OldUninstallString (everything up to
|
|
; and including the first ".exe", without a leading quote), or "" when there is none.
|
|
Function ResolveOldUninstallExe
|
|
Push $R7
|
|
Push $R8
|
|
StrLen $R8 $OldUninstallString
|
|
StrCpy $R7 0
|
|
roue_loop:
|
|
${If} $R7 >= $R8
|
|
StrCpy $OldUninstallExe ""
|
|
Goto roue_out
|
|
${EndIf}
|
|
StrCpy $OldUninstallExe $OldUninstallString 4 $R7
|
|
${If} $OldUninstallExe == ".exe"
|
|
IntOp $R7 $R7 + 4
|
|
StrCpy $OldUninstallExe $OldUninstallString $R7
|
|
StrCpy $R8 $OldUninstallExe 1
|
|
${If} $R8 == '"'
|
|
StrCpy $OldUninstallExe $OldUninstallExe "" 1
|
|
${EndIf}
|
|
Goto roue_out
|
|
${EndIf}
|
|
IntOp $R7 $R7 + 1
|
|
Goto roue_loop
|
|
roue_out:
|
|
Pop $R8
|
|
Pop $R7
|
|
FunctionEnd
|
|
|
|
; Runs "$OldUninstallString _?=$OldInstallDir" in place and sets $OldUninstallExit:
|
|
; the uninstaller's own exit code when it ran, "missing" when the executable the
|
|
; registry names does not exist, "launch-failed" when ExecWait could not start it.
|
|
; The last two used to leave the code empty, which WaitForOldPayloadToDisappear read
|
|
; as a cancelled uninstall and aborted on, although nobody had cancelled anything.
|
|
; Uses $R5 for the exit code, as the callers did before.
|
|
Function RunOldUninstaller
|
|
Call ResolveOldUninstallExe
|
|
StrCpy $R5 ""
|
|
${If} $OldUninstallExe == ""
|
|
${OrIfNot} ${FileExists} "$OldUninstallExe"
|
|
StrCpy $OldUninstallExit "missing"
|
|
!insertmacro CockLog "old uninstall: uninstaller '$OldUninstallExe' does not exist (command '$OldUninstallString')"
|
|
Return
|
|
${EndIf}
|
|
ClearErrors
|
|
ExecWait '$OldUninstallString _?=$OldInstallDir' $R5
|
|
${If} ${Errors}
|
|
${OrIf} $R5 == ""
|
|
StrCpy $OldUninstallExit "launch-failed"
|
|
!insertmacro CockLog "old uninstall: could not start '$OldUninstallString _?=$OldInstallDir'"
|
|
Return
|
|
${EndIf}
|
|
StrCpy $OldUninstallExit $R5
|
|
FunctionEnd
|
|
|
|
Function AutoUninstallIfNeeded
|
|
|
|
SetShellVarContext all
|
|
|
|
; _?=$INSTDIR keeps the old uninstaller running in place. An NSIS uninstaller
|
|
; normally copies itself to %TEMP% and runs from there so that it can delete its
|
|
; own directory, and the process we started exits at once. ExecWait then returns
|
|
; before the uninstall has run, and the temp copy keeps deleting files - from
|
|
; 3.1.0 and 3.1.1 on disk a blanket "RMDir /r $INSTDIR" - while the section
|
|
; below is still writing the new payload. _?= disables the copy and makes
|
|
; ExecWait block until the uninstall finishes. It must be the last argument
|
|
; and must not be quoted, even when the path contains spaces.
|
|
;
|
|
; --- 32-bit uninstall ---
|
|
SetRegView 32
|
|
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
|
|
|
|
; On the /R path the updater passes /D=<appDir>, so $INSTDIR already is the old
|
|
; install dir. Publish it for the backup/restore pair below.
|
|
StrCpy $OldInstallDir $INSTDIR
|
|
!insertmacro CockLog "uninstall (32-bit): '$R0 _?=$OldInstallDir'"
|
|
StrCmp $R0 "" done32
|
|
DetailPrint "Removing previous version (32-bit)..."
|
|
Call BackupUserDataBeforeUninstall
|
|
StrCpy $OldUninstallString $R0
|
|
Call RunOldUninstaller
|
|
!insertmacro CockLog "uninstall (32-bit): exit=$OldUninstallExit"
|
|
Call WaitForOldPayloadToDisappear
|
|
Call RestoreUserDataAfterUninstall
|
|
|
|
done32:
|
|
|
|
; --- 64-bit uninstall ---
|
|
${If} ${RunningX64}
|
|
SetRegView 64
|
|
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
|
|
|
|
StrCpy $OldInstallDir $INSTDIR
|
|
!insertmacro CockLog "uninstall (64-bit): '$R0 _?=$OldInstallDir'"
|
|
StrCmp $R0 "" done64
|
|
DetailPrint "Removing previous version (64-bit)..."
|
|
Call BackupUserDataBeforeUninstall
|
|
StrCpy $OldUninstallString $R0
|
|
Call RunOldUninstaller
|
|
!insertmacro CockLog "uninstall (64-bit): exit=$OldUninstallExit"
|
|
Call WaitForOldPayloadToDisappear
|
|
Call RestoreUserDataAfterUninstall
|
|
|
|
done64:
|
|
${EndIf}
|
|
|
|
FunctionEnd
|
|
|
|
; Waits for the previous installation's payload to be gone, so the sections below never
|
|
; write into a directory an old uninstaller is still emptying.
|
|
;
|
|
; ExecWait only waits for as long as the uninstaller runs in place: an NSIS uninstaller
|
|
; that did not get a usable _?= copies itself to %TEMP%, starts that copy and exits, so
|
|
; 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
|
|
; 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.
|
|
Function WaitForOldPayloadToDisappear
|
|
${If} $OldInstallDir == ""
|
|
Return
|
|
${EndIf}
|
|
; The registered uninstaller could not be run at all (missing, or ExecWait could not
|
|
; start it): nothing is going to empty the directory, so waiting is pointless and
|
|
; aborting as "cancelled" was wrong. Remove the previous payload the same way the
|
|
; skipped-files recovery below does, and abort - naming why - only if that fails.
|
|
${If} $OldUninstallExit == "missing"
|
|
${OrIf} $OldUninstallExit == "launch-failed"
|
|
${IfNot} ${FileExists} "$OldInstallDir\*.*"
|
|
!insertmacro CockLog "old uninstall: uninstaller $OldUninstallExit and $OldInstallDir is already gone"
|
|
Return
|
|
${EndIf}
|
|
!insertmacro CockLog "old uninstall: uninstaller $OldUninstallExit ('$OldUninstallString'); removing $OldInstallDir payload directly"
|
|
Call AppendUninstallerLog
|
|
Call RemoveOldPayloadLeftovers
|
|
${If} $OldPayloadRemoved != "1"
|
|
StrCpy $AbortReason \
|
|
"The previous version in $OldInstallDir could not be removed: its uninstaller ($OldUninstallExe) could not be run ($OldUninstallExit), and removing the files directly failed.$\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}
|
|
!insertmacro CockLog "old uninstall: continuing after removing the payload of an uninstaller that could not run"
|
|
Return
|
|
${EndIf}
|
|
StrCpy $R2 0
|
|
old_uninstall_wait:
|
|
IfFileExists "$OldInstallDir\cockatrice.exe" 0 old_uninstall_gone
|
|
IntOp $R2 $R2 + 1
|
|
${If} $R2 > 60
|
|
!insertmacro CockLog "old uninstall: $OldInstallDir still holds cockatrice.exe after 30 s (uninstaller exit=$OldUninstallExit)"
|
|
; The uninstaller's own account of what it skipped or why it stopped lives in its
|
|
; separate log; copy it here so the report sent from AbortInstall carries it.
|
|
Call AppendUninstallerLog
|
|
${If} $OldUninstallExit != "0"
|
|
; Cancelled or failed with a non-zero exit code (an uninstaller that could not be
|
|
; started is handled above): 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}
|
|
Sleep 500
|
|
Goto old_uninstall_wait
|
|
old_uninstall_gone:
|
|
${If} $R2 > 0
|
|
; ExecWait returned while the old payload was still there: that uninstaller
|
|
; copied itself to %TEMP%, and the copy is still working through its file list.
|
|
; cockatrice.exe is gone before the rest of it, so let the copy settle before
|
|
; the sections below start writing files it would otherwise remove.
|
|
Sleep 1000
|
|
!insertmacro CockLog "old uninstall: ExecWait returned early, settled 1 s after $R2 * 500 ms"
|
|
${Else}
|
|
!insertmacro CockLog "old uninstall: $OldInstallDir was already clear when ExecWait returned"
|
|
${EndIf}
|
|
Return
|
|
FunctionEnd
|
|
|
|
; Called when the old uninstaller exited successfully but left its payload behind, or
|
|
; could not be run at all (missing or not startable) and the payload is still there.
|
|
; 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
|
|
|
|
; BackupUserDataBeforeUninstall / RestoreUserDataAfterUninstall
|
|
;
|
|
; The uninstallers shipped in 3.1.0/3.1.1 run a blanket "RMDir /r $INSTDIR".
|
|
; For a portable install that deletes $INSTDIR\data (decks, settings, pictures)
|
|
; along with the payload, and for a normal install it deletes anything the user
|
|
; pointed into $INSTDIR. The fixed uninstaller preserves foreign files, but the
|
|
; destructive one is already on disk on affected machines, so stash a copy
|
|
; outside $INSTDIR before invoking it and put it back afterwards. Best effort
|
|
; only: a failed backup must never abort the install.
|
|
;
|
|
; The backup is left in %TEMP% afterwards on a best-effort basis only: the next
|
|
; installer run that takes a new backup wipes it first (see below), so it is not
|
|
; a durable second copy. Anything this backup does not cover - e.g. loose files
|
|
; with other extensions - relies on the client's startup safety backup
|
|
; (warnAboutDataPathsInProgramDir), which only exists after the first launch of
|
|
; the new client.
|
|
;
|
|
; The 32-bit and 64-bit uninstall blocks below share $TEMP\CockatriceDataBackup.
|
|
; When both are registered with different InstallLocations, backing up the second
|
|
; directory into the first one's backup would restore the first dir's files into
|
|
; the second one. BackupUserDataBeforeUninstall therefore starts over when
|
|
; $OldInstallDir changes: the previous pass already restored its own backup, so
|
|
; only its leftover %TEMP% copy is lost. RelocateRestoredDataToInstDir only sees
|
|
; the last pass, so with two different old dirs plus a new target dir the first
|
|
; dir's restored data stays in place instead of moving along - no loss, but the
|
|
; user has to copy it manually.
|
|
;
|
|
; The portable-data branch below is defensive: both callers (AutoUninstallIfNeeded
|
|
; on /R, componentsPagePre interactively) only run for non-portable installs, and
|
|
; portable installs never write the registry uninstall keys that trigger them.
|
|
; It fires only when $OldInstallDir nevertheless contains portable.dat - e.g. a
|
|
; stale registry InstallLocation pointing at a portable dir, or portable.dat added
|
|
; to a normal install dir. The loose-decks and install-subdirs branches are the
|
|
; live ones for normal installs.
|
|
Function BackupUserDataBeforeUninstall
|
|
; $OldInstallDir is the directory being uninstalled: on the /R path it equals
|
|
; $INSTDIR (updater passes /D=<appDir>), on the interactive path componentsPagePre
|
|
; sets it from the registry InstallLocation via $GuardDir, because $INSTDIR is
|
|
; still the default there (directory page comes later).
|
|
${If} $OldInstallDir == ""
|
|
StrCpy $OldInstallDir $INSTDIR
|
|
${EndIf}
|
|
; %TEMP% survives across installer runs, so a previous run's safety copy would
|
|
; otherwise merge into this run: CopyFiles merges/overwrites, resurrecting files
|
|
; the user deleted since (e.g. deck A from run 1 reappearing alongside deck B
|
|
; from run 2) or restoring stale content after a failed copy. Wipe once per
|
|
; installer run - $DataBackupDir is empty until the first backup of this run,
|
|
; so the second call in sequence (32-bit then 64-bit uninstall) still merges.
|
|
${If} $DataBackupDir == ""
|
|
RMDir /r "$TEMP\CockatriceDataBackup"
|
|
${EndIf}
|
|
; The 32-bit and 64-bit uninstall blocks share one backup dir. When the second
|
|
; block points at a different directory, merging would restore the first dir's
|
|
; files into the second one, so start over instead. The previous pass already
|
|
; restored its own backup; only its leftover %TEMP% copy is discarded.
|
|
; StrCmp is case-sensitive while Windows paths are not, so a casing-only
|
|
; difference triggers a harmless redundant re-backup of the same content.
|
|
${If} $DataBackupDir != ""
|
|
${AndIf} $DataBackupForDir != ""
|
|
${AndIf} $OldInstallDir != $DataBackupForDir
|
|
!insertmacro CockLog "pre-uninstall backup: old dir changed ($DataBackupForDir -> $OldInstallDir), starting a fresh backup"
|
|
RMDir /r "$TEMP\CockatriceDataBackup"
|
|
StrCpy $DataBackupDir ""
|
|
StrCpy $DataBackupDone ""
|
|
StrCpy $LooseDecksBackupDone ""
|
|
StrCpy $InstallSubdirsBackupDone ""
|
|
${EndIf}
|
|
${If} ${FileExists} "$OldInstallDir\portable.dat"
|
|
${AndIf} ${FileExists} "$OldInstallDir\data"
|
|
StrCpy $DataBackupDir "$TEMP\CockatriceDataBackup"
|
|
StrCpy $DataBackupDone "1"
|
|
!insertmacro CockLog "pre-uninstall backup: copying $OldInstallDir\data (downloaded pics excluded)"
|
|
DetailPrint "Backing up portable user data before removing previous version..."
|
|
CreateDirectory "$DataBackupDir\data"
|
|
; Copy every top-level entry except pics (downloaded pictures are large and
|
|
; re-downloadable): copying everything first and deleting pics afterwards
|
|
; would duplicate gigabytes into %TEMP% just to throw them away.
|
|
FindFirst $DataBackupFindHandle $DataBackupFindName "$OldInstallDir\data\*.*"
|
|
${If} $DataBackupFindHandle != ""
|
|
backup_data_loop:
|
|
${If} $DataBackupFindName != "."
|
|
${AndIf} $DataBackupFindName != ".."
|
|
${AndIf} $DataBackupFindName != "pics"
|
|
${If} ${FileExists} "$OldInstallDir\data\$DataBackupFindName\*.*"
|
|
CreateDirectory "$DataBackupDir\data\$DataBackupFindName"
|
|
CopyFiles /SILENT "$OldInstallDir\data\$DataBackupFindName\*.*" "$DataBackupDir\data\$DataBackupFindName\"
|
|
${Else}
|
|
CopyFiles /SILENT "$OldInstallDir\data\$DataBackupFindName" "$DataBackupDir\data\"
|
|
${EndIf}
|
|
${EndIf}
|
|
ClearErrors
|
|
FindNext $DataBackupFindHandle $DataBackupFindName
|
|
IfErrors 0 backup_data_loop
|
|
FindClose $DataBackupFindHandle
|
|
${EndIf}
|
|
; Keep user-made custom pics (data\pics\CUSTOM).
|
|
${If} ${FileExists} "$OldInstallDir\data\pics\CUSTOM"
|
|
CreateDirectory "$DataBackupDir\data\pics\CUSTOM"
|
|
CopyFiles /SILENT "$OldInstallDir\data\pics\CUSTOM\*.*" "$DataBackupDir\data\pics\CUSTOM\"
|
|
${EndIf}
|
|
DetailPrint "Portable user data backup complete."
|
|
${EndIf}
|
|
; Deck extensions mirror DeckLoader::ACCEPTED_FILE_EXTENSIONS, minus *.txt: a
|
|
; blanket *.txt copy would also grab payload-adjacent text files, while the
|
|
; deck-specific extensions are unambiguous user content.
|
|
${If} ${FileExists} "$OldInstallDir\*.cod"
|
|
${OrIf} ${FileExists} "$OldInstallDir\*.dec"
|
|
${OrIf} ${FileExists} "$OldInstallDir\*.dek"
|
|
${OrIf} ${FileExists} "$OldInstallDir\*.mwDeck"
|
|
${If} $DataBackupDir == ""
|
|
StrCpy $DataBackupDir "$TEMP\CockatriceDataBackup"
|
|
${EndIf}
|
|
StrCpy $LooseDecksBackupDone "1"
|
|
!insertmacro CockLog "pre-uninstall backup: copying loose deck files"
|
|
DetailPrint "Backing up deck files stored in the install directory..."
|
|
CreateDirectory "$DataBackupDir\loose_decks"
|
|
CopyFiles /SILENT "$OldInstallDir\*.cod" "$DataBackupDir\loose_decks\"
|
|
CopyFiles /SILENT "$OldInstallDir\*.dec" "$DataBackupDir\loose_decks\"
|
|
CopyFiles /SILENT "$OldInstallDir\*.dek" "$DataBackupDir\loose_decks\"
|
|
CopyFiles /SILENT "$OldInstallDir\*.mwDeck" "$DataBackupDir\loose_decks\"
|
|
${EndIf}
|
|
; Data folders below the top level (e.g. $OldInstallDir\decks) are deleted by the
|
|
; old "RMDir /r $INSTDIR" just like loose decks, and on a 3.1.1 -> new upgrade the
|
|
; client's startup backup has never run yet, so stash every top-level directory
|
|
; that is not part of the payload. Payload dirs stay in place: they are rewritten
|
|
; by the section below, and restoring them could put stale DLLs next to the new
|
|
; executables. "data" is skipped once the portable branch above already took it;
|
|
; without portable.dat it is user content like any other unknown dir.
|
|
FindFirst $DataBackupFindHandle $DataBackupFindName "$OldInstallDir\*.*"
|
|
${If} $DataBackupFindHandle != ""
|
|
backup_subdirs_loop:
|
|
${If} $DataBackupFindName != "."
|
|
${AndIf} $DataBackupFindName != ".."
|
|
${AndIf} $DataBackupFindName != "plugins"
|
|
${AndIf} $DataBackupFindName != "Plugins"
|
|
${AndIf} $DataBackupFindName != "translations"
|
|
${AndIf} $DataBackupFindName != "sounds"
|
|
${AndIf} $DataBackupFindName != "themes"
|
|
${If} $DataBackupFindName == "data"
|
|
${AndIf} $DataBackupDone == "1"
|
|
Goto backup_subdirs_next
|
|
${EndIf}
|
|
${If} ${FileExists} "$OldInstallDir\$DataBackupFindName\*.*"
|
|
${If} $DataBackupDir == ""
|
|
StrCpy $DataBackupDir "$TEMP\CockatriceDataBackup"
|
|
${EndIf}
|
|
StrCpy $InstallSubdirsBackupDone "1"
|
|
!insertmacro CockLog "pre-uninstall backup: copying subdir $OldInstallDir\$DataBackupFindName"
|
|
DetailPrint "Backing up $DataBackupFindName stored in the install directory..."
|
|
CreateDirectory "$DataBackupDir\install_subdirs\$DataBackupFindName"
|
|
CopyFiles /SILENT "$OldInstallDir\$DataBackupFindName\*.*" "$DataBackupDir\install_subdirs\$DataBackupFindName\"
|
|
${EndIf}
|
|
${EndIf}
|
|
backup_subdirs_next:
|
|
ClearErrors
|
|
FindNext $DataBackupFindHandle $DataBackupFindName
|
|
IfErrors 0 backup_subdirs_loop
|
|
FindClose $DataBackupFindHandle
|
|
${EndIf}
|
|
; Remember which directory the current backup belongs to, so a later pass for a
|
|
; different directory starts over instead of merging (see above).
|
|
${If} $DataBackupDir != ""
|
|
StrCpy $DataBackupForDir $OldInstallDir
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
Function RestoreUserDataAfterUninstall
|
|
StrCmp $DataBackupDir "" done
|
|
${If} $OldInstallDir == ""
|
|
StrCpy $OldInstallDir $INSTDIR
|
|
${EndIf}
|
|
${If} $DataBackupDone == "1"
|
|
${AndIf} ${FileExists} "$DataBackupDir\data"
|
|
${If} ${FileExists} "$OldInstallDir\data"
|
|
; Existence-only: a partially-deleted data dir (locked files during the old
|
|
; uninstall) looks the same as a survived one, so the backup is always kept.
|
|
; Merge the backup back over it to repair partial deletes; CopyFiles only
|
|
; adds/overwrites, so a fully-survived dir is unaffected.
|
|
!insertmacro CockLog "post-uninstall restore: $OldInstallDir\data still exists, merging backup and keeping it"
|
|
CopyFiles /SILENT "$DataBackupDir\data\*.*" "$OldInstallDir\data\"
|
|
DetailPrint "Previous user data is still in place; pre-uninstall backup merged back and kept at $DataBackupDir\data."
|
|
${Else}
|
|
!insertmacro CockLog "post-uninstall restore: restoring $OldInstallDir\data"
|
|
CreateDirectory "$OldInstallDir\data"
|
|
CopyFiles /SILENT "$DataBackupDir\data\*.*" "$OldInstallDir\data\"
|
|
DetailPrint "Restored portable user data from pre-uninstall backup."
|
|
${EndIf}
|
|
${EndIf}
|
|
${If} $LooseDecksBackupDone == "1"
|
|
${AndIf} ${FileExists} "$DataBackupDir\loose_decks"
|
|
!insertmacro CockLog "post-uninstall restore: restoring loose deck files"
|
|
CreateDirectory "$OldInstallDir"
|
|
CopyFiles /SILENT "$DataBackupDir\loose_decks\*.cod" "$OldInstallDir\"
|
|
CopyFiles /SILENT "$DataBackupDir\loose_decks\*.dec" "$OldInstallDir\"
|
|
CopyFiles /SILENT "$DataBackupDir\loose_decks\*.dek" "$OldInstallDir\"
|
|
CopyFiles /SILENT "$DataBackupDir\loose_decks\*.mwDeck" "$OldInstallDir\"
|
|
DetailPrint "Restored deck files stored in the install directory."
|
|
${EndIf}
|
|
${If} $InstallSubdirsBackupDone == "1"
|
|
${AndIf} ${FileExists} "$DataBackupDir\install_subdirs"
|
|
!insertmacro CockLog "post-uninstall restore: restoring install subdirectories"
|
|
CreateDirectory "$OldInstallDir"
|
|
FindFirst $DataBackupFindHandle $DataBackupFindName "$DataBackupDir\install_subdirs\*.*"
|
|
${If} $DataBackupFindHandle != ""
|
|
restore_subdirs_loop:
|
|
${If} $DataBackupFindName != "."
|
|
${AndIf} $DataBackupFindName != ".."
|
|
${AndIf} ${FileExists} "$DataBackupDir\install_subdirs\$DataBackupFindName\*.*"
|
|
!insertmacro CockLog "post-uninstall restore: restoring subdir $OldInstallDir\$DataBackupFindName"
|
|
CreateDirectory "$OldInstallDir\$DataBackupFindName"
|
|
CopyFiles /SILENT "$DataBackupDir\install_subdirs\$DataBackupFindName\*.*" "$OldInstallDir\$DataBackupFindName\"
|
|
${EndIf}
|
|
ClearErrors
|
|
FindNext $DataBackupFindHandle $DataBackupFindName
|
|
IfErrors 0 restore_subdirs_loop
|
|
FindClose $DataBackupFindHandle
|
|
${EndIf}
|
|
DetailPrint "Restored folders stored in the install directory."
|
|
${EndIf}
|
|
; Runs once per uninstall block, and there are two (32-bit, 64-bit): show the
|
|
; notice a single time per installer run, covering both loose decks and
|
|
; restored subdirectories.
|
|
${IfNot} ${Silent}
|
|
${AndIf} $LooseDeckNoticeShown != "1"
|
|
${If} $LooseDecksBackupDone == "1"
|
|
${OrIf} $InstallSubdirsBackupDone == "1"
|
|
StrCpy $LooseDeckNoticeShown "1"
|
|
MessageBox MB_OK|MB_ICONINFORMATION "Your decks or data folders were stored inside the Cockatrice install folder ($OldInstallDir), which the previous version's uninstaller deletes.$\n$\nThey have been restored from a backup to that folder (loose deck files ending in .txt are not backed up - move those manually). If you install to a different folder, they are moved there automatically. Please move them out of the install folder afterwards (Settings -> General -> Paths) so a future uninstall cannot delete them again."
|
|
${EndIf}
|
|
${EndIf}
|
|
done:
|
|
FunctionEnd
|
|
|
|
; RelocateRestoredDataToInstDir
|
|
;
|
|
; componentsPagePre backs up and restores around the old uninstaller before the
|
|
; directory page, so $OldInstallDir (registry InstallLocation) is authoritative
|
|
; there while $INSTDIR is still the default. When the user then picks a different
|
|
; target directory, the restored user data would otherwise stay orphaned in the
|
|
; old directory while the new install starts empty. Copy it across when they differ
|
|
; (a copy, not a move: the source is user data and is deliberately left in place).
|
|
; Only the last uninstall pass's $OldInstallDir is relocated (see the shared-backup
|
|
; comment above); with two different old dirs the first one's data stays restored
|
|
; in place instead of moving along.
|
|
; StrCmp is case-sensitive while Windows paths are not; a casing-only difference
|
|
; triggers a harmless redundant copy onto itself.
|
|
Function RelocateRestoredDataToInstDir
|
|
StrCmp $DataBackupDir "" done
|
|
StrCmp $OldInstallDir "" done
|
|
StrCmp $OldInstallDir $INSTDIR done
|
|
${If} $DataBackupDone == "1"
|
|
${AndIf} ${FileExists} "$OldInstallDir\data"
|
|
!insertmacro CockLog "relocate: copying restored data $OldInstallDir\data -> $INSTDIR\data"
|
|
CreateDirectory "$INSTDIR\data"
|
|
CopyFiles /SILENT "$OldInstallDir\data\*.*" "$INSTDIR\data\"
|
|
DetailPrint "Copied restored user data to the new install folder."
|
|
${EndIf}
|
|
${If} $LooseDecksBackupDone == "1"
|
|
${If} ${FileExists} "$OldInstallDir\*.cod"
|
|
${OrIf} ${FileExists} "$OldInstallDir\*.dec"
|
|
${OrIf} ${FileExists} "$OldInstallDir\*.dek"
|
|
${OrIf} ${FileExists} "$OldInstallDir\*.mwDeck"
|
|
!insertmacro CockLog "relocate: copying restored decks $OldInstallDir -> $INSTDIR"
|
|
CreateDirectory "$INSTDIR"
|
|
CopyFiles /SILENT "$OldInstallDir\*.cod" "$INSTDIR\"
|
|
CopyFiles /SILENT "$OldInstallDir\*.dec" "$INSTDIR\"
|
|
CopyFiles /SILENT "$OldInstallDir\*.dek" "$INSTDIR\"
|
|
CopyFiles /SILENT "$OldInstallDir\*.mwDeck" "$INSTDIR\"
|
|
DetailPrint "Copied restored deck files to the new install folder."
|
|
${EndIf}
|
|
${EndIf}
|
|
${If} $InstallSubdirsBackupDone == "1"
|
|
${AndIf} ${FileExists} "$DataBackupDir\install_subdirs"
|
|
FindFirst $DataBackupFindHandle $DataBackupFindName "$DataBackupDir\install_subdirs\*.*"
|
|
${If} $DataBackupFindHandle != ""
|
|
relocate_subdirs_loop:
|
|
${If} $DataBackupFindName != "."
|
|
${AndIf} $DataBackupFindName != ".."
|
|
${AndIf} ${FileExists} "$OldInstallDir\$DataBackupFindName\*.*"
|
|
!insertmacro CockLog "relocate: copying restored subdir $OldInstallDir\$DataBackupFindName -> $INSTDIR\$DataBackupFindName"
|
|
CreateDirectory "$INSTDIR\$DataBackupFindName"
|
|
CopyFiles /SILENT "$OldInstallDir\$DataBackupFindName\*.*" "$INSTDIR\$DataBackupFindName\"
|
|
DetailPrint "Copied restored folder $DataBackupFindName to the new install folder."
|
|
${EndIf}
|
|
ClearErrors
|
|
FindNext $DataBackupFindHandle $DataBackupFindName
|
|
IfErrors 0 relocate_subdirs_loop
|
|
FindClose $DataBackupFindHandle
|
|
${EndIf}
|
|
${EndIf}
|
|
done:
|
|
FunctionEnd
|
|
|
|
; --- Running instance handling ---
|
|
; Cockatrice, Oracle and Servatrice must not be running while files are
|
|
; replaced or deleted. A still-running process holds locks on its .exe and
|
|
; Qt runtime DLLs, so a silent upgrade could otherwise end up with a mix of
|
|
; old and new Qt DLLs next to the new executable, failing with
|
|
; "The procedure entry point X could not be located in the dynamic link
|
|
; library ...Qt6Network.dll" on the next start.
|
|
;
|
|
; Both questions this installer has about running processes - "may I replace
|
|
; the payload?" and "what is holding this file I could not delete?" - are
|
|
; answered by tools/installer-holder-probe, a small executable built from this
|
|
; repository and embedded in the installer and the uninstaller alike. It names
|
|
; every process and service that has one of the payload files open or mapped
|
|
; (the Restart Manager, RmRegisterResources/RmGetList) and, separately, every
|
|
; process whose image lives in the directory being replaced.
|
|
;
|
|
; The second lookup is not redundant. Restart Manager answers "who has this
|
|
; file open", and a running instance does not have to have one open: its own
|
|
; image and the modules it loaded are mapped into it rather than opened by it,
|
|
; and a mapped image is not reliably reported. That is the state the failed
|
|
; updates this probe exists for were in - every DLL still on disk, an instance
|
|
; running from the directory, and "nothing is holding them any more" as the
|
|
; answer. A guard built on file handles alone therefore misses exactly the
|
|
; instance it exists to find; it would pass, the purge would fail, and the
|
|
; install would end in a warning with no offer to close and try again.
|
|
;
|
|
; Why not enumerate processes from this script: the NSIS stub is a 32-bit
|
|
; process, so a bare "powershell" started from it is the 32-bit Windows
|
|
; PowerShell, whose Get-Process cannot read the path of a 64-bit process. A
|
|
; guard built on that misses exactly the running instance it exists to find.
|
|
; The probe opens each process with PROCESS_QUERY_LIMITED_INFORMATION and calls
|
|
; QueryFullProcessImageNameW, which has no bitness mismatch, and Restart
|
|
; Manager is a system API rather than a view of this process's architecture.
|
|
;
|
|
; The files that count are the payload the purge removes and the installer
|
|
; replaces: the three executables, the top-level DLLs, qt.conf and everything
|
|
; below Plugins. A document, a log or a screenshot the user happens to have
|
|
; open in the install directory is not part of it and cannot block an upgrade;
|
|
; the image lookup matches on the directory a process runs from, so it does not
|
|
; widen that set.
|
|
;
|
|
; Nothing is force-stopped without the user asking for it: Cockatrice may be holding
|
|
; an unsaved deck or showing a "really leave this game?" confirmation, Oracle writes
|
|
; the card database in place and can be killed mid-write, and Servatrice may be serving
|
|
; a game. A close is a request (WM_CLOSE), made automatically once because that is what
|
|
; the user started the installer to do, and only Cockatrice can answer one - the other
|
|
; two have no window. When the close is not answered the user is told which program is
|
|
; in the way and is offered to end all three themselves - the last resort, with what it
|
|
; costs spelled out in the prompt.
|
|
|
|
; One sentence for every prompt whose Yes ends the whole set, so that answer reads the same
|
|
; wherever the user meets it: this guard before anything is replaced, the uninstaller's, and
|
|
; the recovery prompt after a failed purge.
|
|
!define END_COST "An unsaved deck, a running game or an unwritten setting is lost, Servatrice drops its players, and an Oracle that is mid-write can damage the card database."
|
|
|
|
; The other half of that answer - what is left to do when nothing is ended - is shared by
|
|
; every prompt that names the holders, for the same reason.
|
|
!define AFTER_CLOSING_GUIDANCE "Answer the confirmation Cockatrice may be showing - open it from the notification area if it is hidden there - and use Task Manager for a program that would not end."
|
|
|
|
; usage: set the variable named by the second argument to the probe's listing, then call
|
|
; this. Sets $GuardPrompt to the question the guard, the uninstaller's guard and the
|
|
; recovery path all ask.
|
|
;
|
|
; One macro rather than three copies, because the wording is a promise: Yes ends the
|
|
; programs the payload installs - which is exactly the set EndHolders acts on - and anything
|
|
; else the probe named has to be closed by hand, so a listing that includes a scanner or a
|
|
; service hosted elsewhere does not come with an answer that cannot do what it says.
|
|
;
|
|
; The assembled string has to fit the characters one NSIS string holds, which is what
|
|
; bounds HOLDER_LINE_CHAR_MAX in tools\installer-holder-probe\holder_line.h: with the
|
|
; longest listing that can arrive and the sentence only a failed end adds, it stays inside
|
|
; that budget instead of being cut off at the tail, where the Cancel line is.
|
|
;
|
|
; @param header the sentence that introduces the listing, with its trailing colon
|
|
; @param listing the variable holding the probe's line, $GuardOutput or $HolderOutput
|
|
; @param cancelWord what Cancel gives up, "installation" or "uninstallation"
|
|
!macro CockEndPrompt header listing cancelWord
|
|
StrCpy $GuardPrompt "${header}$\r$\n$\r$\n${listing}"
|
|
; Said only after the previous answer was Yes: without it the same dialog reappears word
|
|
; for word, which is what "nothing happened" looked like - the end can fail on a process
|
|
; the installer may not open, and it never touches a program that is not one of ours.
|
|
${If} $GuardEndFailed = 1
|
|
StrCpy $GuardPrompt "$GuardPrompt$\r$\n$\r$\nEnding them did not free the files."
|
|
${EndIf}
|
|
StrCpy $GuardPrompt "$GuardPrompt$\r$\n$\r$\nChoose Yes to end the Cockatrice, Oracle and Servatrice programs listed above. ${END_COST}$\r$\n$\r$\nChoose No to try again after closing anything else listed above by hand. ${AFTER_CLOSING_GUIDANCE}$\r$\n$\r$\nChoose Cancel to cancel the ${cancelWord}."
|
|
!macroend
|
|
|
|
; Emitted once for the installer and once for the uninstaller: the uninstaller
|
|
; cannot call installer functions, and its own copy of the probe keeps a running
|
|
; 64-bit instance from being missed there too.
|
|
!macro CockHolderProbeFunctions prefix tag
|
|
Function ${prefix}ExtractHolderProbe
|
|
InitPluginsDir
|
|
IfFileExists "$PLUGINSDIR\installer-holder-probe.exe" ck_probe_ready_${tag}
|
|
File "/oname=$PLUGINSDIR\installer-holder-probe.exe" "..\..\..\installer-holder-probe.exe"
|
|
ck_probe_ready_${tag}:
|
|
FunctionEnd
|
|
|
|
; usage: set $GuardDir to the directory to check, then call this. Sets
|
|
; $GuardExit to the probe's exit code and $GuardOutput to its one-line answer.
|
|
; 0 = nothing holds the payload and nothing runs from the directory, 1 = the
|
|
; line names what does, anything else = the probe could not answer (2 = no
|
|
; directory was given, 3 = the Restart Manager call failed and no process runs
|
|
; from the directory either, "timeout" = it did not return in time).
|
|
Function ${prefix}RunHolderProbe
|
|
Call ${prefix}ExtractHolderProbe
|
|
; The directory travels in an environment variable, never on the command
|
|
; line: a path with spaces, quotes or a trailing backslash then does not
|
|
; have to survive NSIS quoting.
|
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
|
; /TIMEOUT because this can run before the first page: a Restart Manager
|
|
; that does not answer must not leave the installer with no window and no
|
|
; way out.
|
|
nsExec::ExecToStack /TIMEOUT=20000 '"$PLUGINSDIR\installer-holder-probe.exe"'
|
|
Pop $GuardExit
|
|
Pop $GuardOutput
|
|
FunctionEnd
|
|
|
|
; usage: set $GuardDir to the directory being replaced, then call this. Asks the running
|
|
; Cockatrice to close, and waits for it to exit.
|
|
;
|
|
; The set is what the probe names: the processes holding a file of the version being
|
|
; replaced, plus the processes running from the directory. tools\installer-holder-probe
|
|
; closes that set by pid, which is why nothing here has to match an executable path. Only
|
|
; cockatrice.exe is asked: Oracle writes the card database in place and Servatrice has no
|
|
; window to ask, so both are left alone here and only ended by the explicit answer.
|
|
;
|
|
; Each instance is brought to the foreground before WM_CLOSE is posted to its main window.
|
|
; WM_CLOSE is answered, not obeyed - Cockatrice asks before it quits while a card database
|
|
; update is running, a game is open or a deck is unsaved - and that question is only useful
|
|
; if it is in front of the installer instead of behind it.
|
|
;
|
|
; The probe waits up to 30 s for the processes it asked to exit, so the check that follows
|
|
; measures a settled directory rather than sleeping for a fixed time. Its line goes into the
|
|
; log as it stands: it names every holder, including the ones left to the user, so a close
|
|
; that did nothing can be told apart from one that was never sent.
|
|
;
|
|
; Never a force-close here: the application decides whether to quit. Ending is a separate
|
|
; answer the guard prompt offers when a close did not free the files, spelled out with what
|
|
; it costs - see EndHolders - and it ends every program the payload installs.
|
|
Function ${prefix}CloseHolders
|
|
Call ${prefix}ExtractHolderProbe
|
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
|
; The probe waits 30 s itself. The timeout here only bounds a probe that never returns;
|
|
; the Restart Manager call inside it is the same one the holder listing makes.
|
|
nsExec::ExecToStack /TIMEOUT=40000 '"$PLUGINSDIR\installer-holder-probe.exe" --close-holders'
|
|
Pop $R3
|
|
Pop $R4
|
|
; Not the CockLog macro: this function exists twice and the uninstaller copy may only
|
|
; Call un. functions, so the macro's two lines are spelled with the prefix here.
|
|
Push "close-holders: probe exit=$R3 [$R4]"
|
|
Call ${prefix}CockLogWrite
|
|
DetailPrint "Closing Cockatrice: $R4"
|
|
FunctionEnd
|
|
|
|
; usage: set $GuardDir to the directory being replaced, then call this. Ends the running
|
|
; Cockatrice, Oracle and Servatrice instances the probe names.
|
|
;
|
|
; This is the only place anything is stopped without being asked to stop, so it is only
|
|
; reached after the user picked it from a prompt that says what it costs: a client that is
|
|
; still running may have an unsaved deck, a game or a settings write in flight, Oracle may
|
|
; be part-way through writing the card database, and Servatrice may be serving a game. The
|
|
; probe ends the three programs the payload installs, services included, and nothing else:
|
|
; a scanner or a program the user keeps in the directory is named and left to the user.
|
|
Function ${prefix}EndHolders
|
|
Call ${prefix}ExtractHolderProbe
|
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
|
; The probe waits up to 5 s for what it ended to be gone, so the check that follows
|
|
; measures a settled directory instead of racing the processes it just terminated.
|
|
nsExec::ExecToStack /TIMEOUT=20000 '"$PLUGINSDIR\installer-holder-probe.exe" --end-holders'
|
|
Pop $R3
|
|
Pop $R4
|
|
Push "end-holders: probe exit=$R3 [$R4]"
|
|
Call ${prefix}CockLogWrite
|
|
DetailPrint "Ending Cockatrice, Oracle and Servatrice: $R4"
|
|
FunctionEnd
|
|
!macroend
|
|
|
|
!insertmacro CockHolderProbeFunctions "" install
|
|
!insertmacro CockHolderProbeFunctions "un." uninstall
|
|
|
|
; usage: set $GuardDir to the directory to guard, then call this.
|
|
;
|
|
; Asks Cockatrice to close once, then prompts until the payload is free. The automatic
|
|
; close is the same request clicking the window's close button makes, so the common case -
|
|
; the client was still shutting down - never turns into a question. The prompt is what
|
|
; remains when the close cannot reach everything: Oracle writes the card database in
|
|
; place, Servatrice has no window, and Cockatrice may still be waiting for an answer in
|
|
; its own confirmation. It offers to end the programs the payload installs - and only those,
|
|
; which the text says - with what that costs (Yes), a plain re-check for after the user
|
|
; closed things by hand (No), and Cancel - ending is the last resort, see EndHolders.
|
|
;
|
|
; An error is always shown, in every mode. A /S install without /R has nobody at
|
|
; the keyboard, but this installer does not promise unattended runs: when an
|
|
; error needs a decision, asking for one is better than silently leaving a
|
|
; half-updated install behind. The happy path has no dialog, so a /S reinstall
|
|
; that has nothing to report stays silent.
|
|
Function GuardAppsNotRunning
|
|
!insertmacro CockLog "guard: checking $GuardDir (launcherExited=$LauncherExited)"
|
|
StrCpy $GuardCloseTried 0
|
|
StrCpy $GuardEndFailed 0
|
|
guard_check:
|
|
Call RunHolderProbe
|
|
${If} $GuardExit == "0"
|
|
!insertmacro CockLog "guard: nothing is holding the payload"
|
|
DetailPrint "No running Cockatrice, Oracle or Servatrice instance was found."
|
|
Return
|
|
${EndIf}
|
|
${If} $GuardExit == "1"
|
|
!insertmacro CockLog "guard: still in use by $GuardOutput"
|
|
; Close it for the user first. The probe knows exactly which cockatrice.exe holds
|
|
; these files, and asking it to close is the same request as clicking the window's
|
|
; close button - the application still answers its own "really quit?" prompt, so
|
|
; nothing is lost. One attempt per guard call keeps the common case (the client was
|
|
; still shutting down) from turning into a question, without hiding the case where
|
|
; the close did not help: the pass after it names what is still holding the files.
|
|
${If} $GuardCloseTried = 0
|
|
StrCpy $GuardCloseTried 1
|
|
!insertmacro CockLog "guard: asking cockatrice to close, then checking again"
|
|
Call CloseHolders
|
|
Goto guard_check
|
|
${EndIf}
|
|
DetailPrint "The install directory is in use by: $GuardOutput"
|
|
; The automatic close already ran once. Asking Cockatrice to close again is unlikely
|
|
; to help: an instance that did not answer the first one is usually sitting on a
|
|
; confirmation of its own, and the close probe leaves such an instance alone so that a
|
|
; second WM_CLOSE cannot hide the window the question is in - which is how a pre-3.1.2
|
|
; update used to leave a headless process behind. An instance with no window has
|
|
; nothing to ask at all. Ending is what is left that can free the files, so the prompt
|
|
; offers it here, with what it costs in the text: an unsaved deck, a game, a settings
|
|
; write, and an Oracle that may be mid-way through the card database. The answers that
|
|
; end nothing stay reachable from the same dialog: the instance's own confirmation,
|
|
; and closing anything by hand.
|
|
!insertmacro CockEndPrompt "Cockatrice, Oracle or Servatrice is still using files of the installation:" $GuardOutput "installation"
|
|
MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON2 "$GuardPrompt" IDYES guard_end IDNO guard_check
|
|
!insertmacro CockLog "guard: cancelled by user"
|
|
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."
|
|
; 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
|
|
; Only reached through Yes: AbortInstall does not return.
|
|
guard_end:
|
|
!insertmacro CockLog "guard: ending the payload programs on request, then checking again"
|
|
Call EndHolders
|
|
; Everything the payload installs is ended, but a program that could not be opened
|
|
; (another account, a protected process) survives, and a program that is not one of
|
|
; ours is never touched at all, so what is left may still have to be closed by hand.
|
|
; Go back to the same prompt, which says the end did not free the files.
|
|
StrCpy $GuardEndFailed 1
|
|
Goto guard_check
|
|
${EndIf}
|
|
; Not an answer at all: the probe was not configured, the Restart Manager
|
|
; call failed and nothing runs from the directory, or it timed out. Carrying
|
|
; on from that is a decision, so it is asked for rather than assumed - the
|
|
; stale-runtime check after the purge still stands between a failed guard
|
|
; and a mismatched install, but it only fires after files have been
|
|
; replaced, and on an upgrade the launcher asked for nobody is watching the
|
|
; installer's exit code. Retry asks the probe again (a Restart Manager that
|
|
; was busy answers the second time), Ignore installs anyway, Abort cancels
|
|
; instead of writing a payload next to files the guard could not rule out.
|
|
!insertmacro CockLog "guard: the holder probe could not answer ($GuardExit): $GuardOutput"
|
|
DetailPrint "Could not check whether Cockatrice is still running: $GuardOutput"
|
|
MessageBox MB_ABORTRETRYIGNORE|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
|
|
"This installer could not check whether Cockatrice, Oracle or Servatrice is still running.$\r$\n$\r$\nThe check reported: $GuardOutput$\r$\n$\r$\nClose them, then choose Retry to check again. Choose Ignore to install anyway, or Abort to cancel the installation." \
|
|
IDRETRY guard_check IDABORT guard_unknown_abort
|
|
!insertmacro CockLog "guard: continuing without an answer from the probe"
|
|
Return
|
|
guard_unknown_abort:
|
|
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."
|
|
; 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
|
|
FunctionEnd
|
|
|
|
Function EnsureAppsNotRunning
|
|
; guards the install directory
|
|
StrCpy $GuardDir $INSTDIR
|
|
Call GuardAppsNotRunning
|
|
FunctionEnd
|
|
|
|
; Aborts the installation, showing $AbortReason to the user.
|
|
;
|
|
; Every abort goes through here so no failure ends silently: the in-app updater
|
|
; runs this installer silent and detached and never looks at its exit code, and
|
|
; a /S install without /R has nobody watching its exit code either, so a plain
|
|
; Abort would leave the user with no update and no explanation. An error always
|
|
; shows its dialog, in every mode; a silent run stays silent only for as long
|
|
; as nothing goes wrong. The log path is part of the dialog, so a report can be
|
|
; acted on without asking the user to find the file.
|
|
Function AbortInstall
|
|
!insertmacro CockLog "ABORT: $AbortReason"
|
|
!insertmacro CockLog "full installer log: $InstallLogPath"
|
|
MessageBox MB_OK|MB_ICONEXCLAMATION "$AbortReason$\r$\n$\r$\nA detailed installer log was written to:$\r$\n$InstallLogPath"
|
|
; Offer to send the installer log to Sentry so aborted installs can be diagnosed.
|
|
; Only a package built with a baked DSN offers to send, so the prompt can only appear
|
|
; when a send can actually go out. A preprocessor guard rather than a runtime test: a
|
|
; package built without a DSN compiles the block out entirely, so it cannot even carry
|
|
; the helper or its baked endpoint. No /SD: the abort is exactly the case the offer
|
|
; exists for, including on the silent /R upgrade.
|
|
!if "@CPACK_SENTRY_ENABLED@" == "1"
|
|
MessageBox MB_YESNO|MB_ICONQUESTION "Send the installer log to the developers to help fix this? The log contains file paths from this machine, which may include your Windows user name." IDYES sentry_send
|
|
!insertmacro CockLog "sentry send: declined by user"
|
|
Goto sentry_done
|
|
sentry_send:
|
|
; The abort reason is already in the log file (CockLog writes "ABORT: ..." as the
|
|
; first thing AbortInstall does), so the helper reads it from there. Nothing
|
|
; reason-shaped crosses the command line, which keeps quoting trivial and needs
|
|
; no NSIS string functions.
|
|
InitPluginsDir
|
|
; Embedded by the same relative path that resolves NSIS.definitions.nsh above (both are
|
|
; configure_file'd into the build root, which is three levels up from project.nsi). An
|
|
; absolute CMake path here carries forward slashes, which makensis does not treat as
|
|
; absolute and so fails to find the file at package time.
|
|
File "/oname=$PLUGINSDIR\send-installer-log.ps1" "..\..\..\SendInstallerLog.ps1"
|
|
; The send can take up to the /TIMEOUT below, so put something on the details pane
|
|
; the moment the user says yes rather than leaving the window blank until it returns.
|
|
DetailPrint "Sending installer log..."
|
|
nsExec::ExecToStack /TIMEOUT=60000 'powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\send-installer-log.ps1" -LogPath "$InstallLogPath"'
|
|
Pop $SentryResult
|
|
Pop $SentryOutput
|
|
; $SentryOutput is a single line by construction: the helper flattens its message
|
|
; and this log file is uploaded verbatim, so a stray newline must not be able to
|
|
; forge extra lines into the artifact we send.
|
|
!insertmacro CockLog "sentry send: result=$SentryResult output=$SentryOutput"
|
|
${If} $SentryResult == "0"
|
|
MessageBox MB_OK|MB_ICONINFORMATION "The installer log was sent. Thank you for helping improve Cockatrice!"
|
|
${Else}
|
|
MessageBox MB_OK|MB_ICONEXCLAMATION "The installer log could not be sent. You can still share it manually; it was written to:$\r$\n$InstallLogPath"
|
|
${EndIf}
|
|
sentry_done:
|
|
!endif
|
|
SetErrorLevel 1
|
|
Abort
|
|
FunctionEnd
|
|
|
|
; usage: set $GuardDir and $WaitPid, then call this. Asks the one process the installer
|
|
; waits on to close and sets $R3 to what it could do: 1 = a window exists and was asked to
|
|
; close (or is waiting on a question of its own), 4 = it has no window a user could answer,
|
|
; anything else = the probe could not answer. Only 4 lets the wait act on its own; every
|
|
; other code keeps the user in the decision, because the installer holds a live handle and
|
|
; "the probe did not find it" is not the same answer as "it is gone".
|
|
;
|
|
; The guard's CloseHolders answers the same question for every holder and leaves a
|
|
; windowless instance to the user, which is right there: the prompt names it. The launcher
|
|
; is the one process nobody can be asked about - the in-app updater runs the installer
|
|
; silent and detached, and a client that has already hidden its window has answered its
|
|
; own prompts - so this answer has to say whether there was a window to ask at all.
|
|
Function CloseLauncher
|
|
Call ExtractHolderProbe
|
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_LAUNCHER_PID", "$WaitPid")'
|
|
; The probe waits 30 s itself when it posts a close. The timeout here only bounds a
|
|
; probe that never returns; the Restart Manager call the other modes make is not
|
|
; reached in this one.
|
|
nsExec::ExecToStack /TIMEOUT=40000 '"$PLUGINSDIR\installer-holder-probe.exe" --close-launcher'
|
|
Pop $R3
|
|
Pop $R4
|
|
Push "close-launcher: probe exit=$R3 [$R4]"
|
|
Call CockLogWrite
|
|
DetailPrint "Closing Cockatrice: $R4"
|
|
FunctionEnd
|
|
|
|
; Waits for the application that launched this installer to exit, using the /PID=
|
|
; it passed. Sets $LauncherExited to 1 when that process is provably gone.
|
|
;
|
|
; This is the authority for the one question the rest of the installer can only
|
|
; approximate: is the process that started us still holding its Qt DLLs mapped?
|
|
; A mapped module cannot be deleted, so an answer of "nothing is holding them"
|
|
; obtained from file handles is what allows the update to write a new payload on
|
|
; top of files it could not remove. Waiting on a real handle has no such gap,
|
|
; needs no process enumeration at all, and is unaffected by a process the holder
|
|
; probe cannot inspect.
|
|
;
|
|
; Sets $LauncherExited to 0 when no usable /PID= was given, which is the older
|
|
; client that does not pass one; the caller then has no authority available and
|
|
; falls back to the holder probe.
|
|
;
|
|
; A launcher that survives the first grace is asked to close by the probe, and the
|
|
; probe also answers whether there was a window a close could reach. A client hides
|
|
; its window the moment its own close is accepted, so a launcher without one has
|
|
; already answered its own prompts and is stuck in shutdown: no dialog can reach it
|
|
; there, and the wait ends it after one more grace instead of asking the user a
|
|
; question whose "keep waiting" answer can never change anything.
|
|
Function WaitForLauncherToExit
|
|
StrCpy $LauncherExited 0
|
|
StrCpy $WaitWindowless 0
|
|
!insertmacro CockLog "wait-launcher: pid='$WaitPid'"
|
|
${If} $WaitPid == ""
|
|
!insertmacro CockLog "wait-launcher: no pid given, falling back to name check"
|
|
Return
|
|
${EndIf}
|
|
; System.dll maps its output registers so that .rN writes $N and .RN writes
|
|
; $RN. The caller reads $R0/$R1, so the uppercase .RN form is required; the
|
|
; original .r0/.r1 wrote $0/$1 and left $R1 empty, which made the "did not
|
|
; exit" test below compare "" against "0" and abort on every /PID= upgrade.
|
|
; Reopened on every round below, so a launcher that exited in the meantime answers
|
|
; "already exited" instead of being waited on again.
|
|
;
|
|
; PROCESS_TERMINATE rides along with PROCESS_SYNCHRONIZE so that the "end it"
|
|
; answer below acts on this very handle: the pid looked up again after a dialog
|
|
; that can stay open as long as the user takes would name whatever process holds
|
|
; that pid by then, and a handle cannot be handed to another process.
|
|
;
|
|
; When those rights are refused - a launcher started by another account, a
|
|
; protected process - the wait still has to happen, so the ask is repeated with
|
|
; synchronize access alone and $WaitCanEnd records that ending is not on offer.
|
|
; Reporting "already exited" instead would be the worse answer: the payload would
|
|
; be written over a live process, which is what this wait exists to prevent.
|
|
;
|
|
; Ten seconds, not the full grace: a client that started an update exits in well
|
|
; under a second, so at ten seconds it is either flushing a card database or - far
|
|
; more often - sitting on its own "quit anyway?" confirmation with nobody looking at
|
|
; it. Silence for a minute is what makes that read as a hung installer. The full
|
|
; grace is spent after the first question, so a user who answers Cockatrice's
|
|
; confirmation is not asked again while they do it.
|
|
StrCpy $WaitSlice 10000
|
|
wait_launcher_open:
|
|
ClearErrors
|
|
StrCpy $WaitCanEnd 1
|
|
System::Call 'kernel32::OpenProcess(i 0x00100001, i 0, i $WaitPid) p.R0'
|
|
; OpenProcess returns a null handle for a process that is already gone. The
|
|
; System plugin does not raise the NSIS error flag from a called function's
|
|
; return value, so ${Errors} alone never saw that case and the null handle was
|
|
; passed straight to WaitForSingleObject. Fold a plugin failure into the same
|
|
; null-handle case instead of reading a handle that was never written.
|
|
${If} ${Errors}
|
|
StrCpy $R0 0
|
|
${EndIf}
|
|
${If} $R0 == 0
|
|
StrCpy $WaitCanEnd 0
|
|
ClearErrors
|
|
System::Call 'kernel32::OpenProcess(i 0x00100000, i 0, i $WaitPid) p.R0'
|
|
${If} ${Errors}
|
|
StrCpy $R0 0
|
|
${EndIf}
|
|
${EndIf}
|
|
${If} $R0 == 0
|
|
; The launcher is gone - OpenProcess cannot open a dead process - which is
|
|
; exactly the state the wait was for. A handle refused for want of the terminate
|
|
; right is not this case; the synchronize-only ask above covers it.
|
|
!insertmacro CockLog "wait-launcher: already exited"
|
|
DetailPrint "The application that started this installer has already exited."
|
|
StrCpy $LauncherExited 1
|
|
Return
|
|
${EndIf}
|
|
DetailPrint "Waiting for the application that started this installer to exit ..."
|
|
; WAIT_OBJECT_0 is 0; WAIT_TIMEOUT is 258 and a failed wait reports 0xFFFFFFFF.
|
|
System::Call 'kernel32::WaitForSingleObject(p R0, i $WaitSlice) i.R1'
|
|
${If} $R1 != 0
|
|
; $R0 is deliberately still open: it is the process that timed out, and the answer
|
|
; below has to act on that process and not on whatever $WaitPid names by then.
|
|
!insertmacro CockLog "wait-launcher: did not exit within $WaitSlice ms (wait=$R1, canEnd=$WaitCanEnd)"
|
|
DetailPrint "The application that started this installer did not exit in time."
|
|
; Ask the launcher itself before asking the user. A client that has already hidden
|
|
; its window has answered its own prompts and is stuck in shutdown: there is nothing
|
|
; left a dialog could reach, and the "ask it to close again" answer the dialog used
|
|
; to offer could only repeat itself until the user cancelled.
|
|
StrCpy $GuardDir $INSTDIR
|
|
Call CloseLauncher
|
|
; $R3 = what the probe could do (see CloseLauncher), $R4 = its line, already logged.
|
|
; A process that answered the close during the probe's own wait is gone by now.
|
|
System::Call 'kernel32::WaitForSingleObject(p R0, i 0) i.R1'
|
|
${If} $R1 == 0
|
|
System::Call 'kernel32::CloseHandle(p R0)'
|
|
StrCpy $R0 0
|
|
Goto wait_launcher_open
|
|
${EndIf}
|
|
${If} $R3 == "4"
|
|
; Nothing a user could answer: the window is gone, so the launcher is past its
|
|
; own prompts and stuck in shutdown. It cannot be asked, and it is not going to
|
|
; ask the user anything either, so the dialog below would be unanswerable.
|
|
${If} $WaitCanEnd == 1
|
|
${If} $WaitWindowless == 0
|
|
; Hiding the window is what the client does when its close is accepted,
|
|
; so what remains is the shutdown it already started: one full grace for
|
|
; that, without a question nobody could act on.
|
|
StrCpy $WaitWindowless 1
|
|
StrCpy $WaitSlice 60000
|
|
!insertmacro CockLog "wait-launcher: no window to ask, giving it one more minute"
|
|
System::Call 'kernel32::CloseHandle(p R0)'
|
|
StrCpy $R0 0
|
|
Goto wait_launcher_open
|
|
${EndIf}
|
|
${If} $WaitWindowless == 1
|
|
; It did not use that minute either. Ending it is what the dialog's Yes
|
|
; answer would have done, and there is no question left to put to the
|
|
; user: the instance lost its window before this round started.
|
|
StrCpy $WaitWindowless 2
|
|
!insertmacro CockLog "wait-launcher: still no window to ask; ending the launcher"
|
|
DetailPrint "The application that started this installer could not be asked to close; ending it."
|
|
System::Call 'kernel32::TerminateProcess(p R0, i 1) i.R2'
|
|
!insertmacro CockLog "wait-launcher: ending the launcher returned $R2"
|
|
System::Call 'kernel32::CloseHandle(p R0)'
|
|
StrCpy $R0 0
|
|
Goto wait_launcher_open
|
|
${EndIf}
|
|
${EndIf}
|
|
${EndIf}
|
|
; A window exists, or the launcher cannot be ended from here: the three answers
|
|
; below. "Carry on anyway" is not one of them - a live process has its image and its
|
|
; Qt DLLs mapped, so writing the payload over them is the broken install this wait
|
|
; exists to prevent. Yes ends the launcher, Cancel cancels, and No keeps waiting:
|
|
; the close and the bring-forward just happened above.
|
|
MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON2 \
|
|
"Cockatrice has not finished closing, so the installation is paused rather than replace files it is still using.$\r$\n$\r$\nIt may be waiting for you to answer its own question about an unsaved deck, an open game or a card database update.$\r$\n$\r$\nChoose Yes to end it now - an unsaved deck, a running game or an unwritten setting is lost.$\r$\n$\r$\nChoose No to keep waiting, or Cancel to cancel the installation." \
|
|
IDYES wait_launcher_end IDNO wait_launcher_retry
|
|
!insertmacro CockLog "wait-launcher: cancelled by user"
|
|
StrCpy $AbortReason \
|
|
"The application that started this update has not exited yet, so the installation was cancelled rather than replace files it is still using.$\r$\n$\r$\nPlease close Cockatrice and run the installer again."
|
|
Call AbortInstall
|
|
; Only reached through Yes or No: AbortInstall does not return.
|
|
wait_launcher_end:
|
|
; End it through the handle waited on above, and through nothing else: no pid is
|
|
; looked up again, so a process that inherited the number in the meantime - or a
|
|
; /PID= that was not the launcher to begin with - cannot be hit by this.
|
|
!insertmacro CockLog "wait-launcher: ending the launcher on request"
|
|
${If} $WaitCanEnd == 0
|
|
!insertmacro CockLog "wait-launcher: the launcher could not be opened to end it, asking it to close instead"
|
|
Goto wait_launcher_retry
|
|
${EndIf}
|
|
System::Call 'kernel32::TerminateProcess(p R0, i 1) i.R3'
|
|
System::Call 'kernel32::CloseHandle(p R0)'
|
|
StrCpy $R0 0
|
|
!insertmacro CockLog "wait-launcher: ending the launcher returned $R3"
|
|
Goto wait_launcher_open
|
|
wait_launcher_retry:
|
|
System::Call 'kernel32::CloseHandle(p R0)'
|
|
StrCpy $R0 0
|
|
; The question has been asked and answered once; from here the wait is the full
|
|
; grace again, so a slow flush or a confirmation being answered does not turn
|
|
; into a stream of prompts. The close itself and the bring-forward already
|
|
; happened just before the dialog, so this only waits. The windowless grace starts
|
|
; over as well: "keep waiting" must not be answered by ending the process a minute
|
|
; later without asking again.
|
|
StrCpy $WaitWindowless 0
|
|
StrCpy $WaitSlice 60000
|
|
Goto wait_launcher_open
|
|
${EndIf}
|
|
System::Call 'kernel32::CloseHandle(p R0)'
|
|
StrCpy $R0 0
|
|
!insertmacro CockLog "wait-launcher: launcher exited"
|
|
DetailPrint "The application that started this installer has exited."
|
|
StrCpy $LauncherExited 1
|
|
FunctionEnd
|
|
|
|
Function EnsureOldInstallNotRunning
|
|
; Guards the directory of a *previous* installation, as recorded in the
|
|
; registry. Needed by componentsPagePre, which runs the old uninstaller
|
|
; before the directory page, so $INSTDIR is still the default there and not
|
|
; the directory the old install lives in. Every uninstaller shipped before
|
|
; this change had no running instance guard of its own, so the old one would
|
|
; delete files while an instance of the application is still holding them.
|
|
ReadRegStr $R3 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "InstallLocation"
|
|
${If} $R3 == ""
|
|
; no usable record of where the old install is; $INSTDIR is the default
|
|
; install directory at this point, which is where it usually is anyway
|
|
DetailPrint "Could not read the previous installation directory from the registry; checking $INSTDIR instead."
|
|
StrCpy $R3 $INSTDIR
|
|
${EndIf}
|
|
StrCpy $GuardDir $R3
|
|
Call GuardAppsNotRunning
|
|
FunctionEnd
|
|
|
|
; Sets $R0 to 1 if $INSTDIR looks like an existing Cockatrice install. Used to
|
|
; guard the stale-runtime-DLL purge so a first-time install into a pre-existing
|
|
; directory with an unrelated "Plugins" folder is never touched, while an
|
|
; install that only ever held e.g. servatrice.exe is still purged.
|
|
Function IsExistingCockatriceInstall
|
|
StrCpy $R0 0
|
|
${If} ${FileExists} "$INSTDIR\cockatrice.exe"
|
|
StrCpy $R0 1
|
|
${EndIf}
|
|
${If} ${FileExists} "$INSTDIR\oracle.exe"
|
|
StrCpy $R0 1
|
|
${EndIf}
|
|
${If} ${FileExists} "$INSTDIR\servatrice.exe"
|
|
StrCpy $R0 1
|
|
${EndIf}
|
|
${If} ${FileExists} "$INSTDIR\uninstall.exe"
|
|
StrCpy $R0 1
|
|
${EndIf}
|
|
; A leftover runtime DLL counts as an install as well. A previously failed
|
|
; update can leave a directory in which the old uninstaller removed the
|
|
; executables and uninstall.exe but silently skipped the DLLs its running
|
|
; instance had locked - which is exactly the broken install the purge is
|
|
; meant to heal, and without these markers none of the files above would be
|
|
; there to prove that it is one. Every Qt module Cockatrice links is part of
|
|
; the payload, so match them all: probing only Qt6Core.dll would miss a
|
|
; directory where that one file happened to be deletable and the others
|
|
; were not. Qt6*.dll rather than *.dll on purpose: this function decides
|
|
; whether a directory is ours, and a folder that merely holds some DLLs of
|
|
; its own must not be purged. Qt6*.dll is enough even when only the protobuf
|
|
; runtime survived, because the whole Qt set is mapped by the same process
|
|
; that maps abseil_dll.dll and so is locked or free with it.
|
|
StrCpy $R3 "Qt6*.dll"
|
|
Call CheckStaleRuntimeMask
|
|
${If} ${FileExists} "$INSTDIR\qt.conf"
|
|
StrCpy $R0 1
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
; Sets $R0 to 1 if a runtime file of a previous version is still present after
|
|
; the purge. NSIS silently skips whatever it cannot delete, so one surviving file
|
|
; is enough to prove that something still holds a lock on the install directory.
|
|
;
|
|
; Any DLL at all counts, not a list of expected ones. The purge removes every
|
|
; top-level DLL, so after it has run the only DLL that can still be there is one
|
|
; it failed to delete, whatever its name. A mask per library family (Qt, then
|
|
; OpenSSL, then zlib) is what let abseil_dll.dll through: it is in neither the
|
|
; purge nor this check, so it survived a 3.0.2 to 3.1 update untouched, the new
|
|
; executables were written next to it, and the client then failed to start with
|
|
; "abseil_dll.dll is either not designed to run on Windows or it contains an
|
|
; error". Probing *.dll has no list to fall out of date when the next
|
|
; transitive dependency appears.
|
|
Function HasStaleRuntimeFiles
|
|
StrCpy $R0 0
|
|
StrCpy $R3 "*.dll"
|
|
Call CheckStaleRuntimeMask
|
|
; IfFileExists takes wildcards, so a Plugins directory the purge could not
|
|
; remove counts as a leftover too.
|
|
${If} ${FileExists} "$INSTDIR\Plugins\*.*"
|
|
!insertmacro CockLog "stale-runtime: Plugins directory survived the purge"
|
|
StrCpy $R0 1
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
; usage: set $R3 to a file mask, call this. Sets $R0 to 1 when $INSTDIR holds a
|
|
; matching file. Returns early once something was found, so $R0 stays 1.
|
|
Function CheckStaleRuntimeMask
|
|
${If} $R0 = 1
|
|
Return
|
|
${EndIf}
|
|
FindFirst $StaleFindHandle $StaleFindName "$INSTDIR\$R3"
|
|
${If} $StaleFindHandle != ""
|
|
!insertmacro CockLog "stale-runtime: '$StaleFindName' matched $R3 in $INSTDIR"
|
|
StrCpy $R0 1
|
|
FindClose $StaleFindHandle
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
; Clears the read-only attribute of everything the purge is about to delete.
|
|
;
|
|
; Delete and RMDir only set the error flag: a read-only file is left on disk
|
|
; untouched, and the retry loop then cannot make progress however long it waits,
|
|
; because the file is not held - it is protected. The abort diagnostics have a
|
|
; branch for exactly that ("read-only, which stops Delete"), so a survivor that
|
|
; is both free and read-only was a dead end until now. This runs on every
|
|
; attempt, so an attribute set again between attempts is cleared as well.
|
|
; SetFileAttributes has no wildcard support, hence the walks.
|
|
Function ClearReadOnlyPurgeTargets
|
|
FindFirst $StaleFindHandle $StaleFindName "$INSTDIR\*.dll"
|
|
${If} $StaleFindHandle != ""
|
|
crp_dll_loop:
|
|
SetFileAttributes "$INSTDIR\$StaleFindName" NORMAL
|
|
ClearErrors
|
|
FindNext $StaleFindHandle $StaleFindName
|
|
IfErrors 0 crp_dll_loop
|
|
FindClose $StaleFindHandle
|
|
${EndIf}
|
|
StrCpy $R6 "$INSTDIR\Plugins"
|
|
Call ClearReadOnlyTree
|
|
FunctionEnd
|
|
|
|
; usage: set $R6 to a directory. Clears the read-only attribute of every entry
|
|
; below it, so RMDir /r is not stopped by a single file. Saves and restores the
|
|
; registers it works in, because it recurses into itself.
|
|
Function ClearReadOnlyTree
|
|
Push $R6
|
|
Push $R7
|
|
Push $R8
|
|
FindFirst $R7 $R8 "$R6\*.*"
|
|
${If} $R7 != ""
|
|
crt_loop:
|
|
${If} $R8 != "."
|
|
${AndIf} $R8 != ".."
|
|
SetFileAttributes "$R6\$R8" NORMAL
|
|
; Only an entry with contents has anything below it that the purge
|
|
; would fail on; an empty directory's own attribute is already
|
|
; cleared above and there is nothing left to descend into.
|
|
${If} ${FileExists} "$R6\$R8\*.*"
|
|
Push $R6
|
|
StrCpy $R6 "$R6\$R8"
|
|
Call ClearReadOnlyTree
|
|
Pop $R6
|
|
${EndIf}
|
|
${EndIf}
|
|
ClearErrors
|
|
FindNext $R7 $R8
|
|
IfErrors 0 crt_loop
|
|
FindClose $R7
|
|
${EndIf}
|
|
Pop $R8
|
|
Pop $R7
|
|
Pop $R6
|
|
FunctionEnd
|
|
|
|
; Removes the Plugins directory and all top-level DLLs from $INSTDIR, then
|
|
; retries up to two more times (2 s apart) when files remain locked, so that
|
|
; transient holders such as antivirus scanners have a chance to release their
|
|
; handles before the installer gives up. Three attempts total. The read-only
|
|
; attribute is cleared before each attempt: a protected file is not a held one
|
|
; and would never clear on its own.
|
|
; Sets $R0 to 0 when the purge is clean, or to 1 when stale DLLs remain
|
|
; after all attempts.
|
|
Function RetryDllPurge
|
|
StrCpy $R5 0 ; counts retries after the initial attempt
|
|
rdp_loop:
|
|
Call ClearReadOnlyPurgeTargets
|
|
RMDir /r "$INSTDIR\Plugins"
|
|
Delete "$INSTDIR\*.dll"
|
|
Call HasStaleRuntimeFiles
|
|
; $R0 = 0: all clear, nothing left to do.
|
|
${If} $R0 = 0
|
|
Return
|
|
${EndIf}
|
|
IntOp $R5 $R5 + 1
|
|
; Allow two retries after the initial attempt ($R5 is the retry counter,
|
|
; so the loop runs at most 3 times: attempt + retry 1 + retry 2).
|
|
${If} $R5 > 2
|
|
!insertmacro CockLog "RetryDllPurge: stale files remain after 3 attempts, giving up"
|
|
Return ; $R0 is already 1
|
|
${EndIf}
|
|
!insertmacro CockLog "RetryDllPurge: retry $R5 of 2 after stale purge, sleeping 2 s"
|
|
Sleep 2000
|
|
Goto rdp_loop
|
|
FunctionEnd ; RetryDllPurge
|
|
; Turns a Win32 error code into the wording that goes into the log.
|
|
; usage: set $0 to the code, result in $R0.
|
|
;
|
|
; These are Windows' own names for the codes and deliberately nothing more. A
|
|
; code on its own cannot say whether an upgrade was blocked by a running instance
|
|
; or by a permission: deleting a file whose image is mapped by another process
|
|
; surfaces as ACCESS_DENIED, exactly like a file whose ACL denies delete does.
|
|
; LogProcessesUsingInstallDir is what separates the two, which is why it runs
|
|
; right after this and why the two are read together.
|
|
Function GetWin32ErrorText
|
|
${If} $0 = 2
|
|
StrCpy $R0 "already gone"
|
|
${ElseIf} $0 = 3
|
|
StrCpy $R0 "path not found"
|
|
${ElseIf} $0 = 5
|
|
StrCpy $R0 "access denied"
|
|
${ElseIf} $0 = 19
|
|
StrCpy $R0 "write-protected media"
|
|
${ElseIf} $0 = 32
|
|
StrCpy $R0 "sharing violation"
|
|
${ElseIf} $0 = 33
|
|
StrCpy $R0 "lock violation"
|
|
${ElseIf} $0 = 1224
|
|
StrCpy $R0 "file is mapped in memory"
|
|
${Else}
|
|
StrCpy $R0 "windows error"
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
; Records which runtime files the purge left behind, what Windows said about each
|
|
; one, and which processes are using the install directory.
|
|
;
|
|
; Diagnostic only, and only on the abort path: it deletes nothing, so it can never
|
|
; turn a failed upgrade into a half-removed old install, and it decides nothing -
|
|
; the abort that called it is already decided. It exists because NSIS Delete/RMDir
|
|
; only set the error flag: without this, a report of "files of a previous version
|
|
; survived the purge" said which files were present but never which one was held,
|
|
; and never whether the cause was a lock or a permission.
|
|
;
|
|
; Each survivor is opened for DELETE access and then asked the question DeleteFile
|
|
; asks: SetFileInformationByHandle(FileDispositionInfo) marks it for deletion on
|
|
; close, and the mark is cleared again right away, so nothing is deleted (and if
|
|
; that cancel ever failed, the file would go when the handle closes - a file the
|
|
; purge already tried to delete, and the line says so). That separates the three
|
|
; causes a failed Delete cannot tell apart: another handle that does not share
|
|
; delete refuses the mark, a read-only attribute refuses it, and a file nobody
|
|
; holds accepts it and would have been deleted. The mark is read through ?e, so a
|
|
; refusal reports Windows' own code; clearing LastError first keeps a zero code
|
|
; meaning "accepted". The wording below reports what Windows said and never claims
|
|
; a cause the code cannot prove. Which process is holding a survivor, if any, is
|
|
; what the holders line after the loop answers.
|
|
Function LogStaleRuntimeDiagnostics
|
|
FindFirst $StaleFindHandle $StaleFindName "$INSTDIR\*.dll"
|
|
${If} $StaleFindHandle != ""
|
|
stale_diag_loop:
|
|
; ClearErrors tells a plugin failure apart from an API failure: the System
|
|
; plugin sets the error flag only when it could not make the call at all,
|
|
; in which case it pushes its own message instead of the call's value and
|
|
; the pop below must not run.
|
|
ClearErrors
|
|
System::Call 'kernel32::SetLastError(i 0)'
|
|
System::Call 'kernel32::CreateFileW(w "$INSTDIR\$StaleFindName", i 0x00010000, i 0, p 0, i 3, i 0, p 0) p.R1 ?e'
|
|
${If} ${Errors}
|
|
StrCpy $R0 "the delete probe could not be run"
|
|
${Else}
|
|
Pop $R2
|
|
${If} $R2 = 0
|
|
; The open worked, which says nothing about Delete: a DLL a running
|
|
; process has mapped opens for DELETE and still refuses DeleteFile, and
|
|
; a read-only file does the same until its attribute is cleared. Ask
|
|
; Windows the question the purge actually asks - mark the file for
|
|
; deletion on close, then cancel that again. This is the same request
|
|
; DeleteFile makes on a temporary handle, so its answer says whether a
|
|
; delete would succeed now.
|
|
ClearErrors
|
|
System::Call '*(&i1 1) p.R4'
|
|
System::Call 'kernel32::SetFileInformationByHandle(p R1, i 4, p R4, i 1) i.R5 ?e'
|
|
${If} ${Errors}
|
|
StrCpy $R0 "a delete-access open now succeeds, but Windows could not be asked whether it would delete it"
|
|
${Else}
|
|
Pop $R6
|
|
${If} $R5 != 0
|
|
; Accepted: cancel the mark again, so this stays a question and
|
|
; does not turn into the deletion it is asking about. A cancel
|
|
; that fails leaves the file to go when the handle closes, which
|
|
; is what the purge wanted anyway; the line says so.
|
|
ClearErrors
|
|
System::Call '*(&i1 0) p.R4'
|
|
System::Call 'kernel32::SetFileInformationByHandle(p R1, i 4, p R4, i 1) i.R5 ?e'
|
|
${If} ${Errors}
|
|
StrCpy $R0 "a delete would succeed now, but the check could not undo the mark it set"
|
|
${Else}
|
|
Pop $R6
|
|
${If} $R5 != 0
|
|
StrCpy $R0 "a delete would succeed now - the earlier failure was transient"
|
|
${Else}
|
|
StrCpy $R0 "a delete would succeed now, and it is marked for deletion from here on"
|
|
${EndIf}
|
|
${EndIf}
|
|
${Else}
|
|
StrCpy $0 $R6
|
|
Call GetWin32ErrorText
|
|
StrCpy $R0 "a delete-access open now succeeds, but Windows refused to mark it for deletion: win32 error $R6: $R0"
|
|
${EndIf}
|
|
${EndIf}
|
|
System::Call 'kernel32::CloseHandle(p R1)'
|
|
; The read-only attribute is a cause on its own, and the purge clears it
|
|
; before every attempt, so a survivor that is read-only had it set again.
|
|
System::Call 'kernel32::GetFileAttributesW(w "$INSTDIR\$StaleFindName") i.R3'
|
|
${If} $R3 != -1
|
|
IntOp $R3 $R3 & 1
|
|
${If} $R3 = 1
|
|
StrCpy $R0 "$R0; it is read-only again"
|
|
${EndIf}
|
|
${EndIf}
|
|
${Else}
|
|
StrCpy $0 $R2
|
|
Call GetWin32ErrorText
|
|
StrCpy $R0 "win32 error $R2: $R0"
|
|
${EndIf}
|
|
${EndIf}
|
|
!insertmacro CockLog "stale-runtime: '$StaleFindName' is still present: $R0"
|
|
ClearErrors
|
|
FindNext $StaleFindHandle $StaleFindName
|
|
IfErrors 0 stale_diag_loop
|
|
FindClose $StaleFindHandle
|
|
${EndIf}
|
|
Call LogProcessesUsingInstallDir
|
|
FunctionEnd
|
|
|
|
; Names the applications and services that still have the payload open or mapped.
|
|
; Writes one line to the log and sets $HolderFound (1 when anything was named), so the
|
|
; abort message can repeat that line to the user.
|
|
;
|
|
; The lookup is tools/installer-holder-probe, the same executable the guard runs before
|
|
; anything is replaced; here it answers the other question, about files a purge could not
|
|
; remove. It asks the Restart Manager (RmRegisterResources/RmGetList), which names a
|
|
; process holding a plain handle on a file - an antivirus scanner - and names services as
|
|
; well as user programs, and it adds every process running from the directory, because a
|
|
; running instance holds its payload as mapped images rather than as open handles and the
|
|
; Restart Manager does not reliably report those. Its remaining blind spot is a handle
|
|
; taken in kernel mode, where there is no process to name; then the per-file codes logged
|
|
; above are the whole story, and the probe says so by naming nothing rather than by
|
|
; failing.
|
|
;
|
|
; $INSTDIR travels through the same COCKATRICE_GUARD_DIR environment variable the guard
|
|
; uses, so a path with spaces or apostrophes never has to be embedded in a command line.
|
|
Function LogProcessesUsingInstallDir
|
|
StrCpy $HolderExit 0
|
|
StrCpy $HolderOutput ""
|
|
StrCpy $HolderFound 0
|
|
; The directory that just failed the purge, not whatever a check before the directory
|
|
; page looked at: a portable install into a custom folder reaches here with the two
|
|
; being different.
|
|
StrCpy $GuardDir $INSTDIR
|
|
Call RunHolderProbe
|
|
StrCpy $HolderExit $GuardExit
|
|
StrCpy $HolderOutput $GuardOutput
|
|
; The exit code, not the text, decides what happened: it cannot be mangled by what
|
|
; nsExec leaves on the end of the captured output. 1 = the line below names what is
|
|
; holding the files, 0 = nothing holds them any more, anything else = the probe could
|
|
; not answer, and says why in its own output.
|
|
${If} $HolderExit == "1"
|
|
StrCpy $HolderFound 1
|
|
${ElseIf} $HolderExit == "0"
|
|
StrCpy $HolderOutput "nothing is holding them and no program is running from the directory"
|
|
${Else}
|
|
StrCpy $HolderOutput "the holder probe could not run: $HolderOutput"
|
|
${EndIf}
|
|
!insertmacro CockLog "holders: $HolderOutput"
|
|
FunctionEnd
|
|
|
|
; Called after a purge left files behind and LogStaleRuntimeDiagnostics named who is
|
|
; holding them. Instead of aborting straight away - which on /R strands the user, since
|
|
; the old version is already uninstalled - it closes once, purges again, and only then
|
|
; asks: end what is still holding the files, or try the removal again by hand. Sets
|
|
; $StaleRuntimeAfterPurge to the last purge's verdict, so the caller still aborts when the
|
|
; files survive.
|
|
;
|
|
; The shape is the guard's, deliberately: one automatic attempt at a graceful close, then a
|
|
; prompt whose Yes ends the payload programs and whose No retries. A client that was still
|
|
; shutting down releases the files in the automatic round and never sees a question, and an
|
|
; Oracle or Servatrice - which a close cannot reach - is what the prompt exists for, so the
|
|
; user is never left with a dialog whose answers cannot free anything.
|
|
;
|
|
; The automatic close is a request (WM_CLOSE) and the client keeps the last word: it may be
|
|
; waiting for an answer to its own "really quit?" confirmation (a running card database
|
|
; update, an open game, an unsaved deck), and that stays its decision. The instance is
|
|
; brought forward first, so that confirmation is on screen rather than behind this
|
|
; installer's prompt.
|
|
;
|
|
; Ending is the same last resort the guard offers, with the same cost spelled out, and only
|
|
; ever the programs the payload installs - see EndHolders.
|
|
;
|
|
; Restart Manager's RmShutdown was the alternative and was rejected: it shuts down
|
|
; services as well as applications, and the only way to bring them back is RmRestart,
|
|
; which would restart this very application into files that are about to be replaced.
|
|
;
|
|
; Always offered, in every mode: a /S install without /R is the only case without someone
|
|
; at the keyboard, and this installer does not promise unattended runs - if an error needs
|
|
; a decision, it asks for one. No offer when the probe named nobody: there is nothing to
|
|
; end, and the abort's per-file diagnostics already record what Windows said.
|
|
;
|
|
; Every round offers all three answers: Yes ends the programs the payload installs - and
|
|
; says so, because anything else the probe named is left to the user - and purges again, No
|
|
; purges again without ending anything (for when the user closed things by hand), and
|
|
; Cancel gives up. Each round re-reads the holders, so the prompt names what survived the
|
|
; last attempt rather than repeating the first listing.
|
|
Function RecoverFromHolders
|
|
${If} $HolderFound != 1
|
|
!insertmacro CockLog "recover-holders: no holder was named, nothing to offer"
|
|
Return
|
|
${EndIf}
|
|
StrCpy $GuardEndFailed 0
|
|
; The automatic attempt, the same one the guard makes before it asks anything.
|
|
!insertmacro CockLog "recover-holders: closing cockatrice, then trying the removal again"
|
|
StrCpy $GuardDir $INSTDIR
|
|
Call CloseHolders
|
|
Goto rfh_retry
|
|
rfh_prompt:
|
|
; The same question the guard asks, so the two cannot drift apart in what they promise.
|
|
!insertmacro CockEndPrompt "Files of the previous version of Cockatrice are still in use by:" $HolderOutput "installation"
|
|
MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON2 "$GuardPrompt" IDYES rfh_end IDNO rfh_retry
|
|
!insertmacro CockLog "recover-holders: cancelled by user"
|
|
Return
|
|
; Only reached through Yes: Return does not come back here.
|
|
rfh_end:
|
|
!insertmacro CockLog "recover-holders: ending the payload programs on request, then trying the removal again"
|
|
Call EndHolders
|
|
; The next prompt says the end did not free the files, instead of naming the same
|
|
; survivors word for word.
|
|
StrCpy $GuardEndFailed 1
|
|
rfh_retry:
|
|
Call RetryDllPurge
|
|
StrCpy $StaleRuntimeAfterPurge $R0
|
|
!insertmacro CockLogListing "after-retry-purge" "*.dll"
|
|
!insertmacro CockLogListing "after-retry-plugins" "Plugins\*.*"
|
|
!insertmacro CockLog "recover-holders: stale-runtime after retry=$StaleRuntimeAfterPurge"
|
|
${If} $StaleRuntimeAfterPurge = 0
|
|
DetailPrint "Files of the previous version were removed; continuing the installation."
|
|
Return
|
|
${EndIf}
|
|
; Still stuck: refresh the names, so the next prompt and the log describe what
|
|
; survived the last attempt and not what was there before it.
|
|
Call LogStaleRuntimeDiagnostics
|
|
${If} $HolderFound != 1
|
|
Return
|
|
${EndIf}
|
|
Goto rfh_prompt
|
|
FunctionEnd
|
|
|
|
; Uninstaller copy of the guard (the uninstaller gets its own function set compiled in, it
|
|
; cannot call the installer functions). ExtractHolderProbe and RunHolderProbe come from
|
|
; CockHolderProbeFunctions above; only the policy differs.
|
|
Function un.EnsureAppsNotRunning
|
|
; guards the install directory; the uninstaller only ever has to check this one
|
|
StrCpy $GuardDir $INSTDIR
|
|
Call un.GuardAppsNotRunning
|
|
FunctionEnd
|
|
|
|
Function un.GuardAppsNotRunning
|
|
StrCpy $GuardCloseTried 0
|
|
StrCpy $GuardEndFailed 0
|
|
un_guard_check:
|
|
!insertmacro CockLogUn "uninst-guard: checking $GuardDir"
|
|
Call un.RunHolderProbe
|
|
${If} $GuardExit == "0"
|
|
!insertmacro CockLogUn "uninst-guard: nothing is holding the payload"
|
|
Return
|
|
${EndIf}
|
|
${If} $GuardExit == "1"
|
|
!insertmacro CockLogUn "uninst-guard: still in use by $GuardOutput"
|
|
; Same convenience as in the installer: ask Cockatrice to close once, then report
|
|
; what the close could not reach.
|
|
${If} $GuardCloseTried = 0
|
|
StrCpy $GuardCloseTried 1
|
|
!insertmacro CockLogUn "uninst-guard: asking cockatrice to close, then checking again"
|
|
Call un.CloseHolders
|
|
Goto un_guard_check
|
|
${EndIf}
|
|
DetailPrint "The install directory is in use by: $GuardOutput"
|
|
; Same approach as in the installer's guard: the automatic close already ran once, and
|
|
; asking again is unlikely to help an instance that is sitting on its own confirmation
|
|
; or has no window a close could reach. Ending is what is left that can free the files,
|
|
; so the prompt offers it here, with what it costs in the text, while keeping the
|
|
; answers that end nothing - the instance's own question, and closing anything by hand
|
|
; - reachable from the same dialog. CockEndPrompt words it, so this copy and the
|
|
; installer's promise the same thing.
|
|
!insertmacro CockEndPrompt "Cockatrice, Oracle or Servatrice is still using files of the installation:" $GuardOutput "uninstallation"
|
|
MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON2 "$GuardPrompt" IDYES un_guard_end IDNO un_guard_check
|
|
!insertmacro CockLogUn "uninst-guard: cancelled by user"
|
|
SetErrorLevel 1
|
|
Abort
|
|
; Only reached through Yes: Abort does not return.
|
|
un_guard_end:
|
|
!insertmacro CockLogUn "uninst-guard: ending the payload programs on request, then checking again"
|
|
Call un.EndHolders
|
|
; Everything the payload installs is ended, but a program that could not be opened
|
|
; (another account, a protected process) survives, and a program that is not one of
|
|
; ours is never touched at all, so what is left may still have to be closed by hand.
|
|
; Go back to the same prompt, which says the end did not free the files.
|
|
StrCpy $GuardEndFailed 1
|
|
Goto un_guard_check
|
|
${EndIf}
|
|
; Same reasoning as in the installer: without an answer from the probe we cannot tell
|
|
; whether a running instance still holds file locks, and refusing outright would leave
|
|
; the user with an uninstaller that never completes. Same three answers too, so an
|
|
; uninstall that hits a Restart Manager that was busy can simply be retried.
|
|
!insertmacro CockLogUn "uninst-guard: the holder probe could not answer ($GuardExit): $GuardOutput"
|
|
DetailPrint "Could not check whether Cockatrice is still running: $GuardOutput"
|
|
MessageBox MB_ABORTRETRYIGNORE|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
|
|
"This uninstaller could not check whether Cockatrice, Oracle or Servatrice is still running.$\r$\n$\r$\nThe check reported: $GuardOutput$\r$\n$\r$\nClose them, then choose Retry to check again. Choose Ignore to uninstall anyway, or Abort to cancel the uninstallation." \
|
|
IDRETRY un_guard_check IDABORT un_guard_unknown_abort
|
|
!insertmacro CockLogUn "uninst-guard: continuing without an answer from the probe"
|
|
Return
|
|
un_guard_unknown_abort:
|
|
!insertmacro CockLogUn "uninst-guard: cancelled after the probe could not answer"
|
|
SetErrorLevel 1
|
|
Abort
|
|
FunctionEnd
|
|
|
|
Function PortableModePageCreate
|
|
|
|
${If} $ReinstallMode = 1
|
|
Abort
|
|
${EndIf}
|
|
|
|
Call SetModeDestinationFromInstdir ; If the user clicks BACK on the directory page we will remember their mode specific directory
|
|
!insertmacro MUI_HEADER_TEXT "Install Mode" "Choose how you want to install Cockatrice."
|
|
nsDialogs::Create 1018
|
|
Pop $0
|
|
${NSD_CreateLabel} 0 10u 100% 24u "Select install mode:"
|
|
Pop $0
|
|
${NSD_CreateRadioButton} 30u 50u -30u 8u "Normal installation"
|
|
Pop $1
|
|
${NSD_CreateRadioButton} 30u 70u -30u 8u "Portable mode (all files in a single folder)"
|
|
Pop $2
|
|
${If} $PortableMode = 0
|
|
SendMessage $1 ${BM_SETCHECK} ${BST_CHECKED} 0
|
|
${Else}
|
|
SendMessage $2 ${BM_SETCHECK} ${BST_CHECKED} 0
|
|
${EndIf}
|
|
nsDialogs::Show
|
|
FunctionEnd
|
|
|
|
Function PortableModePageLeave
|
|
${NSD_GetState} $1 $0
|
|
${If} $0 <> ${BST_UNCHECKED}
|
|
StrCpy $PortableMode 0
|
|
StrCpy $InstDir $NormalDestDir
|
|
Call RequireAdmin
|
|
${Else}
|
|
StrCpy $PortableMode 1
|
|
StrCpy $InstDir $PortableDestDir
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
Function componentsPagePre
|
|
|
|
${If} $ReinstallMode = 1
|
|
Return
|
|
${EndIf}
|
|
|
|
${If} $PortableMode = 0
|
|
SetShellVarContext all
|
|
|
|
# uninstall 32bit version
|
|
SetRegView 32
|
|
|
|
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "UninstallString"
|
|
StrCmp $R0 "" done32
|
|
|
|
${If} $ReinstallMode = 0
|
|
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst32
|
|
; Abort would only skip this page and install over the old version anyway;
|
|
; the message said the old one must go first, so Cancel means cancel.
|
|
!insertmacro CockLog "componentsPagePre: cancelled at the previous-version prompt"
|
|
Quit
|
|
${Else}
|
|
Goto uninst32
|
|
${EndIf}
|
|
|
|
uninst32:
|
|
; the old uninstaller is run here, before the directory page, and every
|
|
; uninstaller predating this change has no guard of its own: make sure no
|
|
; instance of the application is still running out of the *old* install
|
|
; directory before letting it delete anything. The uninstall string is
|
|
; stashed away first, the check works on $R0.
|
|
; $INSTDIR is still the default here (directory page comes later), so the
|
|
; backup/restore dir comes from the registry InstallLocation via $GuardDir,
|
|
; not from $INSTDIR - otherwise a custom-location install is unprotected.
|
|
StrCpy $OldUninstallString $R0
|
|
Call EnsureOldInstallNotRunning
|
|
StrCpy $OldInstallDir $GuardDir
|
|
; Run it silently. The question above is the one the user answered; the
|
|
; uninstaller's own confirmation page would be a second one, and that one opens
|
|
; behind this window, where nobody can answer it. The installer then looks hung,
|
|
; the user clicks Next on it, the new version is installed, and the uninstaller
|
|
; removes everything once this window closes. /S leaves no window to be behind,
|
|
; so ExecWait is waiting for something that cannot outlive it.
|
|
ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
|
|
${If} $R1 == ""
|
|
StrCpy $OldUninstallString "$OldUninstallString /S"
|
|
${Else}
|
|
StrCpy $OldUninstallString $R1
|
|
${EndIf}
|
|
!insertmacro CockLog "componentsPagePre: old 32-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)"
|
|
ClearErrors
|
|
Call BackupUserDataBeforeUninstall
|
|
Call RunOldUninstaller
|
|
!insertmacro CockLog "componentsPagePre: old 32-bit uninstall exit=$OldUninstallExit"
|
|
Call WaitForOldPayloadToDisappear
|
|
Call RestoreUserDataAfterUninstall
|
|
|
|
done32:
|
|
|
|
# uninstall 64bit version
|
|
${If} ${NSIS_IS_64_BIT} == 1
|
|
SetRegView 64
|
|
|
|
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "UninstallString"
|
|
StrCmp $R0 "" done64
|
|
|
|
${If} $ReinstallMode = 0
|
|
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst64
|
|
!insertmacro CockLog "componentsPagePre: cancelled at the previous-version prompt"
|
|
Quit
|
|
${Else}
|
|
Goto uninst64
|
|
${EndIf}
|
|
|
|
uninst64:
|
|
StrCpy $OldUninstallString $R0
|
|
Call EnsureOldInstallNotRunning
|
|
StrCpy $OldInstallDir $GuardDir
|
|
; Silent, for the reason given in the 32-bit branch above.
|
|
ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
|
|
${If} $R1 == ""
|
|
StrCpy $OldUninstallString "$OldUninstallString /S"
|
|
${Else}
|
|
StrCpy $OldUninstallString $R1
|
|
${EndIf}
|
|
!insertmacro CockLog "componentsPagePre: old 64-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)"
|
|
ClearErrors
|
|
Call BackupUserDataBeforeUninstall
|
|
Call RunOldUninstaller
|
|
!insertmacro CockLog "componentsPagePre: old 64-bit uninstall exit=$OldUninstallExit"
|
|
Call WaitForOldPayloadToDisappear
|
|
Call RestoreUserDataAfterUninstall
|
|
|
|
done64:
|
|
${EndIf}
|
|
|
|
${Else}
|
|
Abort
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
Section "Application" SecApplication
|
|
|
|
SetShellVarContext all
|
|
SetOutPath "$INSTDIR"
|
|
|
|
!insertmacro CockLog "section: InstDir=$INSTDIR PortableMode=$PortableMode ReinstallMode=$ReinstallMode"
|
|
|
|
; No application instance may be running while any file below is deleted or
|
|
; replaced: both upgrade blocks start by removing the old executables and
|
|
; runtime DLLs, and NSIS silently skips whatever is locked, which is how an
|
|
; install ends up with a mix of old and new runtimes next to the new executable.
|
|
; The next start then fails with "The procedure entry point X could not be
|
|
; located in the dynamic link library ...Qt6Network.dll" for the Qt ones, or
|
|
; "abseil_dll.dll is either not designed to run on Windows or it contains an
|
|
; error" for the protobuf ones. So the check has to come before the first
|
|
; Delete/RMDir and not after it: an interactive install into an existing portable
|
|
; directory with cockatrice.exe still running would otherwise delete the old
|
|
; files under its feet.
|
|
;
|
|
; It is skipped on the /R path: .onInit already ran it before
|
|
; AutoUninstallIfNeeded removed the previous installation, so by the time this
|
|
; section starts the old files are gone and a second Abort here would leave the
|
|
; user with the old version uninstalled and the new one not installed at all,
|
|
; which is worse than the locked files being prevented. The leftover runtime
|
|
; files are reported at the end of the purge instead.
|
|
${If} $ReinstallMode = 0
|
|
Call EnsureAppsNotRunning
|
|
${EndIf}
|
|
|
|
; Measured before either purge block, because it has to describe the directory as
|
|
; it arrived here and not what a purge left of it: IsExistingCockatriceInstall
|
|
; looks for cockatrice.exe, oracle.exe, servatrice.exe, uninstall.exe, Qt6*.dll
|
|
; and qt.conf, and the portable purge below deletes exactly those. Taken after
|
|
; that purge it answered "not an install" as soon as the only survivor was a file
|
|
; it does not look for - a locked abseil_dll.dll or libssl-3-x64.dll, say - and
|
|
; the abort further down was then skipped for the very directory the purge had
|
|
; just failed in.
|
|
; Remembered because the stale-runtime check below must only ever fire for a
|
|
; directory this installer recognised as a Cockatrice install. Scanning for
|
|
; leftovers in an unrelated directory (a first-time install into a folder that
|
|
; happens to hold some Qt5 DLLs, say) would otherwise turn those files into an
|
|
; abort reason even though the payload is about to overwrite nothing of theirs.
|
|
Call IsExistingCockatriceInstall
|
|
!insertmacro CockLog "section: IsExistingCockatriceInstall=$R0"
|
|
StrCpy $IsUpgrade $R0
|
|
|
|
${If} $PortableMode = 1
|
|
${AndIf} ${FileExists} "$INSTDIR\portable.dat"
|
|
; upgrade portable mode
|
|
!insertmacro CockLog "section: portable upgrade purge"
|
|
!insertmacro CockLogListing "before-portable-purge" "*.dll"
|
|
; plugins and translations are payload and are rewritten by the payload
|
|
; below, so removing them here is safe. sounds and themes are payload
|
|
; too - the default packs are rewritten below as well - but a portable
|
|
; user's custom sounds and themes live in exactly those folders, so a
|
|
; recursive purge here would delete user content along with the defaults.
|
|
; Stale default files from an older release are overwritten or replaced
|
|
; by the payload, never by a purge.
|
|
RMDir /r "$INSTDIR\plugins"
|
|
RMDir /r "$INSTDIR\translations"
|
|
Delete "$INSTDIR\uninstall.exe"
|
|
Delete "$INSTDIR\cockatrice.exe"
|
|
Delete "$INSTDIR\oracle.exe"
|
|
Delete "$INSTDIR\servatrice.exe"
|
|
; Every top-level DLL, not a list of expected ones. The payload is written
|
|
; after this block, so a DLL the new build still needs is put straight back;
|
|
; what this has to guarantee is that nothing from the previous build is left
|
|
; to be loaded next to the new executables. Naming the families by hand is
|
|
; what let abseil_dll.dll through; see HasStaleRuntimeFiles.
|
|
Call RetryDllPurge
|
|
; The purge's own verdict: HasStaleRuntimeFiles measures *.dll + Plugins\*.*,
|
|
; the set that catches any leftover runtime DLL, Qt or otherwise. Held in a
|
|
; variable because $R0 is reused by the next function call in this section.
|
|
StrCpy $StaleRuntimeAfterPurge $R0
|
|
StrCpy $DllPurgeRan 1
|
|
Delete "$INSTDIR\qt.conf"
|
|
Delete "$INSTDIR\qdebug.txt"
|
|
Delete "$INSTDIR\servatrice.sql"
|
|
Delete "$INSTDIR\servatrice.ini.example"
|
|
RMDir "$INSTDIR"
|
|
!insertmacro CockLogListing "after-portable-purge" "*.dll"
|
|
${EndIf}
|
|
|
|
; Remove any runtime DLLs left over from older versions, so a mismatched
|
|
; Qt/OpenSSL/protobuf set cannot survive next to the new executables.
|
|
;
|
|
; A purge that fails is reported with evidence, not only as a survivor list: the
|
|
; abort below logs which files survived, what Windows said about each of them and
|
|
; which processes still have the install directory mapped (see
|
|
; LogStaleRuntimeDiagnostics).
|
|
;
|
|
; Re-running the running-instance check here was considered and dropped: it would only
|
|
; narrow the window the uninstaller ExecWait and the backup/restore copy open between
|
|
; .onInit and this point, and on /R it would now abort after the old version has already
|
|
; been removed - leaving no install at all. The surviving files are the authority on
|
|
; whether anything still holds the directory, and they are measured anyway.
|
|
;
|
|
; The holder list comes from Restart Manager (RmRegisterResources/RmGetList), the API
|
|
; that names every process and service with one of these files open or mapped. It is
|
|
; called from tools/installer-holder-probe rather than from this script, because the
|
|
; RM_PROCESS_INFO array is a table of hand-computed offsets when it is marshalled
|
|
; through the System plugin, and that table is the one thing here that cannot be
|
|
; checked without a Windows toolchain. Compiled by the same build that compiles this
|
|
; installer, the layout belongs to the Windows SDK, and .ci/compile.sh runs the probe
|
|
; on the Windows runners instead of trusting that it compiles.
|
|
${If} $PortableMode = 0
|
|
${AndIf} $IsUpgrade = 1
|
|
!insertmacro CockLog "section: non-portable upgrade purge"
|
|
!insertmacro CockLogListing "before-purge" "*.dll"
|
|
Call RetryDllPurge
|
|
StrCpy $StaleRuntimeAfterPurge $R0
|
|
StrCpy $DllPurgeRan 1
|
|
!insertmacro CockLogListing "after-purge" "*.dll"
|
|
!insertmacro CockLogListing "after-plugins" "Plugins\*.*"
|
|
${EndIf}
|
|
|
|
; Whatever was locked during the purge (by an instance that started while the
|
|
; installer was running, or because the running instance check could not be
|
|
; performed at all) is still on disk, and the new executables are about to be
|
|
; written next to it. That mix is exactly what makes the next start fail with
|
|
; "The procedure entry point X could not be located in the dynamic link library
|
|
; ...Qt6QuickWidgets.dll", "The code execution cannot proceed because
|
|
; Qt6Multimedia.dll was not found", or "abseil_dll.dll is either not designed to
|
|
; run on Windows or it contains an error", so stop here instead of compounding
|
|
; it. Nothing of the new payload has been written at this point, so the next run,
|
|
; with the applications closed, gets a clean install.
|
|
; $StaleRuntimeAfterPurge is RetryDllPurge's verdict, measured with
|
|
; HasStaleRuntimeFiles (*.dll + Plugins\*.*) - the set that catches any leftover
|
|
; runtime DLL, Qt or otherwise. It exists only when a purge ran, and the two
|
|
; purge blocks above cover every upgrade except one: a portable install into a
|
|
; directory without portable.dat is neither of them, so that directory is
|
|
; measured here instead. A test of $IsUpgrade alone could not stand in for it -
|
|
; that only says the directory was ours before the purge, not what the purge
|
|
; left behind.
|
|
; A directory this installer did not recognise as an install never aborts: the
|
|
; unrelated DLLs already in it (a first-time install into a folder that happens
|
|
; to hold some Qt5 DLLs, say) are not leftovers, and the payload overwrites
|
|
; nothing of theirs.
|
|
${If} $IsUpgrade = 1
|
|
${If} $DllPurgeRan = 0
|
|
!insertmacro CockLog "section: no purge applies, measuring the directory instead"
|
|
Call HasStaleRuntimeFiles
|
|
StrCpy $StaleRuntimeAfterPurge $R0
|
|
${EndIf}
|
|
!insertmacro CockLog "section: stale-runtime after purge=$StaleRuntimeAfterPurge"
|
|
${Else}
|
|
StrCpy $StaleRuntimeAfterPurge 0
|
|
${EndIf}
|
|
${If} $StaleRuntimeAfterPurge = 1
|
|
!insertmacro CockLog "section: files of a previous version survived the purge"
|
|
; Who is holding the payload is recorded first, so the log that reaches us
|
|
; answers the question the message can only guess at, and so the prompt can
|
|
; name them and offer to close the ones this installer owns.
|
|
Call LogStaleRuntimeDiagnostics
|
|
${If} $HolderFound = 1
|
|
; On /R the previous version is already uninstalled at this point, so an abort
|
|
; leaves the user with no working Cockatrice. It closes once, then offers to end
|
|
; what is left or to try the removal again; this updates $StaleRuntimeAfterPurge
|
|
; with the last retry's verdict.
|
|
Call RecoverFromHolders
|
|
${Else}
|
|
; Nothing is holding them. The three attempts in RetryDllPurge were two seconds
|
|
; apart and all failed with no process to blame, which is the transient case a
|
|
; virus scanner or a background indexer creates - or a permission problem that
|
|
; will not clear. Give the delete one last chance before giving up: it costs a
|
|
; second on a path that is about to abort anyway, and it keeps a lock that was
|
|
; released in the meantime from turning into a failed upgrade.
|
|
!insertmacro CockLog "section: no holder named, one final purge attempt"
|
|
Sleep 1000
|
|
Call RetryDllPurge
|
|
StrCpy $StaleRuntimeAfterPurge $R0
|
|
!insertmacro CockLog "section: final-purge stale-runtime=$StaleRuntimeAfterPurge"
|
|
${EndIf}
|
|
${EndIf}
|
|
${If} $StaleRuntimeAfterPurge = 1
|
|
!insertmacro CockLog "section: aborting, files of a previous version survived the purge"
|
|
DetailPrint "Files of a previous version could not be removed; aborting the installation."
|
|
; Say what is known and nothing more: the guard ran before anything was replaced,
|
|
; so "most likely still running" is no longer a fair guess and would send the user
|
|
; to close programs that are already closed.
|
|
StrCpy $AbortReason \
|
|
"The installation could not remove all files of a previous version of Cockatrice, and was cancelled rather than leave a mismatched installation behind."
|
|
${If} $HolderFound = 1
|
|
StrCpy $AbortReason "$AbortReason$\r$\n$\r$\nStill using the install directory:$\r$\n$HolderOutput$\r$\n$\r$\nClose them and run the installer again."
|
|
${ElseIf} $HolderExit == "0"
|
|
StrCpy $AbortReason "$AbortReason$\r$\n$\r$\nNo running program was found using those files. A file permission or a virus scanner may be preventing their removal; the installer log records what Windows reported for each file."
|
|
${Else}
|
|
; The check itself failed, so "no running program was found" would be a claim this
|
|
; installer cannot make. Name the failure instead, and leave the per-file codes to
|
|
; the log, where they already are.
|
|
StrCpy $AbortReason "$AbortReason$\r$\n$\r$\nThe check for a running program could not be completed: $HolderOutput$\r$\n$\r$\nClose Cockatrice, Oracle and Servatrice and run the installer again; the installer log records what Windows reported for each file."
|
|
${EndIf}
|
|
; "Run the installer again" is not enough on an in-app update: the old version
|
|
; was uninstalled before the purge, so point at the installer that was downloaded.
|
|
${If} $ReinstallMode = 1
|
|
${AndIf} $PortableMode = 0
|
|
StrCpy $AbortReason "$AbortReason$\r$\n$\r$\nThe previous version has already been removed. To finish the update, run this installer again:$\r$\n$EXEPATH"
|
|
${EndIf}
|
|
Call AbortInstall
|
|
${EndIf}
|
|
|
|
@CPACK_NSIS_EXTRA_PREINSTALL_COMMANDS@
|
|
!insertmacro CockLog "section: writing payload to $INSTDIR"
|
|
@CPACK_NSIS_FULL_INSTALL@
|
|
!insertmacro CockLog "section: payload written"
|
|
; The pre-uninstall backup/restore above runs before the directory page, so a
|
|
; user picking a different target folder would otherwise leave the restored data
|
|
; orphaned in the old directory. Move it across when they differ (no-op on the
|
|
; /R path where they are equal).
|
|
Call RelocateRestoredDataToInstDir
|
|
!insertmacro CockLogListing "final" "Qt6*.dll"
|
|
|
|
${If} $PortableMode = 0
|
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
|
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
|
IntFmt $0 "0x%08X" $0
|
|
|
|
; No Windows Error Reporting LocalDumps registration: crash capture belongs to
|
|
; the in-app reporter (Sentry/breakpad, MiniDumpNormal-class data only). Opting
|
|
; machines into OS-level full dumps (DumpType 2) would leave unredacted memory
|
|
; dumps on user machines that nothing consumes.
|
|
; https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
|
|
|
|
; Older installers did register cockatrice.exe above (and the removed
|
|
; MiniDumpWithFullMemory writer filled that folder), and that key survives this
|
|
; install, so WER would keep writing full dumps. Remove it, and clear the
|
|
; legacy dumps nothing consumes anymore. $LOCALAPPDATA resolves per shell context:
|
|
; under "all" it points at ProgramData, so switch to "current" for the per-user
|
|
; dumps folder and back afterwards; the registry delete works in either context.
|
|
DeleteRegKey HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe"
|
|
SetShellVarContext current
|
|
RMDir /r "$LOCALAPPDATA\CrashDumps\Cockatrice"
|
|
SetShellVarContext all
|
|
|
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayIcon" "$INSTDIR\cockatrice.exe"
|
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayName" "Cockatrice"
|
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayVersion" "@CPACK_PACKAGE_VERSION_MAJOR@.@CPACK_PACKAGE_VERSION_MINOR@.@CPACK_PACKAGE_VERSION_PATCH@"
|
|
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "EstimatedSize" "$0"
|
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "InstallLocation" "$INSTDIR"
|
|
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "NoModify" "1"
|
|
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "NoRepair" "1"
|
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "Publisher" "Cockatrice team"
|
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
|
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "VersionMajor" "@CPACK_PACKAGE_VERSION_MAJOR@"
|
|
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "VersionMinor" "@CPACK_PACKAGE_VERSION_MINOR@"
|
|
|
|
; --- Register .cod file type ---
|
|
WriteRegStr HKCR ".cod" "" "Cockatrice"
|
|
WriteRegStr HKCR "Cockatrice" "" "Cockatrice Deck File"
|
|
WriteRegStr HKCR "Cockatrice\shell\open\command" "" '"$INSTDIR\cockatrice.exe" "%1"'
|
|
|
|
; --- Register custom URI protocol ---
|
|
WriteRegStr HKCR "cockatrice" "" "URL: Cockatrice Protocol"
|
|
WriteRegStr HKCR "cockatrice" "URL Protocol" ""
|
|
WriteRegStr HKCR "cockatrice\shell\open\command" "" '"$INSTDIR\cockatrice.exe" "%1"'
|
|
|
|
IfFileExists "$INSTDIR\vc_redist.x86.exe" VcRedist86Exists PastVcRedist86Check
|
|
VcRedist86Exists:
|
|
ExecWait '"$INSTDIR\vc_redist.x86.exe" /passive /norestart'
|
|
DetailPrint "Wait to ensure unlock of vc_redist file after installation..."
|
|
Sleep 3000
|
|
Delete "$INSTDIR\vc_redist.x86.exe"
|
|
PastVcRedist86Check:
|
|
|
|
IfFileExists "$INSTDIR\vc_redist.x64.exe" VcRedist64Exists PastVcRedist64Check
|
|
VcRedist64Exists:
|
|
ExecWait '"$INSTDIR\vc_redist.x64.exe" /passive /norestart'
|
|
DetailPrint "Sleep to ensure unlock of vc_redist file after installation..."
|
|
Sleep 3000
|
|
Delete "$INSTDIR\vc_redist.x64.exe"
|
|
PastVcRedist64Check:
|
|
|
|
${Else}
|
|
; Create the file the application uses to detect portable mode
|
|
FileOpen $0 "$INSTDIR\portable.dat" w
|
|
FileWrite $0 "PORTABLE"
|
|
FileClose $0
|
|
${EndIf}
|
|
|
|
${If} $ReinstallMode = 1
|
|
IfFileExists "$INSTDIR\cockatrice.exe" 0 +2
|
|
Exec '"$INSTDIR\cockatrice.exe"'
|
|
${EndIf}
|
|
|
|
!insertmacro CockLog "section: application install complete"
|
|
SectionEnd
|
|
|
|
Section "Start menu item" SecStartMenu
|
|
${If} $PortableMode = 0
|
|
SetShellVarContext all
|
|
createDirectory "$SMPROGRAMS\Cockatrice"
|
|
createShortCut "$SMPROGRAMS\Cockatrice\Cockatrice.lnk" "$INSTDIR\cockatrice.exe"
|
|
createShortCut "$SMPROGRAMS\Cockatrice\Oracle.lnk" "$INSTDIR\oracle.exe"
|
|
createShortCut "$SMPROGRAMS\Cockatrice\Servatrice.lnk" "$INSTDIR\servatrice.exe"
|
|
${EndIf}
|
|
SectionEnd
|
|
|
|
Section "un.Application" UnSecApplication
|
|
SetShellVarContext all
|
|
StrCpy $R1 0
|
|
|
|
; Record whether this was a portable install before portable.dat is
|
|
; removed below.
|
|
IfFileExists "$INSTDIR\portable.dat" 0 +2
|
|
StrCpy $R1 1
|
|
|
|
; Remove only what the installer wrote. The two lists below are generated
|
|
; by CPack from the staged install tree: one Delete per installed file and
|
|
; one non-recursive RMDir per installed directory, children first.
|
|
; Anything the user put in $INSTDIR - decks, pictures, portable data
|
|
; folders - is not on those lists and survives, and $INSTDIR itself is
|
|
; only removed when nothing else is left in it. An earlier version of this
|
|
; section ran a blanket "RMDir /r $INSTDIR" to keep build-tree artifacts
|
|
; out of a fresh reinstall; that also deleted user decks whenever a user
|
|
; had pointed their data folders into the install directory, so payload
|
|
; purity may never be bought at that price again.
|
|
@CPACK_NSIS_DELETE_FILES@
|
|
@CPACK_NSIS_DELETE_DIRECTORIES@
|
|
|
|
; Written by the installer itself, so absent from the staged payload
|
|
; above; qdebug.txt is app-generated log output next to the executables.
|
|
Delete "$INSTDIR\uninstall.exe"
|
|
Delete "$INSTDIR\portable.dat"
|
|
Delete "$INSTDIR\qdebug.txt"
|
|
|
|
; Succeeds only when no foreign files remain.
|
|
RMDir "$INSTDIR"
|
|
|
|
RMDir /r "$SMPROGRAMS\Cockatrice"
|
|
|
|
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice"
|
|
|
|
; Leftover WER LocalDumps key from pre-Sentry installers (see the installer note
|
|
; above): removing it here covers machines that uninstall without reinstalling.
|
|
; The legacy full-memory dumps go with it; nothing consumes them anymore. Same shell
|
|
; context note as the installer: the per-user dumps folder needs "current".
|
|
DeleteRegKey HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe"
|
|
SetShellVarContext current
|
|
RMDir /r "$LOCALAPPDATA\CrashDumps\Cockatrice"
|
|
SetShellVarContext all
|
|
|
|
; Only remove the file/protocol associations if we registered them (i.e. the
|
|
; install was not portable) and .cod is still owned by Cockatrice, so we don't
|
|
; clobber a .cod association installed by another application.
|
|
${If} $R1 == 0
|
|
ReadRegStr $0 HKCR ".cod" ""
|
|
${If} $0 == "Cockatrice"
|
|
DeleteRegKey HKCR ".cod"
|
|
DeleteRegKey HKCR "Cockatrice"
|
|
DeleteRegKey HKCR "cockatrice"
|
|
${EndIf}
|
|
${EndIf}
|
|
SectionEnd
|
|
|
|
; unselected because it is /o
|
|
Section /o "un.Configurations, decks, cards, pics" UnSecConfiguration
|
|
SetShellVarContext current
|
|
DeleteRegKey HKCU "Software\Cockatrice"
|
|
|
|
RMDir /r "$LOCALAPPDATA\Cockatrice"
|
|
SectionEnd
|
|
|
|
LangString DESC_SecApplication ${LANG_ENGLISH} "Cockatrice program files"
|
|
LangString DESC_SecStartMenu ${LANG_ENGLISH} "Create start menu items for Cockatrice and Oracle."
|
|
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
|
|
!insertmacro MUI_DESCRIPTION_TEXT ${SecApplication} $(DESC_SecApplication)
|
|
!insertmacro MUI_DESCRIPTION_TEXT ${SecStartMenu} $(DESC_SecStartMenu)
|
|
!insertmacro MUI_FUNCTION_DESCRIPTION_END
|
|
|
|
LangString DESC_UnSecApplication ${LANG_ENGLISH} "Cockatrice program files and start menu items"
|
|
LangString DESC_UnSecConfiguration ${LANG_ENGLISH} "Configurations, decks, card database, pictures"
|
|
!insertmacro MUI_UNFUNCTION_DESCRIPTION_BEGIN
|
|
!insertmacro MUI_DESCRIPTION_TEXT ${UnSecApplication} $(DESC_UnSecApplication)
|
|
!insertmacro MUI_DESCRIPTION_TEXT ${UnSecConfiguration} $(DESC_UnSecConfiguration)
|
|
!insertmacro MUI_UNFUNCTION_DESCRIPTION_END
|