mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
NSIS: - Back up unknown top-level install subdirs (e.g. <installdir>\decks), not just loose deck files, so the 3.1.1 -> new upgrade no longer loses data folders the old RMDir /r deletes before the client ever runs. Payload dirs (plugins/translations/sounds/themes) stay in place to avoid restoring stale DLLs; restore + relocate handle the new backup set and the one-time notice covers both decks and folders. - Restart the %TEMP% backup when the 32/64-bit passes point at different dirs instead of merging (which restored one dir's files into the other); document that only the last pass relocates and the %TEMP% copy is not a durable second copy. Client: - Drop the portable at-risk warning: portable installs never shipped an uninstaller and portable updates preserve data/, so every portable user got a spurious dialog plus daily host-machine copies. - AtRiskDataWarning is now a plain class with Q_DECLARE_TR_FUNCTIONS instead of QObject+Q_OBJECT; logging category renamed to at_risk_data_warning; MainWindow wrapper inlined into startupConfigCheck. - Lower the synchronous startup backup cap 500 -> 200 MB. Utility/tests: - Remove test-only backupAtRiskPath()/findRecentBackupSession() and the no-marker/no-manifest legacy paths; isBackupSessionComplete() now requires the complete marker; tests updated to the plural API.
1671 lines
75 KiB
Text
1671 lines
75 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
|
|
Var AbortReason
|
|
Var WaitPid
|
|
Var LauncherExited
|
|
Var InstallLogPath
|
|
Var LogHandle
|
|
Var LogMessage
|
|
Var LogFindHandle
|
|
Var LogFindName
|
|
Var LogMatchLabel
|
|
Var LogMatchPattern
|
|
|
|
!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 ""
|
|
; 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.
|
|
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 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"
|
|
${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
|
|
|
|
; 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}
|
|
|
|
${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.
|
|
Delete "$INSTDIR\*.dll"
|
|
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.
|
|
Call IsExistingCockatriceInstall
|
|
!insertmacro CockLog "section: IsExistingCockatriceInstall=$R0"
|
|
; 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.
|
|
StrCpy $IsUpgrade $R0
|
|
${If} $PortableMode = 0
|
|
${AndIf} $R0 = 1
|
|
!insertmacro CockLog "section: non-portable upgrade purge"
|
|
!insertmacro CockLogListing "before-purge" "*.dll"
|
|
RMDir /r "$INSTDIR\Plugins"
|
|
Delete "$INSTDIR\*.dll"
|
|
!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.
|
|
${If} $IsUpgrade = 1
|
|
Call HasStaleRuntimeFiles
|
|
!insertmacro CockLog "section: HasStaleRuntimeFiles=$R0"
|
|
${Else}
|
|
StrCpy $R0 0
|
|
${EndIf}
|
|
${If} $R0 = 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."
|
|
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
|
|
|
|
; Enable Windows User-Mode Dumps
|
|
; https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
|
|
WriteRegExpandStr HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpFolder" "%LOCALAPPDATA%\CrashDumps\Cockatrice"
|
|
WriteRegDWORD HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpCount" "5"
|
|
WriteRegDWORD HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpType" "2"
|
|
|
|
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"
|
|
|
|
; 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
|