!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 !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 Function .onInit ${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\ /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} ${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 ; Make sure no 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 Call EnsureAppsNotRunning ${EndIf} ${If} $ReinstallMode = 1 ${AndIf} $PortableMode = 0 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 ; --- 32-bit uninstall --- SetRegView 32 ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString" StrCmp $R0 "" done32 DetailPrint "Removing previous version (32-bit)..." ExecWait '$R0' done32: ; --- 64-bit uninstall --- ${If} ${RunningX64} SetRegView 64 ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString" StrCmp $R0 "" done64 DetailPrint "Removing previous version (64-bit)..." ExecWait '$R0' done64: ${EndIf} 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 ; 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 - EnsureAppsNotRunning has warned about that case already. ${If} $R0 != 1 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 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 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 DetailPrint "$R1 is still running after 60 s; aborting the update so that no data is lost." ; 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. A /S install without /R on the ; other hand is unattended - by 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. ${If} $ReinstallMode = 1 MessageBox MB_OK|MB_ICONEXCLAMATION \ "$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." ${EndIf} SetErrorLevel 1 Abort ${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. DetailPrint "Force-stopping $R1 ..." Call ForceCloseApps Sleep 2000 Call IsAppRunning ${If} $R0 = 0 DetailPrint "$R1 closed." Return ${EndIf} ${If} $R0 != 1 Return ${EndIf} DetailPrint "$R1 is still running after 60 s; aborting the update." ${If} $ReinstallMode = 1 MessageBox MB_OK|MB_ICONEXCLAMATION \ "$R1 could not be stopped, so the installation was aborted.$\r$\n$\r$\nPlease close $R1 and run the installer again." ${EndIf} SetErrorLevel 1 Abort ${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 != 1 Return ${EndIf} Goto ck_wait_prompt ck_abort_install: Abort ${EndIf} FunctionEnd Function EnsureAppsNotRunning ; guards the install directory StrCpy $GuardDir $INSTDIR Call GuardAppsNotRunning 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 ${If} $R0 = 0 ; Without PowerShell we cannot tell whether a running instance is ; holding locks on the files we are about to replace. Warn once and ; continue: refusing to install would leave the user with an installer ; that can never complete, which is worse than the locked-file failure ; this check exists to prevent. MessageBox is shown even in a silent ; install - the user has to be told to close the applications by hand. DetailPrint "Could not start powershell.exe; skipping the running instance check. Close Cockatrice, Oracle and Servatrice manually if the installation reports a problem." 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." 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. ${If} ${FileExists} "$INSTDIR\Qt6Core.dll" StrCpy $R0 1 ${EndIf} ${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 DLL ; is enough to prove that something still holds a lock on the install ; directory. The purge works on wildcards, which the file existence checks do ; not, so this looks for files that are certainly part of the payload. Function HasStaleRuntimeFiles StrCpy $R0 0 ${If} ${FileExists} "$INSTDIR\Qt6Core.dll" StrCpy $R0 1 ${EndIf} ${If} ${FileExists} "$INSTDIR\Qt6Network.dll" StrCpy $R0 1 ${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. StrCpy $OldUninstallString $R0 Call EnsureOldInstallNotRunning ClearErrors ExecWait "$OldUninstallString" 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 ClearErrors ExecWait "$OldUninstallString" done64: ${EndIf} ${Else} Abort ${EndIf} FunctionEnd Section "Application" SecApplication SetShellVarContext all SetOutPath "$INSTDIR" ; 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 Qt DLLs next to the new executable ; ("The procedure entry point X could not be located in the dynamic link ; library ...Qt6Network.dll"). 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 RMDir /r "$INSTDIR\plugins" RMDir /r "$INSTDIR\sounds" RMDir /r "$INSTDIR\themes" RMDir /r "$INSTDIR\translations" Delete "$INSTDIR\uninstall.exe" Delete "$INSTDIR\cockatrice.exe" Delete "$INSTDIR\oracle.exe" Delete "$INSTDIR\servatrice.exe" Delete "$INSTDIR\Qt*.dll" Delete "$INSTDIR\libmysql.dll" Delete "$INSTDIR\icu*.dll" Delete "$INSTDIR\libcrypto*.dll" Delete "$INSTDIR\libssl*.dll" Delete "$INSTDIR\libeay32.dll" Delete "$INSTDIR\ssleay32.dll" Delete "$INSTDIR\qt.conf" Delete "$INSTDIR\qdebug.txt" Delete "$INSTDIR\servatrice.sql" Delete "$INSTDIR\servatrice.ini.example" Delete "$INSTDIR\zlib*.dll" RMDir "$INSTDIR" ${EndIf} ; Remove any runtime DLLs left over from older versions, so a mismatched ; Qt/OpenSSL set cannot survive next to the new executables. Call IsExistingCockatriceInstall ${If} $PortableMode = 0 ${AndIf} $R0 = 1 RMDir /r "$INSTDIR\Plugins" Delete "$INSTDIR\Qt*.dll" Delete "$INSTDIR\libcrypto*.dll" Delete "$INSTDIR\libssl*.dll" Delete "$INSTDIR\zlib*.dll" Delete "$INSTDIR\libmysql.dll" Delete "$INSTDIR\icu*.dll" Delete "$INSTDIR\libeay32.dll" Delete "$INSTDIR\ssleay32.dll" ${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 ; ...Qt6Network.dll", so say so instead of leaving it to the user to find out. Call HasStaleRuntimeFiles ${If} $R0 = 1 DetailPrint "Files of a previous version are still present; the installation may be incomplete." MessageBox MB_OK|MB_ICONEXCLAMATION \ "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.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice and run the installer again." ${EndIf} @CPACK_NSIS_EXTRA_PREINSTALL_COMMANDS@ @CPACK_NSIS_FULL_INSTALL@ ${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} 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 the directory (and the ; portable.dat marker inside it) is removed below. IfFileExists "$INSTDIR\portable.dat" 0 +2 StrCpy $R1 1 ; Remove the entire application directory so any file that is not part of ; the installed payload (e.g. build-tree artifacts such as *.dir folders, ; *_autogen and *.tlog files from a build) cannot survive between an ; uninstall and a fresh reinstall. RMDir /r "$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