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
* [Installer] Offer to close blocking processes and retry purge on upgrade * [Installer] Retry the purge after closing only our own applications The first version of this fix closed every Restart Manager holder through RmShutdown, forcing what refused to close. RmShutdown also stops services, and the only API that brings them back is RmRestart, which would restart Cockatrice itself into the files that are about to be replaced; the forced pass also contradicts the running-instance guard, which never force-closes Cockatrice because it may be holding an unsaved deck. The guard ran first anyway, so for the case the fix exists for - a Cockatrice restarted during the update - the probe never got to run: the guard waited 60 s and aborted. Keep the prompt and the retry, but ask only Cockatrice, Oracle and Servatrice to close, with WM_CLOSE and nothing else, and leave every other holder named by the probe to the user. Repeat the prompt while the user keeps saying Yes, refresh the names after a failed retry, and keep the /R abort message pointing at the downloaded installer. Also clear the read-only attribute before every purge attempt. Delete and RMDir leave such a file in place, so the retry could never make progress, and the diagnostics branch that named the cause had no action behind it. The holder probe goes back to being diagnostic-only, so its Restart Manager code and the --close argument are dropped. * [Installer] Make an accepted close of the running client actually land WM_CLOSE is a request, and Cockatrice answers it: while a card database update is running, a game is open or a deck is unsaved it puts up its own "are you sure?" confirmation. That dialog opened behind the installer's prompt, so the user saw nothing happen; the next Yes was posted to a main window the app's own modal dialog had disabled, so it was ignored as well, and the identical yes/no prompt came back with no hint of why. - Activate the matching window before posting WM_CLOSE, so the app's own confirmation is in front where it can be answered. - Wait up to 30 s for cockatrice.exe to exit instead of purging after a fixed 2 s, so the purge measures the app gone rather than its dialog. - On the second and later rounds ask with Retry/Cancel and say what is needed, instead of repeating the same offer that just did nothing. - Stop promising Oracle and Servatrice: they are console applications, so CloseMainWindow() has nothing to send WM_CLOSE to. They are not force-stopped either - Oracle writes the card database in place. * [Installer] Close the client from the holder probe, not PowerShell Accepting the offer to close did nothing on a real machine: the instance stayed up, never came forward, and the same prompt came back. The prompt names its holders from the Restart Manager probe, but the close itself went through powershell.exe: Get-Process -Name, a strict "path starts with $INSTDIR" filter, then Process.CloseMainWindow() and a WScript.Shell AppActivate. Every step of that is silent. A process whose path cannot be read is filtered out, a process .NET finds no main window for makes CloseMainWindow() return false without posting anything, and AppActivate is refused for a background caller - so an attempted close, a missed close and a close request the client ignored all looked the same in the log: nothing. Ask the probe instead. installer-holder-probe --close-holders closes exactly the processes Restart Manager just named as holding a file of the version being replaced, so no executable path has to be guessed and a third party application that merely shares the image name is never in the set. Per instance it restores and brings the top-level window forward (AttachThreadInput, so the client's own "really quit?" confirmation is on screen rather than behind the installer), posts WM_CLOSE to the main window, and waits up to 30 s for the process to exit. Its report names every holder with what happened to it, including the ones left to the user, and goes into the installer log as one line. That also removes the separate PowerShell polling loop that used to wait for the close: the probe holds a handle on the process it asked, so the purge that follows measures a settled directory. Oracle and Servatrice are still never closed, and nothing is force-stopped. --------- Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2189 lines
104 KiB
Text
2189 lines
104 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 AllowForceClose
|
|
Var GuardDir
|
|
Var OldUninstallString
|
|
Var OldInstallDir
|
|
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
|
|
; OfferToCloseAndRetryPurge counts the accepted close-and-retry rounds, so the
|
|
; prompt can stop repeating an offer that provably did not help (see there).
|
|
Var HolderCloseRounds
|
|
Var WaitPid
|
|
Var LauncherExited
|
|
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
|
|
|
|
Function CockLogWrite
|
|
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
|
|
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 ""
|
|
; 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 by polling powershell.exe for an image name - a guess that can come back
|
|
; "not running" or "could not determine" while the process 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 it is the one check here that needs
|
|
; no powershell.exe: on a machine where the process lookup cannot run at all, the
|
|
; launcher is still proven gone here instead of the whole upgrade being cancelled
|
|
; by a check that was only ever a proxy for this one.
|
|
;
|
|
; Then make sure no *other* application instance is still running (and holding
|
|
; file locks) before the previous version is uninstalled. On a silent update
|
|
; (/R /S) the running processes are asked to close gracefully and waited for;
|
|
; Cockatrice itself is never force-killed (data loss, e.g. an unsaved deck) so the
|
|
; update is aborted instead, while the console applications Oracle and Servatrice
|
|
; are force-stopped by PID once the grace period is over. On interactive installs
|
|
; the user is prompted to close them instead.
|
|
; 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}
|
|
|
|
; 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
|
|
|
|
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
|
|
ExecWait '$R0 _?=$OldInstallDir'
|
|
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
|
|
ExecWait '$R0 _?=$OldInstallDir'
|
|
Call RestoreUserDataAfterUninstall
|
|
|
|
done64:
|
|
${EndIf}
|
|
|
|
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.
|
|
;
|
|
; On a silent update (/R /S) matching processes are asked to close gracefully
|
|
; (WM_CLOSE) and waited for up to 60 s. Cockatrice is never force-stopped: it
|
|
; may be holding an unsaved deck or showing a "really leave this game?"
|
|
; confirmation, so if it refuses to close the install is aborted - nothing has
|
|
; been replaced yet, so the update is simply deferred. Oracle and Servatrice
|
|
; are console applications without a main window, so CloseMainWindow() does
|
|
; nothing for them and waiting cannot help; they hold no unsaved user data and
|
|
; are force-stopped by PID once the grace period is over. On interactive
|
|
; installs and uninstalls the user is prompted to close them instead.
|
|
;
|
|
; Processes are matched by image name AND by their executable path living
|
|
; under the guarded directory ($GuardDir, normally $INSTDIR), so unrelated
|
|
; processes that merely share an image name (e.g. the Oracle DB instance
|
|
; "oracle.exe") are never touched.
|
|
|
|
; usage: set $R2 to the base image name (without extension, e.g. "cockatrice",
|
|
; as accepted by Get-Process -Name), call this, result in $R0
|
|
; (1 = running from $GuardDir, 0 = not running, 2 = could not determine)
|
|
Function IsAppRunning
|
|
; Every caller has to set $GuardDir; an empty one would make the path check
|
|
; below match every process that merely shares the image name, which is the
|
|
; opposite of what this guard is for.
|
|
${If} $GuardDir == ""
|
|
StrCpy $R0 2
|
|
Return
|
|
${EndIf}
|
|
; Pass the directory to guard to PowerShell via an environment variable so
|
|
; it never has to be embedded into the -Command string: apostrophes in a
|
|
; path would terminate the command early, and the path in a -like pattern
|
|
; would treat [ and ] as wildcard characters.
|
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
|
; Exit the process explicitly so nsExec's pop reflects whether a matching
|
|
; process was found: powershell.exe exits 0 whether or not the pipeline
|
|
; produced objects. 0 = not running, 1 = running, anything else = could not
|
|
; be determined (see IsProcessLookupAvailable).
|
|
; A process is only a match when its path is readable and lives under
|
|
; $GuardDir. A process we cannot inspect is not one of ours: the updater is
|
|
; always started by the application it updates, so it runs in the same user
|
|
; session, and an interactive install is elevated and can read the paths of
|
|
; the user's own processes. Assuming a match instead would be a hard stop
|
|
; rather than a fail-closed guard - an Oracle database instance running as
|
|
; SYSTEM has a process named "oracle" whose path raises Access denied, and
|
|
; that would make every silent update abort and every interactive install
|
|
; loop on the Retry prompt with no way out but Cancel.
|
|
nsExec::ExecToLog 'powershell -NoProfile -Command "if (Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | Select-Object -First 1) { exit 1 } else { exit 0 }"'
|
|
Pop $R0
|
|
${If} $R0 == "0"
|
|
StrCpy $R0 0
|
|
${ElseIf} $R0 == "1"
|
|
StrCpy $R0 1
|
|
${Else}
|
|
; powershell.exe could not be run at all (nsExec pushes the string
|
|
; "error"), or died with an unexpected code. That is "could not
|
|
; determine", which has to stay distinguishable from "definitely
|
|
; running": a state that can never clear would block the installer
|
|
; forever. IsProcessLookupAvailable warns about that case once.
|
|
StrCpy $R0 2
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
; Sets $R0 to 1 if PowerShell can be started at all, 0 if it cannot. The
|
|
; running instance check is built on top of it, so a machine where
|
|
; powershell.exe is missing, blocked by policy or refused by antivirus is
|
|
; reported once instead of being reported as "Cockatrice is running" on every
|
|
; single check.
|
|
Function IsProcessLookupAvailable
|
|
nsExec::ExecToLog 'powershell -NoProfile -Command "exit 0"'
|
|
Pop $R0
|
|
${If} $R0 == "0"
|
|
StrCpy $R0 1
|
|
${Else}
|
|
StrCpy $R0 0
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
Function CloseMatchingApps
|
|
; gracefully ask every matching instance to close (sends WM_CLOSE)
|
|
;
|
|
; The filter here is deliberately the strict one: a process is only asked to
|
|
; close when its path is readable and lives under $GuardDir. Treating a
|
|
; process whose path cannot be read (an unrelated oracle.exe / servatrice.exe,
|
|
; or one owned by another account running as SYSTEM) as a match would make
|
|
; the installer close a third party application, and it runs elevated, so
|
|
; CloseMainWindow() would succeed on it.
|
|
;
|
|
; The window is activated first, because WM_CLOSE is answered, not obeyed:
|
|
; Cockatrice replies with a confirmation of its own while a card database
|
|
; update is running, a game is open or a deck is unsaved. Without the
|
|
; activation that dialog opens behind the installer's own prompt, where it
|
|
; looks like the close did nothing and the second WM_CLOSE is delivered to a
|
|
; main window its own modal dialog has disabled, which is ignored. Failing to
|
|
; activate is harmless and swallowed: the close request still goes out.
|
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
|
nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | ForEach-Object { try { $$null = (New-Object -ComObject WScript.Shell).AppActivate($$_.Id) } catch { }; $$null = $$_.CloseMainWindow() }"'
|
|
Pop $R3
|
|
FunctionEnd
|
|
|
|
Function ForceCloseApps
|
|
; stop the matching instances by PID once the grace period is over. Only
|
|
; used for the console applications ($AllowForceClose), which have no window
|
|
; to close and hold no unsaved user data. Same strict filter as above.
|
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
|
nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | ForEach-Object { Stop-Process -Id $$_.Id -Force -ErrorAction SilentlyContinue }"'
|
|
Pop $R3
|
|
FunctionEnd
|
|
|
|
Function WaitForAppToClose
|
|
; usage: set $R1 to the display name (e.g. "cockatrice.exe") and $R2 to the
|
|
; base image name (e.g. "cockatrice")
|
|
Call IsAppRunning
|
|
!insertmacro CockLog "guard $R1: state=$R0 (dir=$GuardDir)"
|
|
; Only a definite "running" is worth waiting for. "Not running" is done, and
|
|
; "could not determine" must not keep the installer waiting on an answer it
|
|
; will never get - but it cannot simply be waved through either, see
|
|
; HandleUnknownAppState.
|
|
${If} $R0 = 0
|
|
Return
|
|
${EndIf}
|
|
${If} $R0 = 2
|
|
Call HandleUnknownAppState
|
|
Return
|
|
${EndIf}
|
|
|
|
${If} ${Silent}
|
|
; ask the application to close gracefully (WM_CLOSE), then wait for it to exit
|
|
DetailPrint "Closing $R1 ..."
|
|
Call CloseMatchingApps
|
|
; wall-clock deadline: each poll has to spawn a fresh powershell.exe, so
|
|
; a bare loop counter would not bound the real wait time to 60 s.
|
|
System::Call 'kernel32::GetTickCount()i.s'
|
|
Pop $R8
|
|
ck_wait_loop:
|
|
Sleep 500
|
|
Call IsAppRunning
|
|
${If} $R0 = 0
|
|
!insertmacro CockLog "guard $R1: closed gracefully"
|
|
DetailPrint "$R1 closed."
|
|
Return
|
|
${EndIf}
|
|
${If} $R0 = 2
|
|
Call HandleUnknownAppState
|
|
Return
|
|
${EndIf}
|
|
System::Call 'kernel32::GetTickCount()i.s'
|
|
Pop $R9
|
|
IntOp $R9 $R9 - $R8
|
|
${If} $R9 < 60000
|
|
Goto ck_wait_loop
|
|
${EndIf}
|
|
; The grace period is over. Cockatrice is never force-stopped: it may be
|
|
; showing a "really leave this game?" confirmation or holding an unsaved
|
|
; deck, and nothing has been replaced yet, so aborting just defers the
|
|
; update and preserves the user's data.
|
|
${If} $AllowForceClose = 0
|
|
!insertmacro CockLog "guard $R1: still running after 60s, aborting (no force close)"
|
|
DetailPrint "$R1 is still running after 60 s; aborting the update so that no data is lost."
|
|
StrCpy $AbortReason \
|
|
"$R1 did not close within 60 seconds, so the installation was aborted to avoid losing unsaved data.$\r$\n$\r$\nPlease close $R1 and run the installer again."
|
|
Call AbortInstall
|
|
${EndIf}
|
|
; Oracle and Servatrice are console applications: CloseMainWindow()
|
|
; returned false for them and waiting longer cannot help. They hold no
|
|
; unsaved user data, so stop them by PID.
|
|
!insertmacro CockLog "guard $R1: still running after 60s, force-stopping"
|
|
DetailPrint "Force-stopping $R1 ..."
|
|
Call ForceCloseApps
|
|
Sleep 2000
|
|
Call IsAppRunning
|
|
${If} $R0 = 0
|
|
!insertmacro CockLog "guard $R1: force-stopped"
|
|
DetailPrint "$R1 closed."
|
|
Return
|
|
${EndIf}
|
|
${If} $R0 = 2
|
|
Call HandleUnknownAppState
|
|
Return
|
|
${EndIf}
|
|
!insertmacro CockLog "guard $R1: could not be stopped, aborting"
|
|
DetailPrint "$R1 is still running after 60 s; aborting the update."
|
|
StrCpy $AbortReason \
|
|
"$R1 could not be stopped, so the installation was aborted.$\r$\n$\r$\nPlease close $R1 and run the installer again."
|
|
Call AbortInstall
|
|
${Else}
|
|
ck_wait_prompt:
|
|
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
|
|
"$R1 is still running.$\r$\n$\r$\nPlease close it, then click Retry.$\r$\nClick Cancel to abort." \
|
|
IDCANCEL ck_abort_install
|
|
Call IsAppRunning
|
|
${If} $R0 = 0
|
|
Return
|
|
${EndIf}
|
|
${If} $R0 = 2
|
|
Call HandleUnknownAppState
|
|
Return
|
|
${EndIf}
|
|
Goto ck_wait_prompt
|
|
ck_abort_install:
|
|
Abort
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
; A process lookup came back "could not determine": powershell.exe ran but the
|
|
; answer was not usable, e.g. the process could not be inspected at all. That
|
|
; state is fine to carry on from when the user is at the keyboard - they can
|
|
; close the applications by hand and the stale-runtime check still stands
|
|
; between a failed guard and a mismatched install.
|
|
;
|
|
; It is not fine on a silent upgrade. There the whole point of this guard is to
|
|
; know that nothing holds locks on the files about to be replaced, and this is
|
|
; exactly the state where that is unknown, with no one to click a dialog: the
|
|
; old code returned and let the install carry on into the mixed Qt DLL set that
|
|
; made the next start fail. Cancel the update instead; the next attempt runs
|
|
; with the applications closed and succeeds.
|
|
Function HandleUnknownAppState
|
|
${If} $ReinstallMode = 1
|
|
StrCpy $AbortReason \
|
|
"The installation was cancelled because it could not determine whether Cockatrice is still running.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice and run the installer again."
|
|
Call AbortInstall
|
|
${EndIf}
|
|
DetailPrint "Could not determine whether $R1 is running; close Cockatrice, Oracle and Servatrice manually if the installation reports a problem."
|
|
${IfNot} ${Silent}
|
|
MessageBox MB_OK|MB_ICONEXCLAMATION \
|
|
"This installer could not determine whether $R1 is still running.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice manually before continuing."
|
|
${EndIf}
|
|
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 the unattended case is handled the same way
|
|
; everywhere: the in-app updater runs this installer silent and detached and
|
|
; never looks at its exit code, so a plain Abort would leave the user with no
|
|
; update and no explanation, while a /S install without /R is unattended - by a
|
|
; package manager, for instance - and a message box there would block forever.
|
|
; Only the first of those two gets a dialog; the second reports through the log
|
|
; and the exit code.
|
|
Function AbortInstall
|
|
!insertmacro CockLog "ABORT: $AbortReason"
|
|
!insertmacro CockLog "full installer log: $InstallLogPath"
|
|
; Show a dialog whenever someone is at the keyboard to see it: an interactive
|
|
; install, or the in-app updater's /R upgrade. The /R upgrade is silent, but
|
|
; the client that launched the installer has already quit, so the user is
|
|
; still there. Only a /S install without /R is attended by a package manager
|
|
; and must go straight to the exit code.
|
|
${If} $ReinstallMode = 1
|
|
${OrIfNot} ${Silent}
|
|
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. This block already runs
|
|
; only when someone is at the keyboard (attended install, or the updater's /R upgrade whose launcher has
|
|
; quit), so a silent or package-manager install never reaches the prompt and its behavior is unchanged.
|
|
; 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 here: a /SD would suppress this prompt on the silent /R upgrade, which is exactly the
|
|
; attended-update case this offer exists for. The runtime gate above (ReinstallMode/Silent) is the
|
|
; hang protection — a package-manager /S install without /R never reaches this box, matching the
|
|
; two sibling MessageBox sites in this file.
|
|
!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
|
|
${EndIf}
|
|
SetErrorLevel 1
|
|
Abort
|
|
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 "not running" obtained by
|
|
; enumerating process names 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 powershell.exe, and is unaffected by a process whose path cannot be
|
|
; read - the case the name based check has to treat as "not ours" so it never
|
|
; closes a third party application.
|
|
;
|
|
; 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 name based guard.
|
|
Function WaitForLauncherToExit
|
|
StrCpy $LauncherExited 0
|
|
!insertmacro CockLog "wait-launcher: pid='$WaitPid'"
|
|
${If} $WaitPid == ""
|
|
!insertmacro CockLog "wait-launcher: no pid given, falling back to name check"
|
|
Return
|
|
${EndIf}
|
|
; PROCESS_SYNCHRONIZE is the only access this needs - enough to wait on the
|
|
; handle, and the least likely to be refused for a reason that has nothing to
|
|
; do with the upgrade.
|
|
;
|
|
; 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.
|
|
ClearErrors
|
|
System::Call 'kernel32::OpenProcess(i 0x00100000, 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
|
|
; The launcher is gone - OpenProcess cannot open a dead process - which is
|
|
; exactly the state the wait was for.
|
|
!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 ..."
|
|
; Same 60 s grace period the name based check uses, so a client that is slow
|
|
; to flush its card database does not get less room than it used to.
|
|
; WAIT_OBJECT_0 is 0; WAIT_TIMEOUT is 258 and a failed wait reports 0xFFFFFFFF.
|
|
System::Call 'kernel32::WaitForSingleObject(p R0, i 60000) i.R1'
|
|
System::Call 'kernel32::CloseHandle(p R0)'
|
|
${If} $R1 != 0
|
|
!insertmacro CockLog "wait-launcher: did not exit within 60s (wait=$R1)"
|
|
DetailPrint "The application that started this installer did not exit within 60 s."
|
|
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
|
|
${EndIf}
|
|
!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
|
|
|
|
Function GuardAppsNotRunning
|
|
; usage: set $GuardDir to the directory to guard, then call this
|
|
Call IsProcessLookupAvailable
|
|
!insertmacro CockLog "guard: powershell available=$R0 dir=$GuardDir launcherExited=$LauncherExited"
|
|
${If} $R0 = 0
|
|
; Without PowerShell we cannot tell whether a running instance is
|
|
; holding locks on the files we are about to replace.
|
|
!insertmacro CockLog "guard: powershell unavailable, skipping instance check"
|
|
DetailPrint "Could not start powershell.exe; skipping the running instance check. Close Cockatrice, Oracle and Servatrice manually if the installation reports a problem."
|
|
; On a silent upgrade that used to be a warning and nothing more, which
|
|
; left the very process that launched the installer free to go on
|
|
; replacing files it still had mapped - the one outcome this guard exists
|
|
; to prevent - and then writing the new payload on top of them. Cancel
|
|
; instead. An interactive install can still continue: the user is at the
|
|
; keyboard, and refusing outright would leave them with an installer that
|
|
; can never complete.
|
|
;
|
|
; Unless the launcher has already been waited on directly: WaitForLauncherToExit
|
|
; needs no powershell.exe, so when it has run, Cockatrice is known to be
|
|
; gone and the lookup failing to run costs nothing but the Oracle and
|
|
; Servatrice checks. An older client that passes no /PID= leaves no such
|
|
; authority, and there the upgrade is cancelled as before.
|
|
${If} $LauncherExited != 1
|
|
${AndIf} $ReinstallMode = 1
|
|
StrCpy $AbortReason \
|
|
"The installation was cancelled because it could not check whether Cockatrice is still running, because powershell.exe could not be started.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice and run the installer again."
|
|
Call AbortInstall
|
|
${EndIf}
|
|
; The user has to be told to close the applications by hand, but a /S
|
|
; install without /R is unattended and would block on this forever.
|
|
${If} $ReinstallMode = 1
|
|
${OrIfNot} ${Silent}
|
|
MessageBox MB_OK|MB_ICONEXCLAMATION "This installer could not check whether Cockatrice is still running, because powershell.exe could not be started.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice manually before continuing."
|
|
${EndIf}
|
|
Return
|
|
${EndIf}
|
|
StrCpy $R1 "cockatrice.exe"
|
|
StrCpy $R2 "cockatrice"
|
|
; a GUI application with unsaved user data: graceful close only
|
|
StrCpy $AllowForceClose 0
|
|
Call WaitForAppToClose
|
|
StrCpy $R1 "oracle.exe"
|
|
StrCpy $R2 "oracle"
|
|
StrCpy $AllowForceClose 1
|
|
Call WaitForAppToClose
|
|
StrCpy $R1 "servatrice.exe"
|
|
StrCpy $R2 "servatrice"
|
|
StrCpy $AllowForceClose 1
|
|
Call WaitForAppToClose
|
|
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.
|
|
;
|
|
; The probe opens each survivor for DELETE access and then closes it again
|
|
; without deleting anything. The code is read by the plugin right after that call
|
|
; (?e), so a failure reports what Windows refused; clearing LastError first makes
|
|
; a zero code mean "the open worked". That is all it means: Windows lets a DLL a
|
|
; running process has mapped be opened for DELETE, and DeleteFile still refuses
|
|
; it until the image is unloaded, so a zero code is not evidence that the
|
|
; failure was transient - the holders line is what names a running process. The
|
|
; plugin parses the call's arguments between the clear and the call, so a
|
|
; non-zero code is reported as what Windows said rather than as proof of a
|
|
; cause - the wording below never claims one.
|
|
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
|
|
; A valid handle: close it. The open 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 (the purge clears it before every attempt
|
|
; now, so a survivor that is read-only had it set again). Report
|
|
; that standing cause instead of calling the failure transient.
|
|
System::Call 'kernel32::CloseHandle(p R1)'
|
|
System::Call 'kernel32::GetFileAttributesW(w "$INSTDIR\$StaleFindName") i.R3'
|
|
${If} $R3 = -1
|
|
StrCpy $R0 "a delete-access open now succeeds, but its attributes could not be read"
|
|
${Else}
|
|
IntOp $R3 $R3 & 1
|
|
${If} $R3 = 1
|
|
StrCpy $R0 "a delete-access open now succeeds, but it is read-only, which stops Delete"
|
|
${Else}
|
|
StrCpy $R0 "a delete-access open now succeeds, but a DLL a running process has mapped still refuses Delete - see holders"
|
|
${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 install directory's files
|
|
; 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 a small executable built from tools/installer-holder-probe, extracted to
|
|
; $PLUGINSDIR only when the purge has already failed: the successful path pays nothing
|
|
; for it, and $PLUGINSDIR takes it away again when the installer exits. It is compiled by
|
|
; the same Windows build that compiles this installer, which is what lets it call the
|
|
; Restart Manager API directly. The same call made from this script would have to
|
|
; marshal RM_PROCESS_INFO through System::Call as a table of hand-computed offsets, and
|
|
; that table is precisely what cannot be checked without a Windows toolchain; in
|
|
; compiled code the layout belongs to the Windows SDK header.
|
|
;
|
|
; Restart Manager is what makes the answer complete. Walking processes can only name one
|
|
; whose image lives here or that loaded one of these libraries, while Restart Manager
|
|
; names a process that holds a plain handle on a file - an antivirus scanner - and names
|
|
; services as well as user programs. Its blind spot is a handle taken in kernel mode,
|
|
; where there is no process to name; then the 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
|
|
; running-instance 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
|
|
InitPluginsDir
|
|
; Embedded by the same relative path that resolves NSIS.definitions.nsh above (both
|
|
; are built into the build root, which is three levels up from project.nsi).
|
|
File "/oname=$PLUGINSDIR\installer-holder-probe.exe" "..\..\..\installer-holder-probe.exe"
|
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$INSTDIR")'
|
|
; /TIMEOUT because this runs on the abort path, ahead of the message box: one call
|
|
; into the Restart Manager cannot take the minute a walk over every process can, but
|
|
; if that service stalls it must not leave the installer hanging with nothing on
|
|
; screen. On a timeout nsExec reports it through the exit code like any other failure.
|
|
nsExec::ExecToStack /TIMEOUT=20000 '"$PLUGINSDIR\installer-holder-probe.exe"'
|
|
Pop $HolderExit
|
|
Pop $HolderOutput
|
|
; 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 any more"
|
|
${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 - ask, close what this
|
|
; installer owns, purge again, and retry while the user keeps asking. Sets
|
|
; $StaleRuntimeAfterPurge to the last purge's verdict, so the caller still aborts
|
|
; when the files survive.
|
|
;
|
|
; Cockatrice is asked to close (WM_CLOSE, the same request as clicking the
|
|
; window's close button) and 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, exactly as the
|
|
; running-instance guard treats it. The instance is brought forward first, so that
|
|
; confirmation is on screen rather than behind this installer's prompt. Oracle and
|
|
; Servatrice have no window to send a close to at all, and nothing here is
|
|
; force-stopped: Oracle writes the card database in place and killing it mid-write
|
|
; would corrupt it. Everything the probe named that this installer cannot close -
|
|
; those two included - is left to the user, whom the prompt names it to.
|
|
;
|
|
; 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.
|
|
;
|
|
; No offer when nobody is at the keyboard (a /S install without /R, e.g. a package
|
|
; manager): a dialog there would block forever. /R is the in-app updater, whose
|
|
; client has already quit, so the user is there - the same reasoning AbortInstall
|
|
; uses for its message box. No offer either when the probe named nobody: there is
|
|
; nothing to close, and the abort's per-file diagnostics already record what
|
|
; Windows said.
|
|
;
|
|
; The first round offers the close; every later one stops offering it and says
|
|
; what to do instead. Repeating the same question is what made an accepted close
|
|
; look like a Yes that was ignored: when Cockatrice was blocked on its own
|
|
; confirmation, the second Yes was delivered to a window that dialog had disabled,
|
|
; so nothing closed, the holders were unchanged, and the identical prompt came
|
|
; back with no hint of what had happened.
|
|
Function OfferToCloseAndRetryPurge
|
|
${If} $ReinstallMode = 0
|
|
${AndIf} ${Silent}
|
|
!insertmacro CockLog "close-holders: unattended install, not offering to close holders"
|
|
Return
|
|
${EndIf}
|
|
${If} $HolderFound != 1
|
|
!insertmacro CockLog "close-holders: no holder was named, nothing to offer"
|
|
Return
|
|
${EndIf}
|
|
StrCpy $HolderCloseRounds 0
|
|
ocrp_prompt:
|
|
${If} $HolderCloseRounds = 0
|
|
MessageBox MB_YESNO|MB_ICONEXCLAMATION|MB_DEFBUTTON2 \
|
|
"Files of the previous version of Cockatrice are still in use by:$\r$\n$\r$\n$HolderOutput$\r$\n$\r$\nThe installer can close Cockatrice for you. It may first ask you to confirm quitting - it does while a card database update is running, a game is open or a deck is unsaved - so answer that window. Close Oracle, Servatrice and any other program listed above by hand.$\r$\n$\r$\nChoose Yes to close Cockatrice and try again, or No to cancel the installation." \
|
|
IDYES ocrp_close
|
|
!insertmacro CockLog "close-holders: declined by user"
|
|
Return
|
|
${EndIf}
|
|
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
|
|
"Files of the previous version of Cockatrice are still in use by:$\r$\n$\r$\n$HolderOutput$\r$\n$\r$\nCockatrice may be waiting for an answer in its own window - it asks before quitting while a card database update is running, a game is open or a deck is unsaved. Switch to it and confirm, then close Oracle, Servatrice and any other program listed above.$\r$\n$\r$\nChoose Retry once they are closed, or Cancel to cancel the installation." \
|
|
IDRETRY ocrp_close
|
|
!insertmacro CockLog "close-holders: declined by user after $HolderCloseRounds round(s)"
|
|
Return
|
|
ocrp_close:
|
|
IntOp $HolderCloseRounds $HolderCloseRounds + 1
|
|
!insertmacro CockLog "close-holders: accepted (round $HolderCloseRounds), holders were [$HolderOutput]"
|
|
; The close is a request, not a fact: AskCockatriceAppsToClose waits for the
|
|
; instances it asked to exit, so a confirmation of Cockatrice's that is still up is
|
|
; reported as still running instead of being measured as a successful close.
|
|
Call AskCockatriceAppsToClose
|
|
Call RetryDllPurge
|
|
StrCpy $StaleRuntimeAfterPurge $R0
|
|
!insertmacro CockLogListing "after-close-purge" "*.dll"
|
|
!insertmacro CockLogListing "after-close-plugins" "Plugins\*.*"
|
|
!insertmacro CockLog "close-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 close and not what was there before it.
|
|
Call LogStaleRuntimeDiagnostics
|
|
${If} $HolderFound != 1
|
|
Return
|
|
${EndIf}
|
|
Goto ocrp_prompt
|
|
FunctionEnd
|
|
|
|
; Asks the running Cockatrice to close, and waits for it to exit.
|
|
;
|
|
; The set is the Restart Manager answer from LogProcessesUsingInstallDir: exactly the
|
|
; processes holding a file of the version being replaced. tools\installer-holder-probe
|
|
; closes that set by pid, which is why nothing here has to match an executable path or
|
|
; depend on powershell.exe. Only cockatrice.exe is asked: Oracle writes the card database
|
|
; in place and Servatrice has no window, so both stay the user's job, as the prompt says.
|
|
;
|
|
; 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 purge 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.
|
|
Function AskCockatriceAppsToClose
|
|
InitPluginsDir
|
|
; LogProcessesUsingInstallDir extracted it already; the check keeps a single copy of
|
|
; the executable in the installer and still works if that order ever changes.
|
|
IfFileExists "$PLUGINSDIR\installer-holder-probe.exe" ck_close_probe_ready
|
|
File "/oname=$PLUGINSDIR\installer-holder-probe.exe" "..\..\..\installer-holder-probe.exe"
|
|
ck_close_probe_ready:
|
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$INSTDIR")'
|
|
; 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
|
|
!insertmacro CockLog "close-holders: probe exit=$R3 [$R4]"
|
|
DetailPrint "Closing Cockatrice: $R4"
|
|
FunctionEnd
|
|
|
|
; Uninstaller copies of the same routines (the uninstaller gets its own
|
|
; function set compiled in, it cannot call the installer functions).
|
|
Function un.IsAppRunning
|
|
; An unset $GuardDir would match every process that merely shares the image
|
|
; name, see IsAppRunning in the installer.
|
|
${If} $GuardDir == ""
|
|
StrCpy $R0 2
|
|
Return
|
|
${EndIf}
|
|
; Pass the directory to guard to PowerShell via an environment variable so
|
|
; it never has to be embedded into the -Command string: apostrophes in a
|
|
; path would terminate the command early, and the path in a -like pattern
|
|
; would treat [ and ] as wildcard characters.
|
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
|
; Exit the process explicitly so nsExec's pop reflects whether a matching
|
|
; process was found: powershell.exe exits 0 whether or not the pipeline
|
|
; produced objects. 0 = not running, 1 = running, anything else = could not
|
|
; be determined (see un.IsProcessLookupAvailable).
|
|
; Strict match: a readable path under $GuardDir, same as in the installer.
|
|
nsExec::ExecToLog 'powershell -NoProfile -Command "if (Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | Select-Object -First 1) { exit 1 } else { exit 0 }"'
|
|
Pop $R0
|
|
${If} $R0 == "0"
|
|
StrCpy $R0 0
|
|
${ElseIf} $R0 == "1"
|
|
StrCpy $R0 1
|
|
${Else}
|
|
; powershell.exe could not be run at all (nsExec pushes the string
|
|
; "error"), or died with an unexpected code. That is "could not
|
|
; determine", which has to stay distinguishable from "definitely
|
|
; running": a state that can never clear would block the uninstaller
|
|
; forever. un.IsProcessLookupAvailable warns about that case once.
|
|
StrCpy $R0 2
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
Function un.IsProcessLookupAvailable
|
|
nsExec::ExecToLog 'powershell -NoProfile -Command "exit 0"'
|
|
Pop $R0
|
|
${If} $R0 == "0"
|
|
StrCpy $R0 1
|
|
${Else}
|
|
StrCpy $R0 0
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
Function un.CloseMatchingApps
|
|
; gracefully ask every matching instance to close (sends WM_CLOSE)
|
|
;
|
|
; Strict filter, same as in the installer: never close a process whose path
|
|
; cannot be read, it is not necessarily one of ours.
|
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
|
nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | ForEach-Object { $$null = $$_.CloseMainWindow() }"'
|
|
Pop $R3
|
|
FunctionEnd
|
|
|
|
Function un.ForceCloseApps
|
|
; stop the matching instances by PID once the grace period is over, see
|
|
; ForceCloseApps in the installer.
|
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
|
nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | ForEach-Object { Stop-Process -Id $$_.Id -Force -ErrorAction SilentlyContinue }"'
|
|
Pop $R3
|
|
FunctionEnd
|
|
|
|
Function un.WaitForAppToClose
|
|
Call un.IsAppRunning
|
|
; Only a definite "running" is worth waiting for, see WaitForAppToClose.
|
|
${If} $R0 != 1
|
|
Return
|
|
${EndIf}
|
|
|
|
${If} ${Silent}
|
|
DetailPrint "Closing $R1 ..."
|
|
Call un.CloseMatchingApps
|
|
; wall-clock deadline: each poll has to spawn a fresh powershell.exe, so
|
|
; a bare loop counter would not bound the real wait time to 60 s.
|
|
System::Call 'kernel32::GetTickCount()i.s'
|
|
Pop $R8
|
|
un_ck_wait_loop:
|
|
Sleep 500
|
|
Call un.IsAppRunning
|
|
${If} $R0 = 0
|
|
DetailPrint "$R1 closed."
|
|
Return
|
|
${EndIf}
|
|
${If} $R0 != 1
|
|
Return
|
|
${EndIf}
|
|
System::Call 'kernel32::GetTickCount()i.s'
|
|
Pop $R9
|
|
IntOp $R9 $R9 - $R8
|
|
${If} $R9 < 60000
|
|
Goto un_ck_wait_loop
|
|
${EndIf}
|
|
; The grace period is over. Cockatrice is never force-stopped, see
|
|
; WaitForAppToClose in the installer. A silent uninstall only reports
|
|
; through the log and its exit code: it is also run unattended (the /R
|
|
; updater uses it via QuietUninstallString, package managers use it
|
|
; directly), and a message box would block there. The installer that ran
|
|
; us reports the incomplete result to the user itself.
|
|
${If} $AllowForceClose = 0
|
|
DetailPrint "$R1 is still running after 60 s; aborting so that no data is lost."
|
|
SetErrorLevel 1
|
|
Abort
|
|
${EndIf}
|
|
DetailPrint "Force-stopping $R1 ..."
|
|
Call un.ForceCloseApps
|
|
Sleep 2000
|
|
Call un.IsAppRunning
|
|
${If} $R0 = 0
|
|
DetailPrint "$R1 closed."
|
|
Return
|
|
${EndIf}
|
|
${If} $R0 != 1
|
|
Return
|
|
${EndIf}
|
|
DetailPrint "$R1 is still running after 60 s; aborting."
|
|
SetErrorLevel 1
|
|
Abort
|
|
${Else}
|
|
un_ck_wait_prompt:
|
|
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
|
|
"$R1 is still running.$\r$\n$\r$\nPlease close it, then click Retry.$\r$\nClick Cancel to abort." \
|
|
IDCANCEL un_ck_abort_install
|
|
Call un.IsAppRunning
|
|
${If} $R0 = 0
|
|
Return
|
|
${EndIf}
|
|
${If} $R0 != 1
|
|
Return
|
|
${EndIf}
|
|
Goto un_ck_wait_prompt
|
|
un_ck_abort_install:
|
|
Abort
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
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
|
|
Call un.IsProcessLookupAvailable
|
|
${If} $R0 = 0
|
|
; Same reasoning as in the installer: without PowerShell we cannot tell
|
|
; whether a running instance still holds file locks, and refusing to
|
|
; uninstall would leave the user with an uninstaller that never
|
|
; completes. Warn once and continue.
|
|
DetailPrint "Could not start powershell.exe; skipping the running instance check. Close Cockatrice, Oracle and Servatrice manually if the uninstall reports a problem."
|
|
MessageBox MB_OK|MB_ICONEXCLAMATION "This uninstaller could not check whether Cockatrice is still running, because powershell.exe could not be started.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice manually before continuing."
|
|
Return
|
|
${EndIf}
|
|
StrCpy $R1 "cockatrice.exe"
|
|
StrCpy $R2 "cockatrice"
|
|
; a GUI application with unsaved user data: graceful close only
|
|
StrCpy $AllowForceClose 0
|
|
Call un.WaitForAppToClose
|
|
StrCpy $R1 "oracle.exe"
|
|
StrCpy $R2 "oracle"
|
|
StrCpy $AllowForceClose 1
|
|
Call un.WaitForAppToClose
|
|
StrCpy $R1 "servatrice.exe"
|
|
StrCpy $R2 "servatrice"
|
|
StrCpy $AllowForceClose 1
|
|
Call un.WaitForAppToClose
|
|
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
|
|
${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
|
|
!insertmacro CockLog "componentsPagePre: old 32-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)"
|
|
ClearErrors
|
|
Call BackupUserDataBeforeUninstall
|
|
ExecWait "$OldUninstallString _?=$OldInstallDir"
|
|
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
|
|
Abort
|
|
${Else}
|
|
Goto uninst64
|
|
${EndIf}
|
|
|
|
uninst64:
|
|
StrCpy $OldUninstallString $R0
|
|
Call EnsureOldInstallNotRunning
|
|
StrCpy $OldInstallDir $GuardDir
|
|
!insertmacro CockLog "componentsPagePre: old 64-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)"
|
|
ClearErrors
|
|
Call BackupUserDataBeforeUninstall
|
|
ExecWait "$OldUninstallString _?=$OldInstallDir"
|
|
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
|
|
; On /R the previous version is already uninstalled at this point, so an abort
|
|
; leaves the user with no working Cockatrice. Offer to close and purge again,
|
|
; repeating while the user keeps saying Yes; this updates
|
|
; $StaleRuntimeAfterPurge with the last retry's verdict.
|
|
Call OfferToCloseAndRetryPurge
|
|
${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."
|
|
; The in-app updater runs this installer silent and detached and never looks
|
|
; at its exit code, so the user has to be told what happened. A /S install
|
|
; without /R is unattended (a package manager, for instance) and a message
|
|
; box there would block forever, so that case only reports through the log
|
|
; and the exit code. AbortInstall owns that decision.
|
|
StrCpy $AbortReason \
|
|
"The installation could not remove all files of a previous version of Cockatrice, most likely because it is still running or was started as administrator, and was cancelled rather than leave a mismatched install behind.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice and run the installer again."
|
|
${If} $HolderFound = 1
|
|
StrCpy $AbortReason "$AbortReason$\r$\n$\r$\nStill using the install directory:$\r$\n$HolderOutput"
|
|
${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
|