diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index c214a333e..dd45f0039 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -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@ diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp index 120e20f3e..b5710b208 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp @@ -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); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.h b/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.h index 9ac774b24..d635ac2a8 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.h @@ -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; }; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp index c84a46528..7cf58d3e0 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp @@ -5,25 +5,15 @@ #include "../client/network/update/client/release_channel.h" #include "../interface/window_main.h" -#include #include -#include -#include #include #include #include #include -#include #include #include #include -// 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(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(parent()), "close", Qt::QueuedConnection); + qCInfo(DlgUpdateLog) << "Opened downloaded update file successfully - closing Cockatrice"; close(); } else { setLabel(tr("Error")); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_update.h b/cockatrice/src/interface/widgets/dialogs/dlg_update.h index 2cef46e70..7bd0020d5 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_update.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_update.h @@ -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; diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp index 41ebb17a2..62769d0e3 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -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 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); diff --git a/cockatrice/src/interface/widgets/tabs/tab_replays.cpp b/cockatrice/src/interface/widgets/tabs/tab_replays.cpp index e26bea2a3..ac4b2cbe9 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_replays.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_replays.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -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; diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 77cc85130..ae967312d 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -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) { diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index 1c4dde300..fc7980d16 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -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; diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index a8d13675d..9c78c48f3 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -982,10 +982,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Comman return Response::RespInvalidData; } - if (cmd.deck_list().size() > static_cast(MAX_FILE_LENGTH)) { - return Response::RespInvalidData; - } - sqlInterface->checkSql(); QString deckStr = fileFromStdString(cmd.deck_list());