From 933fb5d1f279abde6b2531822e5953160b9fc3ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Thu, 17 Sep 2026 20:29:47 +0200 Subject: [PATCH 1/2] [Windows] Close running instances and purge stale runtime DLLs during update --- cmake/NSIS.template.in | 164 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index b3cbcece8..198e91989 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -16,6 +16,7 @@ Var ReinstallMode !include LogicLib.nsh !include FileFunc.nsh !include MUI2.nsh +!include nsExec.nsh !include x64.nsh !define MUI_ABORTWARNING @@ -131,6 +132,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 +152,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 +210,140 @@ ${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. + +Function IsAppRunning + ; usage: set $R1 to the image name, call this, result in $R0 (1 = running, 0 = not running) + nsExec::Exec 'cmd /c tasklist /FI "IMAGENAME eq $R1" /FO CSV /NH | findstr /I "$R1" >nul' + Pop $R0 + ${If} $R0 = 0 + StrCpy $R0 1 + ${Else} + StrCpy $R0 0 + ${EndIf} +FunctionEnd + +Function WaitForAppToClose + ; usage: set $R1 to the image name + 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 ..." + nsExec::Exec 'cmd /c taskkill /IM $R1' + Pop $R2 + 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 ..." + nsExec::Exec 'cmd /c taskkill /F /IM $R1' + Pop $R2 + 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" + Call WaitForAppToClose + StrCpy $R1 "oracle.exe" + Call WaitForAppToClose + StrCpy $R1 "servatrice.exe" + 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::Exec 'cmd /c tasklist /FI "IMAGENAME eq $R1" /FO CSV /NH | findstr /I "$R1" >nul' + Pop $R0 + ${If} $R0 = 0 + StrCpy $R0 1 + ${Else} + StrCpy $R0 0 + ${EndIf} +FunctionEnd + +Function un.WaitForAppToClose + Call un.IsAppRunning + ${If} $R0 = 0 + Return + ${EndIf} + + ${If} ${Silent} + DetailPrint "Closing $R1 ..." + nsExec::Exec 'cmd /c taskkill /IM $R1' + Pop $R2 + 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 ..." + nsExec::Exec 'cmd /c taskkill /F /IM $R1' + Pop $R2 + 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" + Call un.WaitForAppToClose + StrCpy $R1 "oracle.exe" + Call un.WaitForAppToClose + StrCpy $R1 "servatrice.exe" + Call un.WaitForAppToClose +FunctionEnd + Function PortableModePageCreate ${If} $ReinstallMode = 1 @@ -318,6 +463,25 @@ ${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 + 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@ From 8a97d23e2053c76960e04e5fe479da3b8e44da1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sun, 20 Sep 2026 20:59:08 +0200 Subject: [PATCH 2/2] [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. --- cmake/NSIS.template.in | 56 ++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index 198e91989..f3f172693 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -16,7 +16,6 @@ Var ReinstallMode !include LogicLib.nsh !include FileFunc.nsh !include MUI2.nsh -!include nsExec.nsh !include x64.nsh !define MUI_ABORTWARNING @@ -217,10 +216,16 @@ FunctionEnd ; 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 - ; usage: set $R1 to the image name, call this, result in $R0 (1 = running, 0 = not running) - nsExec::Exec 'cmd /c tasklist /FI "IMAGENAME eq $R1" /FO CSV /NH | findstr /I "$R1" >nul' + 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 @@ -229,8 +234,20 @@ Function IsAppRunning ${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 image name + ; 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 @@ -239,8 +256,7 @@ Function WaitForAppToClose ${If} ${Silent} ; ask the application to close gracefully (WM_CLOSE), then wait for it to exit DetailPrint "Closing $R1 ..." - nsExec::Exec 'cmd /c taskkill /IM $R1' - Pop $R2 + Call CloseMatchingApps StrCpy $R8 0 ck_wait_loop: Sleep 500 @@ -255,8 +271,7 @@ Function WaitForAppToClose ${EndIf} ; give up waiting, force close DetailPrint "Force closing $R1 ..." - nsExec::Exec 'cmd /c taskkill /F /IM $R1' - Pop $R2 + Call ForceCloseMatchingApps Sleep 500 ${Else} ck_wait_prompt: @@ -275,17 +290,20 @@ 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::Exec 'cmd /c tasklist /FI "IMAGENAME eq $R1" /FO CSV /NH | findstr /I "$R1" >nul' + 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 @@ -294,6 +312,16 @@ Function un.IsAppRunning ${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 @@ -302,8 +330,7 @@ Function un.WaitForAppToClose ${If} ${Silent} DetailPrint "Closing $R1 ..." - nsExec::Exec 'cmd /c taskkill /IM $R1' - Pop $R2 + Call un.CloseMatchingApps StrCpy $R8 0 un_ck_wait_loop: Sleep 500 @@ -317,8 +344,7 @@ Function un.WaitForAppToClose Goto un_ck_wait_loop ${EndIf} DetailPrint "Force closing $R1 ..." - nsExec::Exec 'cmd /c taskkill /F /IM $R1' - Pop $R2 + Call un.ForceCloseMatchingApps Sleep 500 ${Else} un_ck_wait_prompt: @@ -337,10 +363,13 @@ 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 @@ -471,6 +500,7 @@ ${EndIf} Call EnsureAppsNotRunning ${If} $PortableMode = 0 +${AndIf} ${FileExists} "$INSTDIR\cockatrice.exe" RMDir /r "$INSTDIR\Plugins" Delete "$INSTDIR\Qt*.dll" Delete "$INSTDIR\libcrypto*.dll"