Merge branch 'master' into tooomm-qt5

This commit is contained in:
tooomm 2026-10-04 12:27:58 +02:00 • committed by GitHub
commit 94b85b8940
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
266 changed files with 11392 additions and 930 deletions

View file

@ -12,6 +12,15 @@ Var NormalDestDir
Var PortableDestDir
Var PortableMode
Var ReinstallMode
Var AllowForceClose
Var GuardDir
Var OldUninstallString
Var StaleFindHandle
Var StaleFindName
Var IsUpgrade
Var AbortReason
Var WaitPid
Var LauncherExited
!include LogicLib.nsh
!include FileFunc.nsh
@ -85,6 +94,7 @@ ${IfNot} ${Errors}
/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}
@ -112,6 +122,23 @@ ${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
@ -131,6 +158,27 @@ ${EndIf}
; Now that $PortableMode reflects reality, commit InstDir into the correct slot
Call SetModeDestinationFromInstdir
; 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
Call WaitForLauncherToExit
Call EnsureAppsNotRunning
${EndIf}
${If} $ReinstallMode = 1
${AndIf} $PortableMode = 0
Call AutoUninstallIfNeeded
@ -144,6 +192,9 @@ ${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
@ -199,6 +250,616 @@ ${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 - 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
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
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.
DetailPrint "Force-stopping $R1 ..."
Call ForceCloseApps
Sleep 2000
Call IsAppRunning
${If} $R0 = 0
DetailPrint "$R1 closed."
Return
${EndIf}
${If} $R0 = 2
Call HandleUnknownAppState
Return
${EndIf}
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
${IfNot} ${Silent}
MessageBox MB_OK|MB_ICONEXCLAMATION "$AbortReason"
${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
${If} $WaitPid == ""
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.
ClearErrors
System::Call 'kernel32::OpenProcess(i 0x00100000, i 0, i $WaitPid) p.r0'
${If} ${Errors}
; The launcher is gone - OpenProcess cannot open a dead process - which is
; exactly the state the wait was for.
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
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}
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
${If} $R0 = 0
; Without PowerShell we cannot tell whether a running instance is
; holding locks on the files we are about to replace.
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.
${IfNot} ${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.
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.
;
; The masks below have to cover the purge above one for one. A check that only
; probed named files could not see a locked Qt6Multimedia.dll or a Plugins
; directory the purge was unable to remove, so the exact files that broke the
; install were also the ones this check was blind to.
Function HasStaleRuntimeFiles
StrCpy $R0 0
StrCpy $R3 "Qt*.dll"
Call CheckStaleRuntimeMask
StrCpy $R3 "libcrypto*.dll"
Call CheckStaleRuntimeMask
StrCpy $R3 "libssl*.dll"
Call CheckStaleRuntimeMask
StrCpy $R3 "zlib*.dll"
Call CheckStaleRuntimeMask
StrCpy $R3 "icu*.dll"
Call CheckStaleRuntimeMask
StrCpy $R3 "libeay32.dll"
Call CheckStaleRuntimeMask
StrCpy $R3 "ssleay32.dll"
Call CheckStaleRuntimeMask
${If} $R0 = 1
Return
${EndIf}
${If} ${FileExists} "$INSTDIR\libmysql.dll"
StrCpy $R0 1
Return
${EndIf}
; IfFileExists takes wildcards, so a Plugins directory the purge could not
; remove counts as a leftover too.
${If} ${FileExists} "$INSTDIR\Plugins\*.*"
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 != ""
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
@ -258,8 +919,15 @@ ${If} $PortableMode = 0
${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 "$R0"
ExecWait "$OldUninstallString"
done32:
@ -278,8 +946,10 @@ ${If} $PortableMode = 0
${EndIf}
uninst64:
StrCpy $OldUninstallString $R0
Call EnsureOldInstallNotRunning
ClearErrors
ExecWait "$R0"
ExecWait "$OldUninstallString"
done64:
${EndIf}
@ -294,6 +964,26 @@ 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
@ -308,6 +998,8 @@ ${AndIf} ${FileExists} "$INSTDIR\portable.dat"
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"
@ -318,6 +1010,57 @@ ${AndIf} ${FileExists} "$INSTDIR\portable.dat"
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
; 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
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
; ...Qt6QuickWidgets.dll" or "The code execution cannot proceed because
; Qt6Multimedia.dll was not found", 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
${Else}
StrCpy $R0 0
${EndIf}
${If} $R0 = 1
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.
${IfNot} ${Silent}
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, and was cancelled rather than leave a mismatched install behind.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice and run the installer again."
${EndIf}
SetErrorLevel 1
Abort
${EndIf}
@CPACK_NSIS_EXTRA_PREINSTALL_COMMANDS@
@CPACK_NSIS_FULL_INSTALL@
@ -345,6 +1088,16 @@ ${If} $PortableMode = 0
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'
@ -387,21 +1140,34 @@ SectionEnd
Section "un.Application" UnSecApplication
SetShellVarContext all
RMDir /r "$INSTDIR\plugins"
RMDir /r "$INSTDIR\sounds"
RMDir /r "$INSTDIR\themes"
RMDir /r "$INSTDIR\translations"
Delete "$INSTDIR\*.exe"
Delete "$INSTDIR\*.dll"
Delete "$INSTDIR\qt.conf"
Delete "$INSTDIR\qdebug.txt"
Delete "$INSTDIR\servatrice.sql"
Delete "$INSTDIR\servatrice.ini.example"
RMDir "$INSTDIR"
StrCpy $R1 0
RMDir "$SMPROGRAMS\Cockatrice"
; 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