Compare commits

..

No commits in common. "1365f3cb492c788e1ae3a3927e9fb00dcf7f9033" and "7c9dc303f8aa49dcedd83e480744168bd7b656bc" have entirely different histories.

10 changed files with 6 additions and 674 deletions

View file

@ -12,9 +12,6 @@ Var NormalDestDir
Var PortableDestDir
Var PortableMode
Var ReinstallMode
Var AllowForceClose
Var GuardDir
Var OldUninstallString
!include LogicLib.nsh
!include FileFunc.nsh
@ -134,20 +131,6 @@ ${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
@ -161,9 +144,6 @@ ${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
@ -219,473 +199,6 @@ ${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
@ -745,15 +258,8 @@ ${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 "$OldUninstallString"
ExecWait "$R0"
done32:
@ -772,10 +278,8 @@ ${If} $PortableMode = 0
${EndIf}
uninst64:
StrCpy $OldUninstallString $R0
Call EnsureOldInstallNotRunning
ClearErrors
ExecWait "$OldUninstallString"
ExecWait "$R0"
done64:
${EndIf}
@ -790,26 +294,6 @@ 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
@ -824,8 +308,6 @@ ${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"
@ -836,35 +318,6 @@ ${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
${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@

View file

@ -19,9 +19,6 @@ DlgEditAvatar::DlgEditAvatar(QWidget *parent) : QDialog(parent), image()
textLabel = new QLabel(tr("To change your avatar, choose a new image.\nTo remove your current avatar, confirm "
"without choosing a new image."));
hintLabel = new QLabel(
tr("Images are automatically downscaled to at most %1x%1 pixels.").arg(QString::number(MAX_AVATAR_DIMENSION)));
browseButton = new QPushButton(tr("Browse..."));
connect(browseButton, &QPushButton::clicked, this, &DlgEditAvatar::actBrowse);
@ -29,7 +26,6 @@ DlgEditAvatar::DlgEditAvatar(QWidget *parent) : QDialog(parent), image()
grid->addWidget(imageLabel, 0, 0, 1, 2);
grid->addWidget(textLabel, 1, 0);
grid->addWidget(browseButton, 1, 1);
grid->addWidget(hintLabel, 2, 0, 1, 2);
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgEditAvatar::actOk);
@ -76,10 +72,6 @@ QByteArray DlgEditAvatar::getImage()
return QByteArray();
}
if (image.width() > MAX_AVATAR_DIMENSION || image.height() > MAX_AVATAR_DIMENSION) {
image = image.scaled(MAX_AVATAR_DIMENSION, MAX_AVATAR_DIMENSION, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
for (;;) {
QByteArray ba;
QBuffer buffer(&ba);

View file

@ -29,9 +29,8 @@ private slots:
void actBrowse();
private:
static constexpr int MAX_AVATAR_DIMENSION = 1024;
QImage image;
QLabel *textLabel, *imageLabel, *hintLabel;
QLabel *textLabel, *imageLabel;
QPushButton *browseButton;
};

View file

@ -5,25 +5,15 @@
#include "../client/network/update/client/release_channel.h"
#include "../interface/window_main.h"
#include <QCoreApplication>
#include <QDesktopServices>
#include <QDir>
#include <QFileInfo>
#include <QLabel>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QTimer>
#include <QVBoxLayout>
#include <QtNetwork>
#include <version_string.h>
// Executable that, when it sits next to the downloaded update installer, is installed instead of
// it. A packager shipping a custom build - or someone testing one - can drop the file there and
// have Cockatrice run it rather than the official installer, without the release channel having to
// host an installer for that build. It is a drop-in and is run with the same arguments.
static const QString UPDATE_INSTALLER_OVERRIDE = "Cockatrice-Update-Override.exe";
DlgUpdate::DlgUpdate(QWidget *parent) : QDialog(parent)
{
@ -213,24 +203,6 @@ void DlgUpdate::setLabel(const QString &newText)
statusLabel->setText(newText);
}
void DlgUpdate::warnInstallerIsWaiting()
{
// Modeless on purpose: the installer asks the application to close exactly once (see
// CloseMatchingApps in NSIS.template.in) and only polls afterwards, while Qt drops spontaneous
// close events - a WM_CLOSE from the installer - for windows that are blocked by a modal
// widget (QGuiApplicationPrivate::processCloseEvent). An application modal message box here
// would therefore swallow the installer's one and only request for the rest of its wait.
auto *notification = new QMessageBox(QMessageBox::Warning, tr("Update"),
tr("The update installer is already running and waits about a "
"minute for Cockatrice to close. Cockatrice is still busy, so "
"save your work and close it before then. Otherwise the "
"installer gives up and the update is cancelled."),
QMessageBox::Ok, parentWidget());
notification->setWindowModality(Qt::NonModal);
notification->setAttribute(Qt::WA_DeleteOnClose);
notification->show();
}
void DlgUpdate::updateCheckError(const QString &errorString)
{
setLabel(tr("Error"));
@ -252,12 +224,6 @@ void DlgUpdate::downloadSuccessful(const QUrl &filepath)
QString installerPath = filepath.toLocalFile();
const QString overridePath = QDir(QFileInfo(installerPath).absolutePath()).filePath(UPDATE_INSTALLER_OVERRIDE);
if (QFileInfo::exists(overridePath)) {
qCInfo(DlgUpdateLog) << "Installing the update installer override instead of the download:" << overridePath;
installerPath = overridePath;
}
QString appDir = QDir::toNativeSeparators(QCoreApplication::applicationDirPath());
QProcess process;
process.setProgram(installerPath);
@ -274,31 +240,8 @@ void DlgUpdate::downloadSuccessful(const QUrl &filepath)
// Try to open the installer. If it opens, quit Cockatrice
if (process.startDetached()) {
qCInfo(DlgUpdateLog) << "Opened update installer successfully - closing Cockatrice";
// Close the main window synchronously so file locks are released before the NSIS installer
// (already launched) starts replacing files. This also flushes settings and shuts down the
// tabs, but only when the close is actually accepted: MainWindow may veto it for a running
// card DB update, an open game, or an unsaved deck, and closeForUpdate() also reports a
// close already in progress (reached from a nested event loop while a shutdown prompt is
// up). Only quit when the shutdown really ran - otherwise keep running so the user can
// resolve the blocker, and warn them that the installer only waits about a minute
// before it gives up and cancels the update.
if (auto *window = qobject_cast<MainWindow *>(parent())) {
if (window->closeForUpdate()) {
QTimer::singleShot(0, qApp, [] { QCoreApplication::exit(0); });
} else {
warnInstallerIsWaiting();
}
} else {
// Not a MainWindow, so no faithful close can be requested - but the parent still gets
// its close() call, which is what the code before this change did and lets it flush
// settings and shut down its tabs. Leaving is then unconditional: the installer is
// already running against a process that still holds locks on the files it replaces.
if (auto *widget = parentWidget()) {
widget->close();
}
QTimer::singleShot(0, qApp, [] { QCoreApplication::exit(0); });
}
QMetaObject::invokeMethod(static_cast<MainWindow *>(parent()), "close", Qt::QueuedConnection);
qCInfo(DlgUpdateLog) << "Opened downloaded update file successfully - closing Cockatrice";
close();
} else {
setLabel(tr("Error"));

View file

@ -42,7 +42,6 @@ private:
void addStopDownloadAndRemoveOthers(bool enable);
void beginUpdateCheck();
void setLabel(const QString &text);
void warnInstallerIsWaiting();
QLabel *statusLabel, *descriptionLabel;
QProgressBar *progress;
QPushButton *manualDownload, *gotoDownload, *ok, *stopDownload;

View file

@ -14,7 +14,6 @@
#include <QDateTime>
#include <QDebug>
#include <QDesktopServices>
#include <QDir>
#include <QFileSystemModel>
#include <QGroupBox>
#include <QHBoxLayout>
@ -50,12 +49,6 @@ namespace
// How long to wait after the last visibility change before reading back the
// Public/Private column, in milliseconds.
constexpr int VISIBILITY_REFRESH_DELAY = 500;
// Whether the file's name matches one of the deck formats Cockatrice can load.
bool isSupportedDeckFile(const QString &filePath)
{
return QDir::match(DeckLoader::ACCEPTED_FILE_EXTENSIONS, QFileInfo(filePath).fileName());
}
} // namespace
TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
@ -65,8 +58,6 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
{
localDirModel = new QFileSystemModel(this);
localDirModel->setRootPath(SettingsCache::instance().paths().getDeckPath());
localDirModel->setNameFilters(DeckLoader::ACCEPTED_FILE_EXTENSIONS);
localDirModel->setNameFilterDisables(false);
localDirModel->sort(0, Qt::AscendingOrder);
localDirView = new QTreeView;
@ -321,10 +312,6 @@ void TabDeckStorage::actOpenLocalDeck()
}
QString filePath = localDirModel->filePath(curLeft);
if (!isSupportedDeckFile(filePath)) {
continue;
}
std::optional<LoadedDeck> deckOpt = DeckLoader::loadFromFile(filePath, DeckFileFormat::Cockatrice, true);
if (!deckOpt) {
continue;
@ -389,11 +376,6 @@ void TabDeckStorage::actUpload()
void TabDeckStorage::uploadDeck(const QString &filePath, const QString &targetPath)
{
if (!isSupportedDeckFile(filePath)) {
QMessageBox::critical(this, tr("Error"), tr("Invalid deck file"));
return;
}
QFile deckFile(filePath);
QFileInfo deckFileInfo(deckFile);

View file

@ -9,7 +9,6 @@
#include <QApplication>
#include <QClipboard>
#include <QDesktopServices>
#include <QDir>
#include <QFileSystemModel>
#include <QGroupBox>
#include <QHBoxLayout>
@ -35,12 +34,6 @@
inline Q_LOGGING_CATEGORY(TabReplaysLog, "replays_tab");
namespace
{
// File name filters for the local replay files Cockatrice can load.
const QStringList REPLAY_FILE_NAME_FILTERS = {"*.cor"};
} // namespace
TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User *currentUserInfo)
: Tab(_tabSupervisor), client(_client)
{
@ -69,8 +62,6 @@ QGroupBox *TabReplays::createLeftLayout()
{
localDirModel = new QFileSystemModel(this);
localDirModel->setRootPath(SettingsCache::instance().paths().getReplaysPath());
localDirModel->setNameFilters(REPLAY_FILE_NAME_FILTERS);
localDirModel->setNameFilterDisables(false);
localDirModel->sort(0, Qt::AscendingOrder);
localDirView = new QTreeView;
@ -272,10 +263,6 @@ void TabReplays::actOpenLocalReplay()
}
QString filePath = localDirModel->filePath(curLeft);
if (!QDir::match(REPLAY_FILE_NAME_FILTERS, QFileInfo(filePath).fileName())) {
continue;
}
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly)) {
continue;

View file

@ -871,6 +871,7 @@ void MainWindow::actShow()
void MainWindow::closeEvent(QCloseEvent *event)
{
// workaround Qt bug where closeEvent gets called twice
static bool bClosingDown = false;
if (bClosingDown) {
return;
}
@ -899,18 +900,6 @@ void MainWindow::closeEvent(QCloseEvent *event)
tabSupervisor->deleteLater();
}
bool MainWindow::closeForUpdate()
{
// A shutdown is already being handled (e.g. this is reached from a nested event loop while
// closeEvent() is blocked on a user prompt). close() would hit the re-entrancy guard and return
// true without any shutdown having happened, so report faithfully instead: the caller must keep
// the process alive until the user resolves whatever is blocking the close.
if (bClosingDown) {
return false;
}
return close();
}
void MainWindow::changeEvent(QEvent *event)
{
if (event->type() == QEvent::LanguageChange) {

View file

@ -167,7 +167,6 @@ private:
LagMonitor lagMonitor; ///< watches the main thread for event loop stalls
LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph
bool bHasActivated, askedForDbUpdater;
bool bClosingDown = false; ///< guards closeEvent() against re-entrancy
bool skipStartupAutoConnect = false;
bool startupAutoConnectAttempted = false;
bool firstRunWizardActive = false;
@ -206,13 +205,6 @@ public:
return tabSupervisor;
}
/**
* @brief Closes the window so an update installer can replace the running binaries.
* Returns true only if the shutdown actually ran (settings flushed, tabs shut down);
* false if the close was vetoed by the user or is already in progress.
*/
bool closeForUpdate();
protected:
void closeEvent(QCloseEvent *event) override;
void changeEvent(QEvent *event) override;

View file

@ -982,10 +982,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Comman
return Response::RespInvalidData;
}
if (cmd.deck_list().size() > static_cast<std::string::size_type>(MAX_FILE_LENGTH)) {
return Response::RespInvalidData;
}
sqlInterface->checkSql();
QString deckStr = fileFromStdString(cmd.deck_list());