mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
* [Installer] Abort instead of installing over leftover runtime files
The silent upgrade could leave a mix of old and new Qt DLLs next to the new
executables, which is what makes the next start fail with "The procedure entry
point X could not be located in the dynamic link library ...Qt6QuickWidgets.dll"
or "The code execution cannot proceed because Qt6Multimedia.dll was not found".
The check that was supposed to catch this probed only Qt6Core.dll and
Qt6Network.dll, so it was blind to the two files the reports name. It also only
warned and then installed anyway, so a caught problem was still compounded.
- Scan every file the purge targets with FindFirst, which takes the same
wildcards the Delete/RMDir calls use, instead of two hardcoded names
- Widen IsExistingCockatriceInstall from Qt6Core.dll to Qt6*.dll so a directory
left holding only some Qt modules is still recognised as an install
- Gate the scan on that, so a first-time install into a directory that happens to
hold unrelated Qt5 DLLs is not turned into an abort
- Abort with a non-zero exit code instead of warning, so the new payload is
never written next to files of the previous version
- Skip the message box on an unattended /S install, which would block on it
forever, matching the two existing abort sites
* [Installer] Cancel a silent upgrade when the running-instance guard fails
The running instance guard is what stands between a locked install directory
and the mixed Qt DLL set that breaks the next start, but it failed open in two
places and let the install carry on anyway:
- powershell.exe could not be started: the check warned and returned
- the process lookup answered "could not determine": WaitForAppToClose returned
on $R0 != 1, so "unknown" was treated exactly like "not running"
On a silent upgrade there is nobody at the keyboard to close the applications in
response to that warning, which is precisely the state in which the launcher was
still holding its Qt DLLs mapped.
- Treat "could not determine" as its own state rather than folding it into "not
running", in all three places WaitForAppToClose inspects the result
- Cancel a silent upgrade in both cases; keep an interactive install on the
existing warn-and-continue path, since refusing there would leave an installer
that can never complete and the stale-runtime check still applies
- Route every abort through one AbortInstall so the dialog-vs-silent decision
lives in a single place, and stop the two pre-existing warning dialogs that a
/S install without /R would have blocked on forever
* [Installer] Wait on the process that launched the update installer
The in-app updater starts the installer and only then quits itself, so the
installer has to decide whether the launching process is still alive while it is
running. It did that by polling powershell.exe for an image name under the
install directory, which is a proxy for the real question - is the process that
started me still holding its Qt DLLs mapped - and a proxy that can answer "not
running" or "could not determine" while the process is very much alive. Either
answer let the install carry on into the mixed Qt DLL set.
Take /PID= from the command line and wait on that process handle directly:
- OpenProcess with PROCESS_SYNCHRONIZE only, which is all that is needed to wait
and the least likely to be refused for an unrelated reason
- WaitForSingleObject for the same 60 s the name based check allows, so a client
slow to flush its card database is not squeezed
- Treat an unopenable handle as "already exited", which is what it means for a
dead process
- Cancel the upgrade if the launcher has not exited, rather than replacing files
it is still using
Run it before EnsureAppsNotRunning, not after: it needs no powershell.exe, so it
is the one check that still works on a machine where the process lookup cannot
run, and it is what lets that case keep upgrading instead of being cancelled by
what was only ever a proxy for it. Keep an older client, which passes no /PID=,
on the existing path.
An older installer ignores /PID=, and /D= stays the last parameter either way.
* [Client] Pass our process id to the update installer
The updater starts the installer and only then quits itself, so the installer
begins replacing files while this process still has every Qt runtime DLL mapped,
and a mapped DLL cannot be deleted. It has to work out that it must wait, and
until now all it had was its own image name to go on.
Pass /PID= so it can wait on this process directly instead of guessing, and
cancel the update rather than replace files this process is still using. /D=
stays the last parameter, which NSIS requires.
* [CI] Fail the build when the installer is missing a Qt runtime DLL
The Windows payload is assembled by globbing every *.dll out of the build output
directories, so nothing but luck decides whether a module the client links
statically - Qt6Multimedia.dll, Qt6QuickWidgets.dll - reaches the installer at
all. A module that is left out is invisible until a user installs an update and
the client then refuses to start, with "The code execution cannot proceed because
Qt6Multimedia.dll was not found" or a missing procedure entry point in a
Qt6QuickWidgets.dll carried over from the previous version.
Derive the required list from SERVATRICE_QT_MODULES, COCKATRICE_QT_MODULES and
ORACLE_QT_MODULES, the same module lists the targets are actually built against,
so it cannot drift from what the binaries need, and write it next to the CMake
cache as required-qt-runtime.txt. Add the platform plugin, without which the
client cannot start at all.
Assert it in the existing 7-Zip pass that already rejects build-tree artifacts,
comparing exact relative paths so a same-named file in the wrong directory does
not satisfy a requirement, and normalizing the archive's own separators so it
does not matter which one NSIS chose. Missing manifest, missing entry or missing
7-Zip-independent failures all stop the build rather than warn, which is the
whole point: the previous check already failed hard for artifacts, and a warning
here is what let a broken package ship.
* [Client] Point the Windows qt.conf translations path at the real one
The Windows qt.conf carried Translations = Resources/translations, copied from
the macOS bundle block, but the Windows install has no Resources directory: the
.qm files are installed to COCKATRICE_WIN32_QM_INSTALL_DIR, which is
translations, and main.cpp looks for them at applicationDirPath() +
"/translations". Leave the macOS value alone, which is correct for it.
* [Installer] Close the /? help string so the installer compiles again
The last line of the continued MessageBox string ended in $\n\" instead of $\n".
NSIS reads \" as an escaped quote, so the string never terminated and makensis
rejected the script, which took the CPack packaging step down with it. The
Windows build failed on "Problem running NSIS command" before ever reaching the
new required-runtime check.
* [Installer] Give FindFirst the two arguments it takes
FindFirst takes the pattern and the output variable and leaves the search
handle in $0. There is no handle argument, and FindNext taking only a variable
is the giveaway: the handle has to live somewhere implicit, and it is $0.
The call passed three arguments, so makensis rejected the script and the CPack
packaging step failed with "Problem running NSIS command" before ever reaching
the required-runtime check.
This was not only a compile error. The handle variable was never populated, so
the stale-runtime check that commit 844a1a9b added could never see a match: it
probed $StaleFindHandle, which stayed empty, and the widened wildcard scan over
the purge set was a no-op. Pass the pattern, test $0, and close $0. No FindNext
is needed, because $R0 is a yes/no flag rather than a list.
Drops the now unused StaleFindHandle Var.
* [CI] Dump the NSIS compile log when packaging fails
CPack reports a failed makensis run as "Problem running NSIS command" plus a
path to NSISOutput.log, and that log exists only on the runner, so the actual
compile error never reaches the workflow output. An installer script that does
not compile is therefore undiagnosable from CI alone, which is exactly how a
three-argument FindFirst cost two build cycles here.
* [CI] Run the NSIS log dump under bash
The dump step used the default pwsh shell, where `find` resolves to Windows'
text-search find.exe rather than a file finder, so it died with "FIND:
Parameter format not correct" instead of printing anything. Pin shell: bash.
* [Installer] Test the error flag with LogicLib's ${Errors} condition
makensis rejected the script with "macro _If requires 4 parameter(s), passed
2" at the ClearErrors/OpenProcess pair. A ${If} condition cannot be a bare
$Var: LogicLib needs either a comparison or one of its flag macros, and a lone
$Errors splits into too few arguments for _If. ${Errors} is the documented flag
condition and is the same mechanism as the ${Silent} test already used here.
The dump step earned its keep - this took one run to locate rather than another
round of guessing.
* [Installer] Give FindFirst its handle back and stop blocking silent aborts
makensis: "FindFirst expects 3 parameters, got 2. Usage: FindFirst
$(user_var: handle output) $(user_var: filename output) filespec".
The previous commit was wrong about this. FindFirst does take the handle
explicitly, and FindNext finds it in $0 as a side effect of the FindFirst that
set it, which is why FindNext takes only a variable. The three-argument form the
stale-runtime check was written with was correct all along, and the two-argument
form broke it. Restored, along with Var StaleFindHandle.
That same commit inverted the interactive check in both abort paths.
`${If} $ReinstallMode = 1` with `${OrIfNot} ${Silent}` shows the message box
exactly when the install is the silent /R upgrade - the one case the comment
says must never block, since nothing will dismiss it and the installer hangs
until it is killed. Replaced with `${IfNot} ${Silent}`, so the box appears only
for an interactive install and a silent abort goes straight to SetErrorLevel.
Audited every construct added by this branch against master, which compiles:
all 9 LogicLib condition shapes have a master precedent, FindFirst is the only
FindFirst and is back to 3 arguments, Function/Section and block depth balance,
and the 12 lines with an odd number of quotes are the same pre-existing string
continuations master has.
* [CI] Strip the CR 7-Zip leaves on its paths before comparing them
The installer check reported all 14 required files missing, including
Qt6Core-era basics an 82 MB package obviously contains, so the payload was fine
and the comparison was broken. 7-Zip writes CRLF on Windows, sed 's/^Path = //p'
kept the \r on every line, and grep -qxF matches whole lines - so every entry
carried a trailing \r and nothing matched. The build-tree artifact check next to
it uses an unanchored grep -E and was unaffected, which is what made the two
checks disagree.
Reproduced locally against a 7z archive with CRLF injected: the old pipeline
matches nothing, the new one finds the present files and reports only the
genuinely absent one.
Also match case-insensitively. That is how the installed tree behaves - the
uninstaller clears both $INSTDIR\plugins and $INSTDIR\Plugins - so a
Plugins vs plugins difference in the archive is not a missing file.
* [CI] Print both sides of the installer path comparison, and strip its prefixes
The check still reported all 14 files missing after the CR fix, so the CR was
not the cause and the real difference between 7-Zip's output and the manifest
is still unknown. Inferring it again is what produced two wrong fixes in a row,
so on failure the check now prints the required list and the first 40 paths
7-Zip lists. A bare "missing" line cannot distinguish a missing file from a
misread listing, and twice it was the latter.
Also strip $INSTDIR/, $OUTDIR/, $PLUGINSDIR/ and ./ prefixes from the listed
paths. Written as separate -e expressions rather than one alternation: with |
as the sed delimiter an escaped \| is a literal pipe, so the alternation
version silently matched nothing at all.
Verified against a 7z archive fed through seven path shapes - plain, Windows
backslashes with CRLF, each prefix, lowercase plugins, and all of them combined
- each reporting only the file genuinely absent from the archive.
* [CI] Strip CR from the manifest side of the installer path check
The dump added last run settled it: 7-Zip listed 209 paths including Qt6Gui.dll,
Qt6Multimedia.dll and Plugins/platforms/qwindows.dll verbatim, as plain relative
paths with no prefix - so the archive side was already clean and the previous
fix was right to strip CR there but not sufficient.
The trailing CR was on the other side. CMake writes required-qt-runtime.txt
with the platform line ending, so on Windows $entry arrived as
"Qt6Gui.dll\r", which cannot whole-line match a clean "Qt6Gui.dll". Every
entry then read as missing while the listing held it, which is precisely what
the two sides of the dump showed.
Reproduced locally with a CRLF manifest against a 7z archive: without the strip
every entry is reported missing, with it only the genuinely absent file is.
Checked across plain paths, Windows backslashes with CRLF, $INSTDIR, $OUTDIR,
$PLUGINSDIR, ./ and / prefixes.
The literal $INSTDIR-style prefixes are written as [$] bracket expressions so
shellcheck does not read them as unexpanded variables; that clears the SC2016
that failed the format job, without a disable directive.
* [CI] Reword the bracket-expression comment so the linter stops parsing it
A comment whose first word is shellcheck is read as a directive, so
"# shellcheck does not read these as unexpanded shell variables" was parsed as
a directive named "does" and failed with SC1073 and SC1072 - which cascaded into
the SC1009 on the else clause above it. The SC2016 it was meant to document is
already gone now that the prefixes are written as [$] bracket expressions, so
the note only had to stop looking like a directive.
* [CI] Indent the diagnostic path lists without sed, and undo comment drift
SC2001 asks for parameter expansion instead of sed 's/^/ /'. Echoing
${manifest//$'\n'/$'\n '} prepends the two spaces directly and keeps the
diagnostic block readable. Both occurrences are converted; only the one reading
from a here-string was reported, but the piped one has the same shape.
Also restore master's wording of the build-tree artifact comment. An earlier
edit inserted "in " ("not the install)" became "not in the install)"). It has no
functional effect, but it was an unintended change to a line this branch has no
reason to touch, and .ci/compile.sh now differs from master only by additions.
* [CI] Ship the Qt module runtimes the Windows package check requires
The installer check reports Qt6Concurrent.dll and Qt6ShaderTools.dll missing
from the Windows package. Both are genuinely absent, not another comparison
artifact - the manifest now matches 12 of 14 entries and the dual-list
diagnostic shows the other two are not in the archive at all.
Nothing deploys them. fixup_bundle copies only the DLLs it finds in
Cockatrice.exe's import table, and the *.dll glob only sees what was already
copied next to the executable. Qt6::Concurrent is linked but never imported,
because QtConcurrent::run and QFutureWatcher are header-only in Qt 6, so the
linker drops the import; Qt6::ShaderTools is skipped the same way. The package
has shipped without them and the client still starts, but that is luck rather
than intent, and required-qt-runtime.txt asks for both.
Install the runtime of every linked module explicitly so the installer contains
what the check demands. An unused DLL costs a few hundred kilobytes; a missing
one is what stops the client starting after an update, and stays invisible until
then.
Derive both the manifest and the install list from the imported target's TYPE,
so a statically linked Qt module - which ships no runtime at all - is skipped by
each and the two can never disagree. Scope the manifest to the client rather
than the union across server, client and Oracle: only the client is packaged on
Windows, and the other two module lists are subsets of the client's, so this
changes nothing today.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
1193 lines
49 KiB
Text
1193 lines
49 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
|
|
Var StaleFindHandle
|
|
Var StaleFindName
|
|
Var IsUpgrade
|
|
Var AbortReason
|
|
Var WaitPid
|
|
Var LauncherExited
|
|
|
|
!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\
|
|
/PID=%pid% : Process to wait for before replacing files (used by the in-app updater)$\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}
|
|
|
|
; /PID= carries the process id of the application that started this installer.
|
|
; The in-app updater launches the installer and only then quits itself, so
|
|
; without it the installer has to guess whether the launching process is still
|
|
; alive by polling powershell.exe for an image name - a guess that can come back
|
|
; "not running" or "could not determine" while the process is very much alive and
|
|
; still has its Qt DLLs mapped. An older client does not pass /PID= and an older
|
|
; installer ignores it, so both sides of the pair have to keep working.
|
|
; Note that /D= must stay the last parameter on the command line, which is why
|
|
; the client puts it last and this one anywhere.
|
|
StrCpy $WaitPid ""
|
|
StrCpy $LauncherExited 0
|
|
ClearErrors
|
|
${GetOptions} $9 "/PID=" $8
|
|
${IfNot} ${Errors}
|
|
StrCpy $WaitPid $8
|
|
${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
|
|
|
|
; Wait for the process that launched this installer to exit before anything is
|
|
; replaced or deleted. It goes first because it is the one check here that needs
|
|
; no powershell.exe: on a machine where the process lookup cannot run at all, the
|
|
; launcher is still proven gone here instead of the whole upgrade being cancelled
|
|
; by a check that was only ever a proxy for this one.
|
|
;
|
|
; Then make sure no *other* 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 WaitForLauncherToExit
|
|
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 - but it cannot simply be waved through either, see
|
|
; HandleUnknownAppState.
|
|
${If} $R0 = 0
|
|
Return
|
|
${EndIf}
|
|
${If} $R0 = 2
|
|
Call HandleUnknownAppState
|
|
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 = 2
|
|
Call HandleUnknownAppState
|
|
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."
|
|
StrCpy $AbortReason \
|
|
"$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."
|
|
Call AbortInstall
|
|
${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 = 2
|
|
Call HandleUnknownAppState
|
|
Return
|
|
${EndIf}
|
|
DetailPrint "$R1 is still running after 60 s; aborting the update."
|
|
StrCpy $AbortReason \
|
|
"$R1 could not be stopped, so the installation was aborted.$\r$\n$\r$\nPlease close $R1 and run the installer again."
|
|
Call AbortInstall
|
|
${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 = 2
|
|
Call HandleUnknownAppState
|
|
Return
|
|
${EndIf}
|
|
Goto ck_wait_prompt
|
|
ck_abort_install:
|
|
Abort
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
; A process lookup came back "could not determine": powershell.exe ran but the
|
|
; answer was not usable, e.g. the process could not be inspected at all. That
|
|
; state is fine to carry on from when the user is at the keyboard - they can
|
|
; close the applications by hand and the stale-runtime check still stands
|
|
; between a failed guard and a mismatched install.
|
|
;
|
|
; It is not fine on a silent upgrade. There the whole point of this guard is to
|
|
; know that nothing holds locks on the files about to be replaced, and this is
|
|
; exactly the state where that is unknown, with no one to click a dialog: the
|
|
; old code returned and let the install carry on into the mixed Qt DLL set that
|
|
; made the next start fail. Cancel the update instead; the next attempt runs
|
|
; with the applications closed and succeeds.
|
|
Function HandleUnknownAppState
|
|
${If} $ReinstallMode = 1
|
|
StrCpy $AbortReason \
|
|
"The installation was cancelled because it could not determine whether Cockatrice is still running.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice and run the installer again."
|
|
Call AbortInstall
|
|
${EndIf}
|
|
DetailPrint "Could not determine whether $R1 is running; close Cockatrice, Oracle and Servatrice manually if the installation reports a problem."
|
|
${IfNot} ${Silent}
|
|
MessageBox MB_OK|MB_ICONEXCLAMATION \
|
|
"This installer could not determine whether $R1 is still running.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice manually before continuing."
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
Function EnsureAppsNotRunning
|
|
; guards the install directory
|
|
StrCpy $GuardDir $INSTDIR
|
|
Call GuardAppsNotRunning
|
|
FunctionEnd
|
|
|
|
; Aborts the installation, showing $AbortReason to the user.
|
|
;
|
|
; Every abort goes through here so the unattended case is handled the same way
|
|
; everywhere: 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, while a /S install without /R is unattended - by a
|
|
; package manager, for instance - and a message box there would block forever.
|
|
; Only the first of those two gets a dialog; the second reports through the log
|
|
; and the exit code.
|
|
Function AbortInstall
|
|
${IfNot} ${Silent}
|
|
MessageBox MB_OK|MB_ICONEXCLAMATION "$AbortReason"
|
|
${EndIf}
|
|
SetErrorLevel 1
|
|
Abort
|
|
FunctionEnd
|
|
|
|
; Waits for the application that launched this installer to exit, using the /PID=
|
|
; it passed. Sets $LauncherExited to 1 when that process is provably gone.
|
|
;
|
|
; This is the authority for the one question the rest of the installer can only
|
|
; approximate: is the process that started us still holding its Qt DLLs mapped?
|
|
; A mapped module cannot be deleted, so an answer of "not running" obtained by
|
|
; enumerating process names is what allows the update to write a new payload on
|
|
; top of files it could not remove. Waiting on a real handle has no such gap,
|
|
; needs no powershell.exe, and is unaffected by a process whose path cannot be
|
|
; read - the case the name based check has to treat as "not ours" so it never
|
|
; closes a third party application.
|
|
;
|
|
; Sets $LauncherExited to 0 when no usable /PID= was given, which is the older
|
|
; client that does not pass one; the caller then has no authority available and
|
|
; falls back to the name based guard.
|
|
Function WaitForLauncherToExit
|
|
StrCpy $LauncherExited 0
|
|
${If} $WaitPid == ""
|
|
Return
|
|
${EndIf}
|
|
; PROCESS_SYNCHRONIZE is the only access this needs - enough to wait on the
|
|
; handle, and the least likely to be refused for a reason that has nothing to
|
|
; do with the upgrade.
|
|
ClearErrors
|
|
System::Call 'kernel32::OpenProcess(i 0x00100000, i 0, i $WaitPid) p.r0'
|
|
${If} ${Errors}
|
|
; The launcher is gone - OpenProcess cannot open a dead process - which is
|
|
; exactly the state the wait was for.
|
|
DetailPrint "The application that started this installer has already exited."
|
|
StrCpy $LauncherExited 1
|
|
Return
|
|
${EndIf}
|
|
DetailPrint "Waiting for the application that started this installer to exit ..."
|
|
; Same 60 s grace period the name based check uses, so a client that is slow
|
|
; to flush its card database does not get less room than it used to.
|
|
; WAIT_OBJECT_0 is 0; WAIT_TIMEOUT is 258 and a failed wait reports 0xFFFFFFFF.
|
|
System::Call 'kernel32::WaitForSingleObject(p r0, i 60000) i.r1'
|
|
System::Call 'kernel32::CloseHandle(p r0)'
|
|
${If} $R1 != 0
|
|
DetailPrint "The application that started this installer did not exit within 60 s."
|
|
StrCpy $AbortReason \
|
|
"The application that started this update has not exited yet, so the installation was cancelled rather than replace files it is still using.$\r$\n$\r$\nPlease close Cockatrice and run the installer again."
|
|
Call AbortInstall
|
|
${EndIf}
|
|
DetailPrint "The application that started this installer has exited."
|
|
StrCpy $LauncherExited 1
|
|
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.
|
|
DetailPrint "Could not start powershell.exe; skipping the running instance check. Close Cockatrice, Oracle and Servatrice manually if the installation reports a problem."
|
|
; On a silent upgrade that used to be a warning and nothing more, which
|
|
; left the very process that launched the installer free to go on
|
|
; replacing files it still had mapped - the one outcome this guard exists
|
|
; to prevent - and then writing the new payload on top of them. Cancel
|
|
; instead. An interactive install can still continue: the user is at the
|
|
; keyboard, and refusing outright would leave them with an installer that
|
|
; can never complete.
|
|
;
|
|
; Unless the launcher has already been waited on directly: WaitForLauncherToExit
|
|
; needs no powershell.exe, so when it has run, Cockatrice is known to be
|
|
; gone and the lookup failing to run costs nothing but the Oracle and
|
|
; Servatrice checks. An older client that passes no /PID= leaves no such
|
|
; authority, and there the upgrade is cancelled as before.
|
|
${If} $LauncherExited != 1
|
|
${AndIf} $ReinstallMode = 1
|
|
StrCpy $AbortReason \
|
|
"The installation was cancelled because it could not check whether Cockatrice is still running, because powershell.exe could not be started.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice and run the installer again."
|
|
Call AbortInstall
|
|
${EndIf}
|
|
; The user has to be told to close the applications by hand, but a /S
|
|
; install without /R is unattended and would block on this forever.
|
|
${IfNot} ${Silent}
|
|
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."
|
|
${EndIf}
|
|
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. Every Qt module Cockatrice links is part of
|
|
; the payload, so match them all: probing only Qt6Core.dll would miss a
|
|
; directory where that one file happened to be deletable and the others
|
|
; were not.
|
|
StrCpy $R3 "Qt6*.dll"
|
|
Call CheckStaleRuntimeMask
|
|
${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 file
|
|
; is enough to prove that something still holds a lock on the install directory.
|
|
;
|
|
; The masks below have to cover the purge above one for one. A check that only
|
|
; probed named files could not see a locked Qt6Multimedia.dll or a Plugins
|
|
; directory the purge was unable to remove, so the exact files that broke the
|
|
; install were also the ones this check was blind to.
|
|
Function HasStaleRuntimeFiles
|
|
StrCpy $R0 0
|
|
StrCpy $R3 "Qt*.dll"
|
|
Call CheckStaleRuntimeMask
|
|
StrCpy $R3 "libcrypto*.dll"
|
|
Call CheckStaleRuntimeMask
|
|
StrCpy $R3 "libssl*.dll"
|
|
Call CheckStaleRuntimeMask
|
|
StrCpy $R3 "zlib*.dll"
|
|
Call CheckStaleRuntimeMask
|
|
StrCpy $R3 "icu*.dll"
|
|
Call CheckStaleRuntimeMask
|
|
StrCpy $R3 "libeay32.dll"
|
|
Call CheckStaleRuntimeMask
|
|
StrCpy $R3 "ssleay32.dll"
|
|
Call CheckStaleRuntimeMask
|
|
${If} $R0 = 1
|
|
Return
|
|
${EndIf}
|
|
${If} ${FileExists} "$INSTDIR\libmysql.dll"
|
|
StrCpy $R0 1
|
|
Return
|
|
${EndIf}
|
|
; IfFileExists takes wildcards, so a Plugins directory the purge could not
|
|
; remove counts as a leftover too.
|
|
${If} ${FileExists} "$INSTDIR\Plugins\*.*"
|
|
StrCpy $R0 1
|
|
${EndIf}
|
|
FunctionEnd
|
|
|
|
; usage: set $R3 to a file mask, call this. Sets $R0 to 1 when $INSTDIR holds a
|
|
; matching file. Returns early once something was found, so $R0 stays 1.
|
|
Function CheckStaleRuntimeMask
|
|
${If} $R0 = 1
|
|
Return
|
|
${EndIf}
|
|
FindFirst $StaleFindHandle $StaleFindName "$INSTDIR\$R3"
|
|
${If} $StaleFindHandle != ""
|
|
StrCpy $R0 1
|
|
FindClose $StaleFindHandle
|
|
${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
|
|
; Remembered because the stale-runtime check below must only ever fire for a
|
|
; directory this installer recognised as a Cockatrice install. Scanning for
|
|
; leftovers in an unrelated directory (a first-time install into a folder that
|
|
; happens to hold some Qt5 DLLs, say) would otherwise turn those files into an
|
|
; abort reason even though the payload is about to overwrite nothing of theirs.
|
|
StrCpy $IsUpgrade $R0
|
|
${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
|
|
; ...Qt6QuickWidgets.dll" or "The code execution cannot proceed because
|
|
; Qt6Multimedia.dll was not found", so stop here instead of compounding it.
|
|
; Nothing of the new payload has been written at this point, so the next run -
|
|
; with the applications closed - gets a clean install.
|
|
${If} $IsUpgrade = 1
|
|
Call HasStaleRuntimeFiles
|
|
${Else}
|
|
StrCpy $R0 0
|
|
${EndIf}
|
|
${If} $R0 = 1
|
|
DetailPrint "Files of a previous version could not be removed; aborting the installation."
|
|
; The in-app updater runs this installer silent and detached and never looks
|
|
; at its exit code, so the user has to be told what happened. A /S install
|
|
; without /R is unattended - 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.
|
|
${IfNot} ${Silent}
|
|
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, and was cancelled rather than leave a mismatched install behind.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice and run the installer again."
|
|
${EndIf}
|
|
SetErrorLevel 1
|
|
Abort
|
|
${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
|