Compare commits

...

4 commits

Author SHA1 Message Date
Lukas Brübach
97c0de876d
[Client] Only quit when the main window close is accepted
Wait for MainWindow::close() to be accepted before quitting after the
update installer is launched. When the close is vetoed (a running card DB
update, open games, or an unsaved deck), keep running and tell the user
the installer is already waiting, instead of exiting over their answer.

Also fix the comment so it does not claim settings are saved on the vetoed
path.
2026-09-20 21:08:16 +02:00
Lukas Brübach
de47ddfcae
[Client] Exit deterministically when launching the update installer 2026-09-20 21:08:16 +02:00
Lukas Brübach
1b7f9c0de4
[Windows] Scope running-instance handling to install dir and fix NSIS build
- Drop !include nsExec.nsh: nsExec is a plugin DLL, not a header, so
  makensis aborts before NSIS can build the installer.
- Match processes by image name and executable path under $INSTDIR via
  PowerShell, then close (WM_CLOSE) and force-stop only those PIDs, so an
  unrelated oracle.exe (Oracle DB) is never killed on a silent /R update.
- Gate the stale-runtime-DLL purge on $INSTDIR\cockatrice.exe existing, so
  a first-time install can't recursively delete an unrelated Plugins
  directory.
2026-09-20 21:08:16 +02:00
Lukas Brübach
6d06ae2bcf
[Windows] Close running instances and purge stale runtime DLLs during update 2026-09-20 21:08:16 +02:00
2 changed files with 212 additions and 1 deletions

View file

@ -131,6 +131,13 @@ ${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 or new files are installed.
; On a silent update (/R /S) running processes are asked to close gracefully
; and waited for, then force-closed only on timeout. On interactive installs
; the user is prompted to close them instead.
Call EnsureAppsNotRunning
${If} $ReinstallMode = 1
${AndIf} $PortableMode = 0
Call AutoUninstallIfNeeded
@ -144,6 +151,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 +209,170 @@ ${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.
;
; Processes are matched by image name AND by their executable path living
; under $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 $INSTDIR, 0 = not running)
Function IsAppRunning
nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { $$_.Path -like $\'$INSTDIR\*$\' } | Select-Object -First 1"'
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)
nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { $$_.Path -like $\'$INSTDIR\*$\' } | ForEach-Object { $$null = $$_.CloseMainWindow() }"'
Pop $R3
FunctionEnd
Function ForceCloseMatchingApps
nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { $$_.Path -like $\'$INSTDIR\*$\' } | Stop-Process -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
${If} $R0 = 0
Return
${EndIf}
${If} ${Silent}
; ask the application to close gracefully (WM_CLOSE), then wait for it to exit
DetailPrint "Closing $R1 ..."
Call CloseMatchingApps
StrCpy $R8 0
ck_wait_loop:
Sleep 500
IntOp $R8 $R8 + 1
Call IsAppRunning
${If} $R0 = 0
DetailPrint "$R1 closed."
Return
${EndIf}
${If} $R8 < 60
Goto ck_wait_loop
${EndIf}
; give up waiting, force close
DetailPrint "Force closing $R1 ..."
Call ForceCloseMatchingApps
Sleep 500
${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}
Goto ck_wait_prompt
ck_abort_install:
Abort
${EndIf}
FunctionEnd
Function EnsureAppsNotRunning
StrCpy $R1 "cockatrice.exe"
StrCpy $R2 "cockatrice"
Call WaitForAppToClose
StrCpy $R1 "oracle.exe"
StrCpy $R2 "oracle"
Call WaitForAppToClose
StrCpy $R1 "servatrice.exe"
StrCpy $R2 "servatrice"
Call WaitForAppToClose
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
nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { $$_.Path -like $\'$INSTDIR\*$\' } | Select-Object -First 1"'
Pop $R0
${If} $R0 = 0
StrCpy $R0 1
${Else}
StrCpy $R0 0
${EndIf}
FunctionEnd
Function un.CloseMatchingApps
nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { $$_.Path -like $\'$INSTDIR\*$\' } | ForEach-Object { $$null = $$_.CloseMainWindow() }"'
Pop $R3
FunctionEnd
Function un.ForceCloseMatchingApps
nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { $$_.Path -like $\'$INSTDIR\*$\' } | Stop-Process -Force -ErrorAction SilentlyContinue"'
Pop $R3
FunctionEnd
Function un.WaitForAppToClose
Call un.IsAppRunning
${If} $R0 = 0
Return
${EndIf}
${If} ${Silent}
DetailPrint "Closing $R1 ..."
Call un.CloseMatchingApps
StrCpy $R8 0
un_ck_wait_loop:
Sleep 500
IntOp $R8 $R8 + 1
Call un.IsAppRunning
${If} $R0 = 0
DetailPrint "$R1 closed."
Return
${EndIf}
${If} $R8 < 60
Goto un_ck_wait_loop
${EndIf}
DetailPrint "Force closing $R1 ..."
Call un.ForceCloseMatchingApps
Sleep 500
${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}
Goto un_ck_wait_prompt
un_ck_abort_install:
Abort
${EndIf}
FunctionEnd
Function un.EnsureAppsNotRunning
StrCpy $R1 "cockatrice.exe"
StrCpy $R2 "cockatrice"
Call un.WaitForAppToClose
StrCpy $R1 "oracle.exe"
StrCpy $R2 "oracle"
Call un.WaitForAppToClose
StrCpy $R1 "servatrice.exe"
StrCpy $R2 "servatrice"
Call un.WaitForAppToClose
FunctionEnd
Function PortableModePageCreate
${If} $ReinstallMode = 1
@ -318,6 +492,26 @@ ${AndIf} ${FileExists} "$INSTDIR\portable.dat"
RMDir "$INSTDIR"
${EndIf}
; Belt and braces: the old uninstaller may have already run in the /R path, so
; ensure no application instance is still holding file locks, then remove any
; runtime DLLs left over from older versions. A mismatched Qt/OpenSSL set next
; to the new executable is what causes "The procedure entry point X could not be
; located in the dynamic link library ...Qt6Network.dll" after an update.
Call EnsureAppsNotRunning
${If} $PortableMode = 0
${AndIf} ${FileExists} "$INSTDIR\cockatrice.exe"
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}
@CPACK_NSIS_EXTRA_PREINSTALL_COMMANDS@
@CPACK_NSIS_FULL_INSTALL@

View file

@ -5,11 +5,13 @@
#include "../client/network/update/client/release_channel.h"
#include "../interface/window_main.h"
#include <QCoreApplication>
#include <QDesktopServices>
#include <QLabel>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QTimer>
#include <QVBoxLayout>
#include <QtNetwork>
#include <version_string.h>
@ -240,8 +242,23 @@ void DlgUpdate::downloadSuccessful(const QUrl &filepath)
// Try to open the installer. If it opens, quit Cockatrice
if (process.startDetached()) {
QMetaObject::invokeMethod(static_cast<MainWindow *>(parent()), "close", Qt::QueuedConnection);
qCInfo(DlgUpdateLog) << "Opened downloaded update file 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. In that case keep running so the user
// can resolve the blocker, and tell them the installer is already waiting.
if (auto *window = qobject_cast<MainWindow *>(parent())) {
if (window->close()) {
QTimer::singleShot(0, qApp, &QCoreApplication::quit);
} else {
QMessageBox::warning(this, tr("Update"),
tr("The update installer is already running and will finish the update once "
"Cockatrice closes. Cockatrice is still busy, so it stays open for now."));
}
} else {
QTimer::singleShot(0, qApp, &QCoreApplication::quit);
}
close();
} else {
setLabel(tr("Error"));