diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index b3cbcece8..f3f172693 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -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@