mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-28 00:42:19 -07:00
* [Windows] Close running instances and purge stale runtime DLLs during update
* [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.
* [Windows] Make process detection rely on an explicit PowerShell exit code
* [Windows] Fix exit-code detection in the uninstaller process check
* [Windows] Pass the install dir to PowerShell without quoting pitfalls
* [Windows] Fail closed when a process path is unreadable
* [Windows] Only pre-check running instances for /R updates
* [Windows] Wait on a wall-clock 60 s deadline instead of a loop counter
* [Windows] Verify the force close and abort if the process survived
* [Windows] Defer silent updates instead of force-killing the client
* [Windows] Purge OpenSSL 3 DLLs on portable upgrades and broaden the stale-DLL guard
* [Windows] Replace the GetTickCount macro with a direct System::Call
* [Windows] Only close running instances whose path is readable
CloseMatchingApps reused the fail-closed detection filter, which counts a
process whose Path cannot be read (Access denied) as a match. The installer
runs elevated, so CloseMainWindow() would then be delivered to an unrelated
oracle.exe / servatrice.exe belonging to some other program, silently closing
a third party application during a Cockatrice update.
The set of processes we act on now has to be the strict one: a readable path
under $INSTDIR. Unreadable paths are simply not ours.
* [Windows] Match running instances by a readable path under the install dir
The Path read in the Where-Object filter raises "Access is denied" for
processes the installer cannot open, and both branches of the try/catch
turned that into a match. Together with mapping every non-zero nsExec result
to "running" this made the guard a hard stop rather than a fail-closed one: a
machine with an Oracle database service (or a servatrice service) running as
SYSTEM always has a process named oracle / servatrice whose path cannot be
read, so every silent update aborted after 60 s and every interactive install
re-asked for Retry indefinitely.
A process whose path cannot be read is not one of ours - the updater is
launched by the application it updates and therefore runs in the same user
session, and an interactive install is elevated and can read the paths of the
user's own processes. Both the detection and the close path now require a
readable path under $INSTDIR, which is what the comment above the helpers
already claimed.
* [Windows] Distinguish "could not determine" from "definitely running"
Mapping every non-zero nsExec result to "running" folds nsExec's "error"
return (powershell.exe could not be started at all - removed, blocked by
policy, refused by antivirus) into "Cockatrice is running". That state can
never clear: closing applications does not help, so silent installs aborted
after the full 60 s wait and interactive installs could not get past the
Retry/Cancel prompt.
IsAppRunning now reports three states (0 not running, 1 running, 2 could not
determine) and a new IsProcessLookupAvailable preflight checks that
powershell.exe can run at all. A broken PowerShell is reported to the user
once - with a message box, since a silent install would otherwise show
nothing - and the install continues instead of being blocked forever. Only a
definite "running" is now waited for.
* [Windows] Force-stop the console applications after the grace period
Servatrice and Oracle are console applications, so CloseMainWindow()
returns false and does nothing for them. Every silent update therefore
waited the full 60 s and then aborted, and every interactive install looped
on the Retry prompt, for any user running Servatrice out of the install
directory.
They now get a second shutdown path: once the grace period is over they are
force-stopped by PID with the same strict install-dir filter, since they hold
no unsaved user data. Cockatrice keeps the graceful-only treatment - a
force-kill there could destroy an unsaved deck - and still aborts instead.
* [Windows] Tell the user when a silent install or uninstall is aborted
On the /R path the installer runs with SetSilent and SetAutoClose, so the
DetailPrint before Abort went nowhere: the user clicks Help -> Check for
Client Updates, the download finishes, Cockatrice closes its window, and
nothing else happens - no update and no error, because the client launches the
installer detached and never looks at its exit code.
Every silent abort path now shows a message box telling the user which
application is still open and that they should close it and run the
installer again. The same applies to the silent uninstall, where the only
output is a log file nobody reads.
* [Windows] Treat leftover runtime files as an install marker for the purge
The guard on the stale-DLL purge only looked for the three executables and
uninstall.exe, so it skipped precisely the case the purge exists for: a
previous failed update whose uninstaller removed what it could and silently
skipped the Qt DLLs that a running instance had locked. None of the four
markers is left in that directory, and the stale DLLs then survive into the
new install.
A leftover Qt6Core.dll (what we ship today) or the qt.conf written by a
portable install now count as an install marker as well.
* [Windows] Run the section instance check before any file is deleted
Call EnsureAppsNotRunning sat after the portable-upgrade block, which had
already deleted the old cockatrice.exe, Qt*.dll and plugins directory. An
interactive install ("Portable mode" pointing at an existing portable folder)
has $ReinstallMode = 0, so the .onInit guard is skipped and locked files were
silently skipped - exactly the mixed-Qt-DLL state this change is about.
The check now runs right after SetOutPath, before the first Delete/RMDir in
the section.
* [Windows] Guard the old install before the old uninstaller deletes files
The interactive upgrade path removes the previous installation long before the
install section: componentsPagePre runs the old UninstallString on the
components page, i.e. before the directory page and before any call of ours
can check anything. Every uninstaller shipped before this change has no
un.EnsureAppsNotRunning, so for exactly the versions affected by #1576 the old
files were deleted while cockatrice.exe was still running and the new
installer had no say in it.
componentsPagePre now runs the same check before both ExecWait calls. At that
point $INSTDIR is still only the default, not the directory the old install
lives in, so the new EnsureOldInstallNotRunning guards the location recorded
in the registry instead. The helpers now work on a $GuardDir the caller
picks; the normal entry points keep seeding it with $INSTDIR.
* [Windows] Do not abort the section check past the point of no return
"nothing has been replaced yet, so aborting just defers the update" only holds
for the guard in .onInit. On the /R path AutoUninstallIfNeeded has already run
the old uninstaller, which RMDir /r's the whole install directory, and the
purge below it deletes the runtime files - so an Abort from the section would
leave the user with the old version uninstalled and the new one not installed
at all, which is strictly worse than the bug this change fixes.
The ordering now guarantees the check already passed: .onInit guards the /R
path before the uninstall, so the section only repeats it where that is not
already true, i.e. for interactive installs, where nothing of this installer
has been deleted yet and an Abort is the correct outcome.
As a safety net for whatever the purge could not remove (a still locked file,
or a check that could not run at all), HasStaleRuntimeFiles reports surviving
runtime files with a message box - also on a silent update, where there is no
other way to let the user know the install came out incomplete.
* [Windows] Only block the in-app updater with a message box
The message box on the silent abort would also fire for a plain /S install,
which is exactly the unattended case: a package manager (or a CI job) running
the installer with a leftover Cockatrice process would now hang forever on a
dialog nobody can see instead of aborting.
The box is therefore limited to /R, the path the in-app updater takes, where
the user just clicked Help -> Check for Client Updates, the client launches
the installer detached and never inspects its exit code, and the window is
about to disappear - without a message, the update would silently not happen.
Every silent abort also sets an error level now, so an unattended caller can
see that the run failed instead of having to read the log.
The silent uninstaller reports through log and exit code for the same reason:
it is run unattended by the /R updater itself (QuietUninstallString) and by
package managers. The installer that invoked it tells the user about the
incomplete result itself.
* [Windows] Treat an unset guard directory as undeterminable
$GuardDir is set by the three entry points into the check, but it is now what
decides whether a process counts as one of ours, and an empty string would
match every process that merely shares an image name - the same false positive
the strict path filter exists to avoid. Report it as "could not determine"
instead of running the check, so a future caller that forgets to set it warns
the user rather than closing or killing a third party process.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
997 lines
40 KiB
Text
997 lines
40 KiB
Text
!include ..\..\..\NSIS.definitions.nsh
|
|
Name "@CPACK_PACKAGE_NAME@"
|
|
BrandingText "@CPACK_PACKAGE_FILE_NAME@"
|
|
OutFile "@CPACK_TOPLEVEL_DIRECTORY@/@CPACK_OUTPUT_FILE_NAME@"
|
|
|
|
!define INST_DIR "@CPACK_TEMPORARY_DIRECTORY@"
|
|
|
|
RequestExecutionlevel admin
|
|
SetCompressor LZMA
|
|
|
|
Var NormalDestDir
|
|
Var PortableDestDir
|
|
Var PortableMode
|
|
Var ReinstallMode
|
|
Var AllowForceClose
|
|
Var GuardDir
|
|
Var OldUninstallString
|
|
|
|
!include LogicLib.nsh
|
|
!include FileFunc.nsh
|
|
!include MUI2.nsh
|
|
!include x64.nsh
|
|
|
|
!define MUI_ABORTWARNING
|
|
!define MUI_WELCOMEFINISHPAGE_BITMAP "${NSIS_SOURCE_PATH}\cmake\leftimage.bmp"
|
|
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "${NSIS_SOURCE_PATH}\cmake\leftimage.bmp"
|
|
!define MUI_HEADERIMAGE
|
|
!define MUI_HEADERIMAGE_BITMAP "${NSIS_SOURCE_PATH}\cmake\headerimage.bmp"
|
|
!define MUI_HEADERIMAGE_UNBITMAP "${NSIS_SOURCE_PATH}\cmake\headerimage.bmp"
|
|
!define MUI_WELCOMEPAGE_TEXT "This wizard will guide you through the installation of Cockatrice.$\r$\n$\r$\nClick Next to continue."
|
|
!define MUI_FINISHPAGE_RUN "$INSTDIR/cockatrice.exe"
|
|
!define MUI_FINISHPAGE_RUN_TEXT "Run 'Cockatrice' now"
|
|
!define MUI_ICON "${NSIS_SOURCE_PATH}\cockatrice\resources\appicon.ico"
|
|
|
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
|
|
!insertmacro MUI_PAGE_WELCOME
|
|
|
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
|
|
!insertmacro MUI_PAGE_LICENSE "${NSIS_SOURCE_PATH}\LICENSE"
|
|
|
|
Page Custom PortableModePageCreate PortableModePageLeave
|
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE componentsPagePre
|
|
!insertmacro MUI_PAGE_COMPONENTS
|
|
|
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
|
|
!insertmacro MUI_PAGE_DIRECTORY
|
|
|
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
|
|
!insertmacro MUI_PAGE_INSTFILES
|
|
|
|
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfReinstall
|
|
!insertmacro MUI_PAGE_FINISH
|
|
|
|
!insertmacro MUI_UNPAGE_CONFIRM
|
|
!insertmacro MUI_UNPAGE_COMPONENTS
|
|
!insertmacro MUI_UNPAGE_INSTFILES
|
|
!insertmacro MUI_UNPAGE_FINISH
|
|
|
|
!insertmacro MUI_LANGUAGE English
|
|
|
|
Function .onInit
|
|
|
|
${If} ${NSIS_IS_64_BIT} == 1 #NSIS 64bit
|
|
${IfNot} ${RunningX64}
|
|
MessageBox MB_OK|MB_ICONSTOP "This version of Cockatrice requires a 64-bit Windows system."
|
|
Abort
|
|
${EndIf}
|
|
StrCpy $NormalDestDir "$ProgramFiles64\Cockatrice"
|
|
SetRegView 64
|
|
${Else} #NSIS 32bit
|
|
${If} ${RunningX64}
|
|
MessageBox MB_OK|MB_ICONEXCLAMATION \
|
|
"You are about to install a 32-bit version of Cockatrice on a 64-bit Windows system.$\n\
|
|
We advise you to use the correct 64-bit installer instead to get around potential issues.$\n$\n\
|
|
Download from our webpage: https://cockatrice.github.io"
|
|
${EndIf}
|
|
StrCpy $NormalDestDir "$ProgramFiles\Cockatrice"
|
|
${EndIf}
|
|
|
|
StrCpy $PortableDestDir "$Desktop\CockatricePortable"
|
|
|
|
${GetParameters} $9
|
|
|
|
ClearErrors
|
|
${GetOptions} $9 "/?" $8
|
|
${IfNot} ${Errors}
|
|
MessageBox MB_ICONINFORMATION|MB_SETFOREGROUND "\
|
|
/PORTABLE : Install in portable mode$\n\
|
|
/S : Silent install$\n\
|
|
/R : Silent upgrade$\n\
|
|
/D=%directory% : Specify destination directory$\n"
|
|
Quit
|
|
${EndIf}
|
|
|
|
ClearErrors
|
|
${GetOptions} $9 "/PORTABLE" $8
|
|
${IfNot} ${Errors}
|
|
StrCpy $PortableMode 1
|
|
StrCpy $0 $PortableDestDir
|
|
${Else}
|
|
StrCpy $PortableMode 0
|
|
StrCpy $0 $NormalDestDir
|
|
${If} ${Silent}
|
|
Call RequireAdmin
|
|
${EndIf}
|
|
${EndIf}
|
|
|
|
ClearErrors
|
|
${GetOptions} $9 "/R" $8
|
|
${IfNot} ${Errors}
|
|
StrCpy $ReinstallMode 1
|
|
SetSilent silent
|
|
SetAutoClose true
|
|
${Else}
|
|
StrCpy $ReinstallMode 0
|
|
${EndIf}
|
|
|
|
${If} $InstDir == ""
|
|
; User did not use /D to specify a directory,
|
|
; we need to set a default based on the install mode
|
|
StrCpy $InstDir $0
|
|
${EndIf}
|
|
|
|
; --- Detect portable install when using /R (must come BEFORE SetModeDestinationFromInstdir) ---
|
|
${If} $ReinstallMode = 1
|
|
IfFileExists "$InstDir\portable.dat" 0 not_portable
|
|
StrCpy $PortableMode 1
|
|
Goto portable_done
|
|
not_portable:
|
|
StrCpy $PortableMode 0
|
|
portable_done:
|
|
${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
|
|
${EndIf}
|
|
|
|
FunctionEnd
|
|
|
|
Function un.onInit
|
|
|
|
${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
|
|
UserInfo::GetAccountType
|
|
Pop $8
|
|
${If} $8 != "admin"
|
|
MessageBox MB_ICONSTOP "You need administrator rights to install Cockatrice"
|
|
SetErrorLevel 740 ;ERROR_ELEVATION_REQUIRED
|
|
Abort
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
|
|
Function SetModeDestinationFromInstdir
|
|
${If} $PortableMode = 0
|
|
StrCpy $NormalDestDir $InstDir
|
|
${Else}
|
|
StrCpy $PortableDestDir $InstDir
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
Function SkipIfReinstall
|
|
${If} $ReinstallMode = 1
|
|
Abort
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
Function AutoUninstallIfNeeded
|
|
|
|
SetShellVarContext all
|
|
|
|
; --- 32-bit uninstall ---
|
|
SetRegView 32
|
|
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
|
|
|
|
StrCmp $R0 "" done32
|
|
DetailPrint "Removing previous version (32-bit)..."
|
|
ExecWait '$R0'
|
|
|
|
done32:
|
|
|
|
; --- 64-bit uninstall ---
|
|
${If} ${RunningX64}
|
|
SetRegView 64
|
|
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
|
|
|
|
StrCmp $R0 "" done64
|
|
DetailPrint "Removing previous version (64-bit)..."
|
|
ExecWait '$R0'
|
|
|
|
done64:
|
|
${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
|
|
Abort
|
|
${EndIf}
|
|
|
|
Call SetModeDestinationFromInstdir ; If the user clicks BACK on the directory page we will remember their mode specific directory
|
|
!insertmacro MUI_HEADER_TEXT "Install Mode" "Choose how you want to install Cockatrice."
|
|
nsDialogs::Create 1018
|
|
Pop $0
|
|
${NSD_CreateLabel} 0 10u 100% 24u "Select install mode:"
|
|
Pop $0
|
|
${NSD_CreateRadioButton} 30u 50u -30u 8u "Normal installation"
|
|
Pop $1
|
|
${NSD_CreateRadioButton} 30u 70u -30u 8u "Portable mode (all files in a single folder)"
|
|
Pop $2
|
|
${If} $PortableMode = 0
|
|
SendMessage $1 ${BM_SETCHECK} ${BST_CHECKED} 0
|
|
${Else}
|
|
SendMessage $2 ${BM_SETCHECK} ${BST_CHECKED} 0
|
|
${EndIf}
|
|
nsDialogs::Show
|
|
FunctionEnd
|
|
|
|
Function PortableModePageLeave
|
|
${NSD_GetState} $1 $0
|
|
${If} $0 <> ${BST_UNCHECKED}
|
|
StrCpy $PortableMode 0
|
|
StrCpy $InstDir $NormalDestDir
|
|
Call RequireAdmin
|
|
${Else}
|
|
StrCpy $PortableMode 1
|
|
StrCpy $InstDir $PortableDestDir
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
Function componentsPagePre
|
|
|
|
${If} $ReinstallMode = 1
|
|
Return
|
|
${EndIf}
|
|
|
|
${If} $PortableMode = 0
|
|
SetShellVarContext all
|
|
|
|
# uninstall 32bit version
|
|
SetRegView 32
|
|
|
|
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "UninstallString"
|
|
StrCmp $R0 "" done32
|
|
|
|
${If} $ReinstallMode = 0
|
|
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst32
|
|
Abort
|
|
${Else}
|
|
Goto uninst32
|
|
${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"
|
|
|
|
done32:
|
|
|
|
# uninstall 64bit version
|
|
${If} ${NSIS_IS_64_BIT} == 1
|
|
SetRegView 64
|
|
|
|
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "UninstallString"
|
|
StrCmp $R0 "" done64
|
|
|
|
${If} $ReinstallMode = 0
|
|
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst64
|
|
Abort
|
|
${Else}
|
|
Goto uninst64
|
|
${EndIf}
|
|
|
|
uninst64:
|
|
StrCpy $OldUninstallString $R0
|
|
Call EnsureOldInstallNotRunning
|
|
ClearErrors
|
|
ExecWait "$OldUninstallString"
|
|
|
|
done64:
|
|
${EndIf}
|
|
|
|
${Else}
|
|
Abort
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
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
|
|
RMDir /r "$INSTDIR\plugins"
|
|
RMDir /r "$INSTDIR\sounds"
|
|
RMDir /r "$INSTDIR\themes"
|
|
RMDir /r "$INSTDIR\translations"
|
|
Delete "$INSTDIR\uninstall.exe"
|
|
Delete "$INSTDIR\cockatrice.exe"
|
|
Delete "$INSTDIR\oracle.exe"
|
|
Delete "$INSTDIR\servatrice.exe"
|
|
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"
|
|
Delete "$INSTDIR\qdebug.txt"
|
|
Delete "$INSTDIR\servatrice.sql"
|
|
Delete "$INSTDIR\servatrice.ini.example"
|
|
Delete "$INSTDIR\zlib*.dll"
|
|
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@
|
|
|
|
${If} $PortableMode = 0
|
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
|
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
|
IntFmt $0 "0x%08X" $0
|
|
|
|
; Enable Windows User-Mode Dumps
|
|
; https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
|
|
WriteRegExpandStr HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpFolder" "%LOCALAPPDATA%\CrashDumps\Cockatrice"
|
|
WriteRegDWORD HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpCount" "5"
|
|
WriteRegDWORD HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpType" "2"
|
|
|
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayIcon" "$INSTDIR\cockatrice.exe"
|
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayName" "Cockatrice"
|
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayVersion" "@CPACK_PACKAGE_VERSION_MAJOR@.@CPACK_PACKAGE_VERSION_MINOR@.@CPACK_PACKAGE_VERSION_PATCH@"
|
|
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "EstimatedSize" "$0"
|
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "InstallLocation" "$INSTDIR"
|
|
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "NoModify" "1"
|
|
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "NoRepair" "1"
|
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "Publisher" "Cockatrice team"
|
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
|
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "VersionMajor" "@CPACK_PACKAGE_VERSION_MAJOR@"
|
|
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "VersionMinor" "@CPACK_PACKAGE_VERSION_MINOR@"
|
|
|
|
; --- Register .cod file type ---
|
|
WriteRegStr HKCR ".cod" "" "Cockatrice"
|
|
WriteRegStr HKCR "Cockatrice" "" "Cockatrice Deck File"
|
|
WriteRegStr HKCR "Cockatrice\shell\open\command" "" '"$INSTDIR\cockatrice.exe" "%1"'
|
|
|
|
; --- Register custom URI protocol ---
|
|
WriteRegStr HKCR "cockatrice" "" "URL: Cockatrice Protocol"
|
|
WriteRegStr HKCR "cockatrice" "URL Protocol" ""
|
|
WriteRegStr HKCR "cockatrice\shell\open\command" "" '"$INSTDIR\cockatrice.exe" "%1"'
|
|
|
|
IfFileExists "$INSTDIR\vc_redist.x86.exe" VcRedist86Exists PastVcRedist86Check
|
|
VcRedist86Exists:
|
|
ExecWait '"$INSTDIR\vc_redist.x86.exe" /passive /norestart'
|
|
DetailPrint "Wait to ensure unlock of vc_redist file after installation..."
|
|
Sleep 3000
|
|
Delete "$INSTDIR\vc_redist.x86.exe"
|
|
PastVcRedist86Check:
|
|
|
|
IfFileExists "$INSTDIR\vc_redist.x64.exe" VcRedist64Exists PastVcRedist64Check
|
|
VcRedist64Exists:
|
|
ExecWait '"$INSTDIR\vc_redist.x64.exe" /passive /norestart'
|
|
DetailPrint "Sleep to ensure unlock of vc_redist file after installation..."
|
|
Sleep 3000
|
|
Delete "$INSTDIR\vc_redist.x64.exe"
|
|
PastVcRedist64Check:
|
|
|
|
${Else}
|
|
; Create the file the application uses to detect portable mode
|
|
FileOpen $0 "$INSTDIR\portable.dat" w
|
|
FileWrite $0 "PORTABLE"
|
|
FileClose $0
|
|
${EndIf}
|
|
|
|
${If} $ReinstallMode = 1
|
|
IfFileExists "$INSTDIR\cockatrice.exe" 0 +2
|
|
Exec '"$INSTDIR\cockatrice.exe"'
|
|
${EndIf}
|
|
|
|
SectionEnd
|
|
|
|
Section "Start menu item" SecStartMenu
|
|
${If} $PortableMode = 0
|
|
SetShellVarContext all
|
|
createDirectory "$SMPROGRAMS\Cockatrice"
|
|
createShortCut "$SMPROGRAMS\Cockatrice\Cockatrice.lnk" "$INSTDIR\cockatrice.exe"
|
|
createShortCut "$SMPROGRAMS\Cockatrice\Oracle.lnk" "$INSTDIR\oracle.exe"
|
|
createShortCut "$SMPROGRAMS\Cockatrice\Servatrice.lnk" "$INSTDIR\servatrice.exe"
|
|
${EndIf}
|
|
SectionEnd
|
|
|
|
Section "un.Application" UnSecApplication
|
|
SetShellVarContext all
|
|
StrCpy $R1 0
|
|
|
|
; Record whether this was a portable install before the directory (and the
|
|
; portable.dat marker inside it) is removed below.
|
|
IfFileExists "$INSTDIR\portable.dat" 0 +2
|
|
StrCpy $R1 1
|
|
|
|
; Remove the entire application directory so any file that is not part of
|
|
; the installed payload (e.g. build-tree artifacts such as *.dir folders,
|
|
; *_autogen and *.tlog files from a build) cannot survive between an
|
|
; uninstall and a fresh reinstall.
|
|
RMDir /r "$INSTDIR"
|
|
|
|
RMDir /r "$SMPROGRAMS\Cockatrice"
|
|
|
|
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice"
|
|
|
|
; Only remove the file/protocol associations if we registered them (i.e. the
|
|
; install was not portable) and .cod is still owned by Cockatrice, so we don't
|
|
; clobber a .cod association installed by another application.
|
|
${If} $R1 == 0
|
|
ReadRegStr $0 HKCR ".cod" ""
|
|
${If} $0 == "Cockatrice"
|
|
DeleteRegKey HKCR ".cod"
|
|
DeleteRegKey HKCR "Cockatrice"
|
|
DeleteRegKey HKCR "cockatrice"
|
|
${EndIf}
|
|
${EndIf}
|
|
SectionEnd
|
|
|
|
; unselected because it is /o
|
|
Section /o "un.Configurations, decks, cards, pics" UnSecConfiguration
|
|
SetShellVarContext current
|
|
DeleteRegKey HKCU "Software\Cockatrice"
|
|
|
|
RMDir /r "$LOCALAPPDATA\Cockatrice"
|
|
SectionEnd
|
|
|
|
LangString DESC_SecApplication ${LANG_ENGLISH} "Cockatrice program files"
|
|
LangString DESC_SecStartMenu ${LANG_ENGLISH} "Create start menu items for Cockatrice and Oracle."
|
|
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
|
|
!insertmacro MUI_DESCRIPTION_TEXT ${SecApplication} $(DESC_SecApplication)
|
|
!insertmacro MUI_DESCRIPTION_TEXT ${SecStartMenu} $(DESC_SecStartMenu)
|
|
!insertmacro MUI_FUNCTION_DESCRIPTION_END
|
|
|
|
LangString DESC_UnSecApplication ${LANG_ENGLISH} "Cockatrice program files and start menu items"
|
|
LangString DESC_UnSecConfiguration ${LANG_ENGLISH} "Configurations, decks, card database, pictures"
|
|
!insertmacro MUI_UNFUNCTION_DESCRIPTION_BEGIN
|
|
!insertmacro MUI_DESCRIPTION_TEXT ${UnSecApplication} $(DESC_UnSecApplication)
|
|
!insertmacro MUI_DESCRIPTION_TEXT ${UnSecConfiguration} $(DESC_UnSecConfiguration)
|
|
!insertmacro MUI_UNFUNCTION_DESCRIPTION_END
|