mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
Merge 2ea11d4599 into 7b4827dadd
This commit is contained in:
commit
850f31fe63
5 changed files with 1713 additions and 1623 deletions
File diff suppressed because it is too large
Load diff
72
cmake/nsis/installer-log.nsh
Normal file
72
cmake/nsis/installer-log.nsh
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
; Installer log helpers: CockLog, CockLogWrite and CockLogListing. Included
|
||||||
|
; before the first function that logs, because CockLog is a macro and is expanded
|
||||||
|
; at the point of use. Nothing here returns anything; CockLogWrite uses
|
||||||
|
; $LogMessage/$LogHandle, CockLogListing the $Log* match variables.
|
||||||
|
|
||||||
|
; Installer log
|
||||||
|
; Every run appends a step-by-step trace to a file so a failed upgrade on a
|
||||||
|
; user's machine can be diagnosed after the fact. Kept in $TEMP rather than in
|
||||||
|
; $INSTDIR because the previous version's uninstaller removes the whole install
|
||||||
|
; directory part-way through an upgrade. Read it back with:
|
||||||
|
; notepad %TEMP%\Cockatrice-installer.log
|
||||||
|
!macro CockLog text
|
||||||
|
Push "${text}"
|
||||||
|
Call CockLogWrite
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
; The body is shared because the uninstaller needs its own copy: un.* functions are
|
||||||
|
; compiled into a separate binary, and inside one a Call may only name an un. function,
|
||||||
|
; so CockLog's Call to CockLogWrite cannot be used there.
|
||||||
|
!macro CockLogWriteBody
|
||||||
|
Pop $LogMessage
|
||||||
|
FileOpen $LogHandle "$InstallLogPath" a
|
||||||
|
${If} $LogHandle == ""
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
; FileOpen places the pointer at the start of the file in every mode, so
|
||||||
|
; 'a' alone would overwrite the log from the beginning on each line. Seek
|
||||||
|
; past the existing content first to actually append.
|
||||||
|
FileSeek $LogHandle 0 END
|
||||||
|
FileWrite $LogHandle "$LogMessage$\r$\n"
|
||||||
|
FileClose $LogHandle
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
Function CockLogWrite
|
||||||
|
!insertmacro CockLogWriteBody
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; CockLog for un.* code: same lines, prefixed call target.
|
||||||
|
!macro CockLogUn text
|
||||||
|
Push "${text}"
|
||||||
|
Call un.CockLogWrite
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
Function un.CockLogWrite
|
||||||
|
!insertmacro CockLogWriteBody
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; Logs every file matching $INSTDIR\<mask>. Used to record the DLL inventory
|
||||||
|
; before and after the upgrade purge, which is what shows whether Qt6Gui.dll was
|
||||||
|
; on disk and what (if anything) survived the deletes.
|
||||||
|
!macro CockLogListing label mask
|
||||||
|
Push "${label}"
|
||||||
|
Push "$INSTDIR\${mask}"
|
||||||
|
Call CockLogListing
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
Function CockLogListing
|
||||||
|
Pop $LogMatchPattern
|
||||||
|
Pop $LogMatchLabel
|
||||||
|
!insertmacro CockLog "listing [$LogMatchLabel] pattern=$LogMatchPattern"
|
||||||
|
FindFirst $LogFindHandle $LogFindName "$LogMatchPattern"
|
||||||
|
${If} $LogFindHandle == ""
|
||||||
|
!insertmacro CockLog "listing [$LogMatchLabel] (no matches)"
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
log_listing_loop:
|
||||||
|
!insertmacro CockLog "listing [$LogMatchLabel] $LogFindName"
|
||||||
|
ClearErrors
|
||||||
|
FindNext $LogFindHandle $LogFindName
|
||||||
|
IfErrors 0 log_listing_loop
|
||||||
|
FindClose $LogFindHandle
|
||||||
|
FunctionEnd
|
||||||
688
cmake/nsis/process-guard.nsh
Normal file
688
cmake/nsis/process-guard.nsh
Normal file
|
|
@ -0,0 +1,688 @@
|
||||||
|
; Running instance handling: make sure no installed application still holds the
|
||||||
|
; files that are about to be deleted or replaced, closing or force-stopping it
|
||||||
|
; where the caller allows that, and prompting the user where it does not.
|
||||||
|
;
|
||||||
|
; The probe routines exist twice, once plain and once with the un prefix. That is
|
||||||
|
; forced by NSIS: the uninstaller is a separate program compiled into the same
|
||||||
|
; installer, and it cannot call the installer's functions. Keep the two copies in
|
||||||
|
; step. The launcher wait is installer-only: the uninstaller is never given a
|
||||||
|
; /PID= to wait on.
|
||||||
|
;
|
||||||
|
; Register contract:
|
||||||
|
; $GuardDir in: directory a matched process has to run from
|
||||||
|
; $GuardExit out: RunHolderProbe - 0 when nothing holds the payload, 1
|
||||||
|
; when its line names what does, anything else = no answer
|
||||||
|
; $GuardOutput out: RunHolderProbe - its one-line answer
|
||||||
|
; $GuardCloseRounds/$GuardLastHolders/$GuardRecheckCount
|
||||||
|
; internal: the guard's automatic close and its rechecks
|
||||||
|
; $GuardPrompt internal: the question CockEndPrompt/CockEndRecheckPrompt build
|
||||||
|
; $WaitPid in: WaitForLauncherToExit - the launcher's /PID=
|
||||||
|
; $WaitSlice/$WaitCanEnd/$WaitWindowless
|
||||||
|
; internal: the launcher wait's slice and what it may do
|
||||||
|
; $LauncherExited out: WaitForLauncherToExit - 1 when the /PID= process was
|
||||||
|
; seen to exit, 0 when no usable /PID= was given
|
||||||
|
; $AbortReason out: set before AbortInstall on every abort path
|
||||||
|
;
|
||||||
|
; AbortInstall itself stays in the template: its Sentry offer is substituted by
|
||||||
|
; CPack, which only configures that one file.
|
||||||
|
|
||||||
|
; --- 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.
|
||||||
|
;
|
||||||
|
; Both questions this installer has about running processes - "may I replace
|
||||||
|
; the payload?" and "what is holding this file I could not delete?" - are
|
||||||
|
; answered by tools/installer-holder-probe, a small executable built from this
|
||||||
|
; repository and embedded in the installer and the uninstaller alike. It names
|
||||||
|
; every process and service that has one of the payload files open or mapped
|
||||||
|
; (the Restart Manager, RmRegisterResources/RmGetList) and, separately, every
|
||||||
|
; payload program whose image lies in the directory being replaced rather than
|
||||||
|
; in a folder below it.
|
||||||
|
;
|
||||||
|
; The second lookup is not redundant. Restart Manager answers "who has this
|
||||||
|
; file open", and a running instance does not have to have one open: its own
|
||||||
|
; image and the modules it loaded are mapped into it rather than opened by it,
|
||||||
|
; and a mapped image is not reliably reported. That is the state the failed
|
||||||
|
; updates this probe exists for were in - every DLL still on disk, an instance
|
||||||
|
; running from the directory, and "nothing is holding them any more" as the
|
||||||
|
; answer. A guard built on file handles alone therefore misses exactly the
|
||||||
|
; instance it exists to find; it would pass, the purge would fail, and the
|
||||||
|
; install would end in a warning with no offer to close and try again.
|
||||||
|
;
|
||||||
|
; Why not enumerate processes from this script: the NSIS stub is a 32-bit
|
||||||
|
; process, so a bare "powershell" started from it is the 32-bit Windows
|
||||||
|
; PowerShell, whose Get-Process cannot read the path of a 64-bit process. A
|
||||||
|
; guard built on that misses exactly the running instance it exists to find.
|
||||||
|
; The probe opens each process with PROCESS_QUERY_LIMITED_INFORMATION and calls
|
||||||
|
; QueryFullProcessImageNameW, which has no bitness mismatch, and Restart
|
||||||
|
; Manager is a system API rather than a view of this process's architecture.
|
||||||
|
;
|
||||||
|
; The files that count are the payload the purge removes and the installer
|
||||||
|
; replaces: the three executables, the top-level DLLs, qt.conf and everything
|
||||||
|
; below Plugins. A document, a log or a screenshot the user happens to have
|
||||||
|
; open in the install directory is not part of it and cannot block an upgrade;
|
||||||
|
; the image lookup matches on the directory a process runs from, so it does not
|
||||||
|
; widen that set.
|
||||||
|
;
|
||||||
|
; Nothing is force-stopped without the user asking for it: Cockatrice may be holding
|
||||||
|
; an unsaved deck or showing a "really leave this game?" confirmation, Oracle writes
|
||||||
|
; the card database in place and can be killed mid-write, and Servatrice may be serving
|
||||||
|
; a game. A close is a request, made automatically because that is what the user started
|
||||||
|
; the installer to do: the probe first asks every instance over its local socket, which
|
||||||
|
; reaches a hidden window and an instance sitting on its own confirmation, and falls back
|
||||||
|
; to WM_CLOSE for an older client. The automatic close is retried while the set of holders
|
||||||
|
; changes, so one instance exiting does not leave the next one unasked. When a close is
|
||||||
|
; still not answered the user is told which program is in the way and is offered to force
|
||||||
|
; close all three, check again after closing them by hand, or abort - the last resort, with
|
||||||
|
; what it costs spelled out in the prompt.
|
||||||
|
|
||||||
|
; One sentence for every prompt whose Yes ends the whole set, so that answer reads the same
|
||||||
|
; wherever the user meets it: this guard before anything is replaced, the uninstaller's, and
|
||||||
|
; the recovery prompt after a failed purge.
|
||||||
|
!define END_COST "An unsaved deck, a running game or an unwritten setting is lost, Servatrice drops its players, and an Oracle that is mid-write can damage the card database."
|
||||||
|
|
||||||
|
; What the user does instead when they say no: the prompt's other answer stops the run, so they
|
||||||
|
; are told how to get past the same wall next time. Also used by the abort dialogs, which is
|
||||||
|
; where the answer to "why did it stop" ends up.
|
||||||
|
!define AFTER_CLOSING_GUIDANCE "Answer the confirmation Cockatrice may be showing - open it from the notification area if it is hidden there - and use Task Manager for a program that would not end."
|
||||||
|
|
||||||
|
; The escape hatch for a user who would rather not force-close anything: the same release,
|
||||||
|
; downloaded and run by hand once Cockatrice is closed. Every abort dialog shows this and
|
||||||
|
; offers to open the page, so an update that cannot free the files still leaves the user a
|
||||||
|
; way forward that does not end in Task Manager.
|
||||||
|
!define LATEST_RELEASE_URL "https://github.com/Cockatrice/Cockatrice/releases/latest"
|
||||||
|
!define MANUAL_UPDATE_GUIDANCE "To update safely instead, close Cockatrice and download the latest installer from:$\r$\n${LATEST_RELEASE_URL}$\r$\nThen run it manually."
|
||||||
|
|
||||||
|
; usage: set the variable named by the second argument to the probe's listing, then call
|
||||||
|
; this. Sets $GuardPrompt to the question the guard and the recovery path ask.
|
||||||
|
;
|
||||||
|
; One macro rather than three copies, because the wording is a promise: Yes ends the
|
||||||
|
; programs the payload installs - which is exactly the set EndHolders acts on - and anything
|
||||||
|
; else the probe named has to be closed by hand, so a listing that includes a scanner or a
|
||||||
|
; service hosted elsewhere does not come with an answer that cannot do what it says.
|
||||||
|
;
|
||||||
|
; Two answers, one of each: the automatic close already ran when this is asked, so Yes is the
|
||||||
|
; only thing left that can free the files and No stops the run. The guard itself uses
|
||||||
|
; CockEndRecheckPrompt instead, whose No checks again; this one stays for the recovery path,
|
||||||
|
; where a retry cannot change anything because the purge was already retried.
|
||||||
|
;
|
||||||
|
; The assembled string has to fit the characters one NSIS string holds, which is what
|
||||||
|
; bounds HOLDER_LINE_CHAR_MAX in tools\installer-holder-probe\holder_line.h: with the
|
||||||
|
; longest listing that can arrive it stays inside that budget instead of being cut off at
|
||||||
|
; the tail, where the cancel line is.
|
||||||
|
;
|
||||||
|
; @param header the sentence that introduces the listing, with its trailing colon
|
||||||
|
; @param listing the variable holding the probe's line, $GuardOutput or $HolderOutput
|
||||||
|
; @param cancelWord what No gives up, "installation" or "uninstallation"
|
||||||
|
!macro CockEndPrompt header listing cancelWord
|
||||||
|
StrCpy $GuardPrompt "${header}$\r$\n$\r$\n${listing}"
|
||||||
|
StrCpy $GuardPrompt "$GuardPrompt$\r$\n$\r$\nChoose Yes to force close the Cockatrice, Oracle and Servatrice programs listed above. ${END_COST}$\r$\n$\r$\nChoose No to cancel the ${cancelWord}. ${AFTER_CLOSING_GUIDANCE}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
; usage: like CockEndPrompt, for a MB_YESNOCANCEL question whose No checks again.
|
||||||
|
;
|
||||||
|
; The automatic close has already run, so the three answers are what is left: force close
|
||||||
|
; the programs the payload installs (Yes), close them by hand and check again (No), or stop
|
||||||
|
; the run (Cancel). No used to be the only alternative to ending them, which meant a user who
|
||||||
|
; would rather close the instance themselves had to abandon the update and start the
|
||||||
|
; installer again. The listing the question is asked with is refreshed on every pass, so the
|
||||||
|
; question changes as programs go away instead of repeating itself word for word.
|
||||||
|
;
|
||||||
|
; The manual-update guidance lives in the abort dialog, which is where a Cancel ends up, not
|
||||||
|
; in this prompt. With the longest listing HOLDER_LINE_CHAR_MAX allows, the assembled string
|
||||||
|
; stays under 900 characters, inside the 1024 one NSIS string holds, so the Cancel line is
|
||||||
|
; never the part that gets cut off.
|
||||||
|
;
|
||||||
|
; @param header the sentence that introduces the listing, with its trailing colon
|
||||||
|
; @param listing the variable holding the probe's line, $GuardOutput
|
||||||
|
; @param cancelWord what Cancel gives up, "installation" or "uninstallation"
|
||||||
|
!macro CockEndRecheckPrompt header listing cancelWord
|
||||||
|
StrCpy $GuardPrompt "${header}$\r$\n$\r$\n${listing}"
|
||||||
|
StrCpy $GuardPrompt "$GuardPrompt$\r$\n$\r$\nChoose Yes to force close the Cockatrice, Oracle and Servatrice programs listed above. ${END_COST}$\r$\n$\r$\nChoose No to close them yourself, then check again - answer the confirmation Cockatrice may be showing, and open it from the notification area if it is hidden there.$\r$\n$\r$\nChoose Cancel to abort the ${cancelWord}."
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
; Emitted once for the installer and once for the uninstaller: the uninstaller
|
||||||
|
; cannot call installer functions, and its own copy of the probe keeps a running
|
||||||
|
; 64-bit instance from being missed there too.
|
||||||
|
!macro CockHolderProbeFunctions prefix tag
|
||||||
|
Function ${prefix}ExtractHolderProbe
|
||||||
|
InitPluginsDir
|
||||||
|
IfFileExists "$PLUGINSDIR\installer-holder-probe.exe" ck_probe_ready_${tag}
|
||||||
|
File "/oname=$PLUGINSDIR\installer-holder-probe.exe" "..\..\..\installer-holder-probe.exe"
|
||||||
|
ck_probe_ready_${tag}:
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; usage: set $GuardDir to the directory to check, then call this. Sets
|
||||||
|
; $GuardExit to the probe's exit code and $GuardOutput to its one-line answer.
|
||||||
|
; 0 = nothing holds the payload and no payload program runs from the directory, 1 = the
|
||||||
|
; line names what does, anything else = the probe could not answer (2 = no
|
||||||
|
; directory was given, 3 = no payload program was named but one refuses to be replaced, or
|
||||||
|
; the Restart Manager call failed and no payload program runs from the directory either,
|
||||||
|
; "timeout" = it did not return in time).
|
||||||
|
Function ${prefix}RunHolderProbe
|
||||||
|
Call ${prefix}ExtractHolderProbe
|
||||||
|
; The directory travels in an environment variable, never on the command
|
||||||
|
; line: a path with spaces, quotes or a trailing backslash then does not
|
||||||
|
; have to survive NSIS quoting.
|
||||||
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
||||||
|
; /TIMEOUT because this can run before the first page: a Restart Manager
|
||||||
|
; that does not answer must not leave the installer with no window and no
|
||||||
|
; way out.
|
||||||
|
nsExec::ExecToStack /TIMEOUT=20000 '"$PLUGINSDIR\installer-holder-probe.exe"'
|
||||||
|
Pop $GuardExit
|
||||||
|
Pop $GuardOutput
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; usage: set $GuardDir to the directory being replaced, then call this. Asks the running
|
||||||
|
; Cockatrice to close, and waits for it to exit.
|
||||||
|
;
|
||||||
|
; The set is what the probe names: the processes holding a file of the version being
|
||||||
|
; replaced, plus the payload programs running from the directory itself. The probe
|
||||||
|
; closes that set by pid, which is why nothing here has to match an executable path. Only
|
||||||
|
; cockatrice.exe is asked: Oracle writes the card database in place and Servatrice has no
|
||||||
|
; window to ask, so both are left alone here and only ended by the explicit answer.
|
||||||
|
;
|
||||||
|
; Each instance is asked to close over the local socket it listens on, which is the same
|
||||||
|
; request as clicking its close button and reaches one whose window is hidden in the
|
||||||
|
; notification area or is sitting on its own confirmation. A client from before that socket
|
||||||
|
; is asked with WM_CLOSE on its own window instead, and a visible window is brought forward
|
||||||
|
; first. Either way the close is answered, not obeyed - Cockatrice asks before it quits while
|
||||||
|
; a card database update is running, a game is open or a deck is unsaved - and that question
|
||||||
|
; is only useful if it is in front of the installer instead of behind it.
|
||||||
|
;
|
||||||
|
; The probe waits up to 30 s for the processes it asked to exit, so the check that follows
|
||||||
|
; measures a settled directory rather than sleeping for a fixed time. Its line goes into the
|
||||||
|
; log as it stands: it names every holder, including the ones left to the user, so a close
|
||||||
|
; that did nothing can be told apart from one that was never sent.
|
||||||
|
;
|
||||||
|
; Never a force-close here: the application decides whether to quit. Ending is a separate
|
||||||
|
; answer the guard prompt offers when a close did not free the files, spelled out with what
|
||||||
|
; it costs - see EndHolders - and it ends every program the payload installs.
|
||||||
|
Function ${prefix}CloseHolders
|
||||||
|
Call ${prefix}ExtractHolderProbe
|
||||||
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
||||||
|
; The probe waits 30 s itself. The timeout here only bounds a probe that never returns;
|
||||||
|
; the Restart Manager call inside it is the same one the holder listing makes.
|
||||||
|
nsExec::ExecToStack /TIMEOUT=40000 '"$PLUGINSDIR\installer-holder-probe.exe" --close-holders'
|
||||||
|
Pop $R3
|
||||||
|
Pop $R4
|
||||||
|
; Not the CockLog macro: this function exists twice and the uninstaller copy may only
|
||||||
|
; Call un. functions, so the macro's two lines are spelled with the prefix here.
|
||||||
|
Push "close-holders: probe exit=$R3 [$R4]"
|
||||||
|
Call ${prefix}CockLogWrite
|
||||||
|
DetailPrint "Closing Cockatrice: $R4"
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; usage: set $GuardDir to the directory being replaced, then call this. Ends the running
|
||||||
|
; Cockatrice, Oracle and Servatrice instances the probe names.
|
||||||
|
;
|
||||||
|
; This is the only place anything is stopped without being asked to stop, so it is only
|
||||||
|
; reached after the user picked it from a prompt that says what it costs: a client that is
|
||||||
|
; still running may have an unsaved deck, a game or a settings write in flight, Oracle may
|
||||||
|
; be part-way through writing the card database, and Servatrice may be serving a game. The
|
||||||
|
; probe ends the three programs the payload installs, services included, and nothing else:
|
||||||
|
; a scanner or a program the user keeps in the directory is named and left to the user.
|
||||||
|
Function ${prefix}EndHolders
|
||||||
|
Call ${prefix}ExtractHolderProbe
|
||||||
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
||||||
|
; The probe waits up to 5 s for what it ended to be gone, so the check that follows
|
||||||
|
; measures a settled directory instead of racing the processes it just terminated.
|
||||||
|
nsExec::ExecToStack /TIMEOUT=20000 '"$PLUGINSDIR\installer-holder-probe.exe" --end-holders'
|
||||||
|
Pop $R3
|
||||||
|
Pop $R4
|
||||||
|
Push "end-holders: probe exit=$R3 [$R4]"
|
||||||
|
Call ${prefix}CockLogWrite
|
||||||
|
DetailPrint "Ending Cockatrice, Oracle and Servatrice: $R4"
|
||||||
|
FunctionEnd
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!insertmacro CockHolderProbeFunctions "" install
|
||||||
|
!insertmacro CockHolderProbeFunctions "un." uninstall
|
||||||
|
|
||||||
|
; usage: set $GuardDir to the directory to guard, then call this.
|
||||||
|
;
|
||||||
|
; Asks Cockatrice to close once, then asks one question with one answer that can free the
|
||||||
|
; payload. The automatic close is the same request clicking the window's close button makes,
|
||||||
|
; so the common case - the client was still shutting down - never turns into a question. The
|
||||||
|
; prompt is what remains when the close cannot reach everything: Oracle writes the card
|
||||||
|
; database in place, Servatrice has no window, and a client that is sitting on its own
|
||||||
|
; confirmation needs an answer nobody can give it from here. It offers to end the programs
|
||||||
|
; the payload installs - and only those, which the text says - with what that costs (Yes),
|
||||||
|
; and its other answer stops the install (No) instead of repeating the same dialog. What
|
||||||
|
; survives an end is reported in the abort, never asked about again - ending is the last
|
||||||
|
; resort, see EndHolders.
|
||||||
|
;
|
||||||
|
; An error is always shown, in every mode. A /S install without /R has nobody at
|
||||||
|
; the keyboard, but this installer does not promise unattended runs: when an
|
||||||
|
; error needs a decision, asking for one is better than silently leaving a
|
||||||
|
; half-updated install behind. The happy path has no dialog, so a /S reinstall
|
||||||
|
; that has nothing to report stays silent.
|
||||||
|
Function GuardAppsNotRunning
|
||||||
|
!insertmacro CockLog "guard: checking $GuardDir (launcherExited=$LauncherExited)"
|
||||||
|
StrCpy $GuardCloseRounds 0
|
||||||
|
StrCpy $GuardRecheckCount 0
|
||||||
|
StrCpy $GuardLastHolders ""
|
||||||
|
guard_check:
|
||||||
|
Call RunHolderProbe
|
||||||
|
${If} $GuardExit == "0"
|
||||||
|
!insertmacro CockLog "guard: nothing is holding the payload"
|
||||||
|
DetailPrint "No running Cockatrice, Oracle or Servatrice instance was found."
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
${If} $GuardExit == "1"
|
||||||
|
!insertmacro CockLog "guard: still in use by $GuardOutput"
|
||||||
|
; Close it for the user first. The probe asks every instance over its local socket
|
||||||
|
; and falls back to WM_CLOSE for an older client, so this is the same request as
|
||||||
|
; clicking the window's close button: the application still answers its own "really
|
||||||
|
; quit?" prompt, so nothing is lost. A second round is spent only when the listing
|
||||||
|
; changed, which means something exited and the rest is worth asking again; repeating
|
||||||
|
; the same request against the same processes cannot change anything.
|
||||||
|
${If} $GuardCloseRounds < 2
|
||||||
|
${AndIf} $GuardOutput != $GuardLastHolders
|
||||||
|
StrCpy $GuardLastHolders $GuardOutput
|
||||||
|
IntOp $GuardCloseRounds $GuardCloseRounds + 1
|
||||||
|
!insertmacro CockLog "guard: asking the payload programs to close (round $GuardCloseRounds)"
|
||||||
|
Call CloseHolders
|
||||||
|
Goto guard_check
|
||||||
|
${EndIf}
|
||||||
|
DetailPrint "The install directory is in use by: $GuardOutput"
|
||||||
|
; The close ran and did not free the files. Force-closing them is what the installer
|
||||||
|
; can do (Yes); closing them by hand is what the user can do (No checks again, with
|
||||||
|
; the fresh listing); Cancel stops the run and the abort dialog offers the same
|
||||||
|
; update from the official download page. Without the middle answer a user who did
|
||||||
|
; not want anything killed had no way forward but to abandon the update.
|
||||||
|
guard_ask:
|
||||||
|
!insertmacro CockEndRecheckPrompt "Cockatrice, Oracle or Servatrice is still using files of the installation:" $GuardOutput "installation"
|
||||||
|
MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON2 "$GuardPrompt" IDYES guard_end IDNO guard_recheck
|
||||||
|
Goto guard_cancel
|
||||||
|
guard_recheck:
|
||||||
|
IntOp $GuardRecheckCount $GuardRecheckCount + 1
|
||||||
|
!insertmacro CockLog "guard: user closed programs by hand, checking again (recheck $GuardRecheckCount)"
|
||||||
|
; A fresh set gets a fresh automatic close, exactly as if the guard had just started:
|
||||||
|
; what the user closed by hand may have been one instance of several.
|
||||||
|
StrCpy $GuardCloseRounds 0
|
||||||
|
StrCpy $GuardLastHolders ""
|
||||||
|
Goto guard_check
|
||||||
|
guard_cancel:
|
||||||
|
!insertmacro CockLog "guard: cancelled by user after $GuardRecheckCount recheck(s)"
|
||||||
|
StrCpy $AbortReason \
|
||||||
|
"The installation was cancelled because Cockatrice, Oracle or Servatrice is still running and holding files of the previous version.$\r$\n$\r$\n${AFTER_CLOSING_GUIDANCE}$\r$\n$\r$\nClose them and run the installer again."
|
||||||
|
; RemoveOldPayloadLeftovers runs with the pre-uninstall backup already taken;
|
||||||
|
; restoring here keeps this abort from stranding the only copy in %TEMP%.
|
||||||
|
Call RestoreUserDataAfterUninstall
|
||||||
|
Call AbortInstall
|
||||||
|
; Only reached through Yes: AbortInstall does not return.
|
||||||
|
guard_end:
|
||||||
|
!insertmacro CockLog "guard: ending the payload programs on request, then checking again"
|
||||||
|
Call EndHolders
|
||||||
|
; Measure once and report, rather than asking the same question again. Everything the
|
||||||
|
; payload installs was ended, so what survived is a program this installer may not
|
||||||
|
; open or does not own - another dialog offering to end it again would only repeat
|
||||||
|
; itself.
|
||||||
|
Call RunHolderProbe
|
||||||
|
${If} $GuardExit == "0"
|
||||||
|
!insertmacro CockLog "guard: nothing is holding the payload"
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
!insertmacro CockLog "guard: still in use after ending them ($GuardExit): $GuardOutput"
|
||||||
|
${If} $GuardExit == "1"
|
||||||
|
StrCpy $AbortReason \
|
||||||
|
"The installation was cancelled because these programs still hold files of the previous version and could not be ended:$\r$\n$\r$\n$GuardOutput$\r$\n$\r$\n${AFTER_CLOSING_GUIDANCE}$\r$\n$\r$\nClose them and run the installer again."
|
||||||
|
${Else}
|
||||||
|
StrCpy $AbortReason \
|
||||||
|
"The installation was cancelled because, after ending Cockatrice, Oracle and Servatrice, the installer could not check whether they still hold files of the previous version.$\r$\n$\r$\nThe check reported: $GuardOutput$\r$\n$\r$\nClose them and run the installer again."
|
||||||
|
${EndIf}
|
||||||
|
Call RestoreUserDataAfterUninstall
|
||||||
|
Call AbortInstall
|
||||||
|
${EndIf}
|
||||||
|
; Not an answer at all: the probe was not configured, the Restart Manager
|
||||||
|
; call failed and no payload program runs from the directory, a payload
|
||||||
|
; program refuses to be replaced but no lookup could name it (it may run
|
||||||
|
; under another account), or the probe timed out. Carrying on from that is a
|
||||||
|
; decision, so it is asked for rather than assumed - the stale-runtime check
|
||||||
|
; after the purge still stands between a failed guard and a mismatched
|
||||||
|
; install, but it only fires after files have been replaced, and on an
|
||||||
|
; upgrade the launcher asked for nobody is watching the installer's exit
|
||||||
|
; code. Retry asks the probe again (a Restart Manager that was busy answers
|
||||||
|
; the second time, and a program the user has closed frees its file), Ignore
|
||||||
|
; installs anyway, Abort cancels instead of writing a payload next to files
|
||||||
|
; the guard could not rule out.
|
||||||
|
!insertmacro CockLog "guard: the holder probe could not answer ($GuardExit): $GuardOutput"
|
||||||
|
DetailPrint "Could not confirm whether Cockatrice is still running: $GuardOutput"
|
||||||
|
MessageBox MB_ABORTRETRYIGNORE|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
|
||||||
|
"This installer could not confirm whether Cockatrice, Oracle or Servatrice is still running.$\r$\n$\r$\nThe check reported: $GuardOutput$\r$\n$\r$\nClose them, then choose Retry to check again. Choose Ignore to install anyway, or Abort to cancel the installation." \
|
||||||
|
IDRETRY guard_check IDABORT guard_unknown_abort
|
||||||
|
!insertmacro CockLog "guard: continuing without an answer from the probe"
|
||||||
|
Return
|
||||||
|
guard_unknown_abort:
|
||||||
|
StrCpy $AbortReason \
|
||||||
|
"The installation was cancelled because it could not confirm whether Cockatrice, Oracle or Servatrice is still running.$\r$\n$\r$\nThe check reported: $GuardOutput$\r$\n$\r$\nPlease close them and run the installer again."
|
||||||
|
; Same reason as the cancel path above: this abort can be reached from the
|
||||||
|
; leftover-removal guard, after the backup was taken.
|
||||||
|
Call RestoreUserDataAfterUninstall
|
||||||
|
Call AbortInstall
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Function EnsureAppsNotRunning
|
||||||
|
; guards the install directory
|
||||||
|
StrCpy $GuardDir $INSTDIR
|
||||||
|
Call GuardAppsNotRunning
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; usage: set $GuardDir and $WaitPid, then call this. Asks the one process the installer
|
||||||
|
; waits on to close and sets $R3 to what it could do: 1 = a window exists and was asked to
|
||||||
|
; close (or is waiting on a question of its own), 4 = it has no window a user could answer,
|
||||||
|
; anything else = the probe could not answer. Only 4 lets the wait act on its own; every
|
||||||
|
; other code keeps the user in the decision, because the installer holds a live handle and
|
||||||
|
; "the probe did not find it" is not the same answer as "it is gone".
|
||||||
|
;
|
||||||
|
; The guard's CloseHolders answers the same question for every holder and leaves a
|
||||||
|
; windowless instance to the user, which is right there: the prompt names it. The launcher
|
||||||
|
; is the one process nobody can be asked about - the in-app updater runs the installer
|
||||||
|
; silent and detached, and a client that has already hidden its window has answered its
|
||||||
|
; own prompts - so this answer has to say whether there was a window to ask at all.
|
||||||
|
Function CloseLauncher
|
||||||
|
Call ExtractHolderProbe
|
||||||
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
||||||
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_LAUNCHER_PID", "$WaitPid")'
|
||||||
|
; The probe waits 30 s itself when it posts a close. The timeout here only bounds a
|
||||||
|
; probe that never returns; the Restart Manager call the other modes make is not
|
||||||
|
; reached in this one.
|
||||||
|
nsExec::ExecToStack /TIMEOUT=40000 '"$PLUGINSDIR\installer-holder-probe.exe" --close-launcher'
|
||||||
|
Pop $R3
|
||||||
|
Pop $R4
|
||||||
|
Push "close-launcher: probe exit=$R3 [$R4]"
|
||||||
|
Call CockLogWrite
|
||||||
|
DetailPrint "Closing Cockatrice: $R4"
|
||||||
|
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 "nothing is holding them"
|
||||||
|
; obtained from file handles 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 process enumeration at all, and is unaffected by a process the holder
|
||||||
|
; probe cannot inspect.
|
||||||
|
;
|
||||||
|
; 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 holder probe.
|
||||||
|
;
|
||||||
|
; A launcher that survives the first grace is asked to close by the probe, and the
|
||||||
|
; probe also answers whether there was a window a close could reach. A client hides
|
||||||
|
; its window the moment its own close is accepted, so a launcher without one has
|
||||||
|
; already answered its own prompts and is stuck in shutdown: no dialog can reach it
|
||||||
|
; there, and the wait ends it after one more grace instead of asking the user a
|
||||||
|
; question whose "keep waiting" answer can never change anything.
|
||||||
|
Function WaitForLauncherToExit
|
||||||
|
StrCpy $LauncherExited 0
|
||||||
|
StrCpy $WaitWindowless 0
|
||||||
|
!insertmacro CockLog "wait-launcher: pid='$WaitPid'"
|
||||||
|
${If} $WaitPid == ""
|
||||||
|
!insertmacro CockLog "wait-launcher: no pid given, falling back to name check"
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
; System.dll maps its output registers so that .rN writes $N and .RN writes
|
||||||
|
; $RN. The caller reads $R0/$R1, so the uppercase .RN form is required; the
|
||||||
|
; original .r0/.r1 wrote $0/$1 and left $R1 empty, which made the "did not
|
||||||
|
; exit" test below compare "" against "0" and abort on every /PID= upgrade.
|
||||||
|
; Reopened on every round below, so a launcher that exited in the meantime answers
|
||||||
|
; "already exited" instead of being waited on again.
|
||||||
|
;
|
||||||
|
; PROCESS_TERMINATE rides along with PROCESS_SYNCHRONIZE so that the "end it"
|
||||||
|
; answer below acts on this very handle: the pid looked up again after a dialog
|
||||||
|
; that can stay open as long as the user takes would name whatever process holds
|
||||||
|
; that pid by then, and a handle cannot be handed to another process.
|
||||||
|
;
|
||||||
|
; When those rights are refused - a launcher started by another account, a
|
||||||
|
; protected process - the wait still has to happen, so the ask is repeated with
|
||||||
|
; synchronize access alone and $WaitCanEnd records that ending is not on offer.
|
||||||
|
; Reporting "already exited" instead would be the worse answer: the payload would
|
||||||
|
; be written over a live process, which is what this wait exists to prevent.
|
||||||
|
;
|
||||||
|
; Ten seconds, not the full grace: a client that started an update exits in well
|
||||||
|
; under a second, so at ten seconds it is either flushing a card database or - far
|
||||||
|
; more often - sitting on its own "quit anyway?" confirmation with nobody looking at
|
||||||
|
; it. Silence for a minute is what makes that read as a hung installer. The full
|
||||||
|
; grace is spent after the first question, so a user who answers Cockatrice's
|
||||||
|
; confirmation is not asked again while they do it.
|
||||||
|
StrCpy $WaitSlice 10000
|
||||||
|
wait_launcher_open:
|
||||||
|
ClearErrors
|
||||||
|
StrCpy $WaitCanEnd 1
|
||||||
|
System::Call 'kernel32::OpenProcess(i 0x00100001, i 0, i $WaitPid) p.R0'
|
||||||
|
; OpenProcess returns a null handle for a process that is already gone. The
|
||||||
|
; System plugin does not raise the NSIS error flag from a called function's
|
||||||
|
; return value, so ${Errors} alone never saw that case and the null handle was
|
||||||
|
; passed straight to WaitForSingleObject. Fold a plugin failure into the same
|
||||||
|
; null-handle case instead of reading a handle that was never written.
|
||||||
|
${If} ${Errors}
|
||||||
|
StrCpy $R0 0
|
||||||
|
${EndIf}
|
||||||
|
${If} $R0 == 0
|
||||||
|
StrCpy $WaitCanEnd 0
|
||||||
|
ClearErrors
|
||||||
|
System::Call 'kernel32::OpenProcess(i 0x00100000, i 0, i $WaitPid) p.R0'
|
||||||
|
${If} ${Errors}
|
||||||
|
StrCpy $R0 0
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
${If} $R0 == 0
|
||||||
|
; The launcher is gone - OpenProcess cannot open a dead process - which is
|
||||||
|
; exactly the state the wait was for. A handle refused for want of the terminate
|
||||||
|
; right is not this case; the synchronize-only ask above covers it.
|
||||||
|
!insertmacro CockLog "wait-launcher: already exited"
|
||||||
|
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 ..."
|
||||||
|
; WAIT_OBJECT_0 is 0; WAIT_TIMEOUT is 258 and a failed wait reports 0xFFFFFFFF.
|
||||||
|
System::Call 'kernel32::WaitForSingleObject(p R0, i $WaitSlice) i.R1'
|
||||||
|
${If} $R1 != 0
|
||||||
|
; $R0 is deliberately still open: it is the process that timed out, and the answer
|
||||||
|
; below has to act on that process and not on whatever $WaitPid names by then.
|
||||||
|
!insertmacro CockLog "wait-launcher: did not exit within $WaitSlice ms (wait=$R1, canEnd=$WaitCanEnd)"
|
||||||
|
DetailPrint "The application that started this installer did not exit in time."
|
||||||
|
; Ask the launcher itself before asking the user. A client that has already hidden
|
||||||
|
; its window has answered its own prompts and is stuck in shutdown: there is nothing
|
||||||
|
; left a dialog could reach, and the "ask it to close again" answer the dialog used
|
||||||
|
; to offer could only repeat itself until the user cancelled.
|
||||||
|
StrCpy $GuardDir $INSTDIR
|
||||||
|
Call CloseLauncher
|
||||||
|
; $R3 = what the probe could do (see CloseLauncher), $R4 = its line, already logged.
|
||||||
|
; A process that answered the close during the probe's own wait is gone by now.
|
||||||
|
System::Call 'kernel32::WaitForSingleObject(p R0, i 0) i.R1'
|
||||||
|
${If} $R1 == 0
|
||||||
|
System::Call 'kernel32::CloseHandle(p R0)'
|
||||||
|
StrCpy $R0 0
|
||||||
|
Goto wait_launcher_open
|
||||||
|
${EndIf}
|
||||||
|
${If} $R3 == "4"
|
||||||
|
; Nothing a user could answer: the window is gone, so the launcher is past its
|
||||||
|
; own prompts and stuck in shutdown. It cannot be asked, and it is not going to
|
||||||
|
; ask the user anything either, so the dialog below would be unanswerable.
|
||||||
|
${If} $WaitCanEnd == 1
|
||||||
|
${If} $WaitWindowless == 0
|
||||||
|
; Hiding the window is what the client does when its close is accepted,
|
||||||
|
; so what remains is the shutdown it already started: one full grace for
|
||||||
|
; that, without a question nobody could act on.
|
||||||
|
StrCpy $WaitWindowless 1
|
||||||
|
StrCpy $WaitSlice 60000
|
||||||
|
!insertmacro CockLog "wait-launcher: no window to ask, giving it one more minute"
|
||||||
|
System::Call 'kernel32::CloseHandle(p R0)'
|
||||||
|
StrCpy $R0 0
|
||||||
|
Goto wait_launcher_open
|
||||||
|
${EndIf}
|
||||||
|
${If} $WaitWindowless == 1
|
||||||
|
; It did not use that minute either. Ending it is what the dialog's Yes
|
||||||
|
; answer would have done, and there is no question left to put to the
|
||||||
|
; user: the instance lost its window before this round started.
|
||||||
|
StrCpy $WaitWindowless 2
|
||||||
|
!insertmacro CockLog "wait-launcher: still no window to ask; ending the launcher"
|
||||||
|
DetailPrint "The application that started this installer could not be asked to close; ending it."
|
||||||
|
System::Call 'kernel32::TerminateProcess(p R0, i 1) i.R2'
|
||||||
|
!insertmacro CockLog "wait-launcher: ending the launcher returned $R2"
|
||||||
|
System::Call 'kernel32::CloseHandle(p R0)'
|
||||||
|
StrCpy $R0 0
|
||||||
|
Goto wait_launcher_open
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
; A window exists, or the launcher cannot be ended from here: the three answers
|
||||||
|
; below. "Carry on anyway" is not one of them - a live process has its image and its
|
||||||
|
; Qt DLLs mapped, so writing the payload over them is the broken install this wait
|
||||||
|
; exists to prevent. Yes ends the launcher, Cancel cancels, and No keeps waiting:
|
||||||
|
; the close and the bring-forward just happened above.
|
||||||
|
MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON2 \
|
||||||
|
"Cockatrice has not finished closing, so the installation is paused rather than replace files it is still using.$\r$\n$\r$\nIt may be waiting for you to answer its own question about an unsaved deck, an open game or a card database update.$\r$\n$\r$\nChoose Yes to end it now - an unsaved deck, a running game or an unwritten setting is lost.$\r$\n$\r$\nChoose No to keep waiting, or Cancel to cancel the installation." \
|
||||||
|
IDYES wait_launcher_end IDNO wait_launcher_retry
|
||||||
|
!insertmacro CockLog "wait-launcher: cancelled by user"
|
||||||
|
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
|
||||||
|
; Only reached through Yes or No: AbortInstall does not return.
|
||||||
|
wait_launcher_end:
|
||||||
|
; End it through the handle waited on above, and through nothing else: no pid is
|
||||||
|
; looked up again, so a process that inherited the number in the meantime - or a
|
||||||
|
; /PID= that was not the launcher to begin with - cannot be hit by this.
|
||||||
|
!insertmacro CockLog "wait-launcher: ending the launcher on request"
|
||||||
|
${If} $WaitCanEnd == 0
|
||||||
|
!insertmacro CockLog "wait-launcher: the launcher could not be opened to end it, asking it to close instead"
|
||||||
|
Goto wait_launcher_retry
|
||||||
|
${EndIf}
|
||||||
|
System::Call 'kernel32::TerminateProcess(p R0, i 1) i.R3'
|
||||||
|
System::Call 'kernel32::CloseHandle(p R0)'
|
||||||
|
StrCpy $R0 0
|
||||||
|
!insertmacro CockLog "wait-launcher: ending the launcher returned $R3"
|
||||||
|
Goto wait_launcher_open
|
||||||
|
wait_launcher_retry:
|
||||||
|
System::Call 'kernel32::CloseHandle(p R0)'
|
||||||
|
StrCpy $R0 0
|
||||||
|
; The question has been asked and answered once; from here the wait is the full
|
||||||
|
; grace again, so a slow flush or a confirmation being answered does not turn
|
||||||
|
; into a stream of prompts. The close itself and the bring-forward already
|
||||||
|
; happened just before the dialog, so this only waits. The windowless grace starts
|
||||||
|
; over as well: "keep waiting" must not be answered by ending the process a minute
|
||||||
|
; later without asking again.
|
||||||
|
StrCpy $WaitWindowless 0
|
||||||
|
StrCpy $WaitSlice 60000
|
||||||
|
Goto wait_launcher_open
|
||||||
|
${EndIf}
|
||||||
|
System::Call 'kernel32::CloseHandle(p R0)'
|
||||||
|
StrCpy $R0 0
|
||||||
|
!insertmacro CockLog "wait-launcher: launcher exited"
|
||||||
|
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
|
||||||
|
|
||||||
|
; Uninstaller copy of the guard (the uninstaller gets its own function set compiled in, it
|
||||||
|
; cannot call the installer functions). ExtractHolderProbe and RunHolderProbe come from
|
||||||
|
; CockHolderProbeFunctions above; only the policy differs.
|
||||||
|
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
|
||||||
|
StrCpy $GuardCloseRounds 0
|
||||||
|
StrCpy $GuardRecheckCount 0
|
||||||
|
StrCpy $GuardLastHolders ""
|
||||||
|
un_guard_check:
|
||||||
|
!insertmacro CockLogUn "uninst-guard: checking $GuardDir"
|
||||||
|
Call un.RunHolderProbe
|
||||||
|
${If} $GuardExit == "0"
|
||||||
|
!insertmacro CockLogUn "uninst-guard: nothing is holding the payload"
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
${If} $GuardExit == "1"
|
||||||
|
!insertmacro CockLogUn "uninst-guard: still in use by $GuardOutput"
|
||||||
|
; Same convenience as in the installer: ask every instance to close, repeat only
|
||||||
|
; while the set changed, then report what the close could not reach.
|
||||||
|
${If} $GuardCloseRounds < 2
|
||||||
|
${AndIf} $GuardOutput != $GuardLastHolders
|
||||||
|
StrCpy $GuardLastHolders $GuardOutput
|
||||||
|
IntOp $GuardCloseRounds $GuardCloseRounds + 1
|
||||||
|
!insertmacro CockLogUn "uninst-guard: asking the payload programs to close (round $GuardCloseRounds)"
|
||||||
|
Call un.CloseHolders
|
||||||
|
Goto un_guard_check
|
||||||
|
${EndIf}
|
||||||
|
DetailPrint "The install directory is in use by: $GuardOutput"
|
||||||
|
; Same question and the same three answers as the installer's guard: force close,
|
||||||
|
; close by hand and check again, or abort. CockEndRecheckPrompt words it, so the two
|
||||||
|
; copies promise the same thing.
|
||||||
|
!insertmacro CockEndRecheckPrompt "Cockatrice, Oracle or Servatrice is still using files of the installation:" $GuardOutput "uninstallation"
|
||||||
|
MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON2 "$GuardPrompt" IDYES un_guard_end IDNO un_guard_recheck
|
||||||
|
!insertmacro CockLogUn "uninst-guard: cancelled by user after $GuardRecheckCount recheck(s)"
|
||||||
|
; The installer's Cancel lands in AbortInstall, which explains what to do next. The
|
||||||
|
; uninstaller has no such dialog, so say the same thing here: a bare Abort would
|
||||||
|
; close the window and leave the user with no reason and no next step.
|
||||||
|
MessageBox MB_OK|MB_ICONEXCLAMATION \
|
||||||
|
"The uninstallation was cancelled because Cockatrice, Oracle or Servatrice is still running and holding files of the installation.$\r$\n$\r$\n${AFTER_CLOSING_GUIDANCE}$\r$\n$\r$\nClose them and run the uninstaller again."
|
||||||
|
SetErrorLevel 1
|
||||||
|
Abort
|
||||||
|
un_guard_recheck:
|
||||||
|
IntOp $GuardRecheckCount $GuardRecheckCount + 1
|
||||||
|
!insertmacro CockLogUn "uninst-guard: user closed programs by hand, checking again (recheck $GuardRecheckCount)"
|
||||||
|
StrCpy $GuardCloseRounds 0
|
||||||
|
StrCpy $GuardLastHolders ""
|
||||||
|
Goto un_guard_check
|
||||||
|
; Only reached through Yes: Abort does not return.
|
||||||
|
un_guard_end:
|
||||||
|
!insertmacro CockLogUn "uninst-guard: ending the payload programs on request, then checking again"
|
||||||
|
Call un.EndHolders
|
||||||
|
; One measurement and a stop, not another question: what survived a terminate is a
|
||||||
|
; program this uninstaller may not open or does not own, and ending it again cannot
|
||||||
|
; change that.
|
||||||
|
Call un.RunHolderProbe
|
||||||
|
${If} $GuardExit == "0"
|
||||||
|
!insertmacro CockLogUn "uninst-guard: nothing is holding the payload"
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
!insertmacro CockLogUn "uninst-guard: still in use after ending them ($GuardExit): $GuardOutput"
|
||||||
|
SetErrorLevel 1
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
; Same reasoning as in the installer: without an answer from the probe we cannot tell
|
||||||
|
; whether a running instance still holds file locks, and refusing outright would leave
|
||||||
|
; the user with an uninstaller that never completes. Same three answers too, so an
|
||||||
|
; uninstall that hits a Restart Manager that was busy, or a payload program the user has
|
||||||
|
; not closed yet, can simply be retried.
|
||||||
|
!insertmacro CockLogUn "uninst-guard: the holder probe could not answer ($GuardExit): $GuardOutput"
|
||||||
|
DetailPrint "Could not confirm whether Cockatrice is still running: $GuardOutput"
|
||||||
|
MessageBox MB_ABORTRETRYIGNORE|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
|
||||||
|
"This uninstaller could not confirm whether Cockatrice, Oracle or Servatrice is still running.$\r$\n$\r$\nThe check reported: $GuardOutput$\r$\n$\r$\nClose them, then choose Retry to check again. Choose Ignore to uninstall anyway, or Abort to cancel the uninstallation." \
|
||||||
|
IDRETRY un_guard_check IDABORT un_guard_unknown_abort
|
||||||
|
!insertmacro CockLogUn "uninst-guard: continuing without an answer from the probe"
|
||||||
|
Return
|
||||||
|
un_guard_unknown_abort:
|
||||||
|
!insertmacro CockLogUn "uninst-guard: cancelled after the probe could not answer"
|
||||||
|
SetErrorLevel 1
|
||||||
|
Abort
|
||||||
|
FunctionEnd
|
||||||
439
cmake/nsis/runtime-purge.nsh
Normal file
439
cmake/nsis/runtime-purge.nsh
Normal file
|
|
@ -0,0 +1,439 @@
|
||||||
|
; Stale runtime purge and its diagnostics: which leftovers make an install
|
||||||
|
; unusable, the retry loop that removes them, the read-only clearing that has to
|
||||||
|
; happen first, and the Restart Manager probe that names whatever survived.
|
||||||
|
;
|
||||||
|
; Everything here runs between the running-instance guard and the payload write,
|
||||||
|
; and none of it aborts on its own: the template decides what to do with the
|
||||||
|
; verdict.
|
||||||
|
;
|
||||||
|
; Register contract:
|
||||||
|
; $R0 out: RetryDllPurge/HasStaleRuntimeFiles/IsExistingCockatriceInstall
|
||||||
|
; verdict (1 = leftovers or an existing install, 0 = neither)
|
||||||
|
; $R5 internal: RetryDllPurge retry counter
|
||||||
|
; $R6-$R8 internal: ClearReadOnlyTree recursion, saved and restored
|
||||||
|
; $HolderExit/$HolderOutput/$HolderFound
|
||||||
|
; out: LogProcessesUsingInstallDir - exit code, the one-line summary
|
||||||
|
; the dialogs show, and 1 when it named a holder
|
||||||
|
; $PLUGINSDIR\installer-holder-probe.exe
|
||||||
|
; built by tools/installer-holder-probe and embedded under the same
|
||||||
|
; relative path NSIS.definitions.nsh resolves to the build root
|
||||||
|
|
||||||
|
; Sets $R0 to 1 if $INSTDIR looks like an existing Cockatrice install. Used to
|
||||||
|
; 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. Qt6*.dll rather than *.dll on purpose: this function decides
|
||||||
|
; whether a directory is ours, and a folder that merely holds some DLLs of
|
||||||
|
; its own must not be purged. Qt6*.dll is enough even when only the protobuf
|
||||||
|
; runtime survived, because the whole Qt set is mapped by the same process
|
||||||
|
; that maps abseil_dll.dll and so is locked or free with it.
|
||||||
|
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.
|
||||||
|
;
|
||||||
|
; Any DLL at all counts, not a list of expected ones. The purge removes every
|
||||||
|
; top-level DLL, so after it has run the only DLL that can still be there is one
|
||||||
|
; it failed to delete, whatever its name. A mask per library family (Qt, then
|
||||||
|
; OpenSSL, then zlib) is what let abseil_dll.dll through: it is in neither the
|
||||||
|
; purge nor this check, so it survived a 3.0.2 to 3.1 update untouched, the new
|
||||||
|
; executables were written next to it, and the client then failed to start with
|
||||||
|
; "abseil_dll.dll is either not designed to run on Windows or it contains an
|
||||||
|
; error". Probing *.dll has no list to fall out of date when the next
|
||||||
|
; transitive dependency appears.
|
||||||
|
Function HasStaleRuntimeFiles
|
||||||
|
StrCpy $R0 0
|
||||||
|
StrCpy $R3 "*.dll"
|
||||||
|
Call CheckStaleRuntimeMask
|
||||||
|
; IfFileExists takes wildcards, so a Plugins directory the purge could not
|
||||||
|
; remove counts as a leftover too.
|
||||||
|
${If} ${FileExists} "$INSTDIR\Plugins\*.*"
|
||||||
|
!insertmacro CockLog "stale-runtime: Plugins directory survived the purge"
|
||||||
|
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 != ""
|
||||||
|
!insertmacro CockLog "stale-runtime: '$StaleFindName' matched $R3 in $INSTDIR"
|
||||||
|
StrCpy $R0 1
|
||||||
|
FindClose $StaleFindHandle
|
||||||
|
${EndIf}
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; Clears the read-only attribute of everything the purge is about to delete.
|
||||||
|
;
|
||||||
|
; Delete and RMDir only set the error flag: a read-only file is left on disk
|
||||||
|
; untouched, and the retry loop then cannot make progress however long it waits,
|
||||||
|
; because the file is not held - it is protected. The abort diagnostics have a
|
||||||
|
; branch for exactly that ("read-only, which stops Delete"), so a survivor that
|
||||||
|
; is both free and read-only was a dead end until now. This runs on every
|
||||||
|
; attempt, so an attribute set again between attempts is cleared as well.
|
||||||
|
; SetFileAttributes has no wildcard support, hence the walks.
|
||||||
|
Function ClearReadOnlyPurgeTargets
|
||||||
|
FindFirst $StaleFindHandle $StaleFindName "$INSTDIR\*.dll"
|
||||||
|
${If} $StaleFindHandle != ""
|
||||||
|
crp_dll_loop:
|
||||||
|
SetFileAttributes "$INSTDIR\$StaleFindName" NORMAL
|
||||||
|
ClearErrors
|
||||||
|
FindNext $StaleFindHandle $StaleFindName
|
||||||
|
IfErrors 0 crp_dll_loop
|
||||||
|
FindClose $StaleFindHandle
|
||||||
|
${EndIf}
|
||||||
|
StrCpy $R6 "$INSTDIR\Plugins"
|
||||||
|
Call ClearReadOnlyTree
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; usage: set $R6 to a directory. Clears the read-only attribute of every entry
|
||||||
|
; below it, so RMDir /r is not stopped by a single file. Saves and restores the
|
||||||
|
; registers it works in, because it recurses into itself.
|
||||||
|
Function ClearReadOnlyTree
|
||||||
|
Push $R6
|
||||||
|
Push $R7
|
||||||
|
Push $R8
|
||||||
|
FindFirst $R7 $R8 "$R6\*.*"
|
||||||
|
${If} $R7 != ""
|
||||||
|
crt_loop:
|
||||||
|
${If} $R8 != "."
|
||||||
|
${AndIf} $R8 != ".."
|
||||||
|
SetFileAttributes "$R6\$R8" NORMAL
|
||||||
|
; Only an entry with contents has anything below it that the purge
|
||||||
|
; would fail on; an empty directory's own attribute is already
|
||||||
|
; cleared above and there is nothing left to descend into.
|
||||||
|
${If} ${FileExists} "$R6\$R8\*.*"
|
||||||
|
Push $R6
|
||||||
|
StrCpy $R6 "$R6\$R8"
|
||||||
|
Call ClearReadOnlyTree
|
||||||
|
Pop $R6
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
ClearErrors
|
||||||
|
FindNext $R7 $R8
|
||||||
|
IfErrors 0 crt_loop
|
||||||
|
FindClose $R7
|
||||||
|
${EndIf}
|
||||||
|
Pop $R8
|
||||||
|
Pop $R7
|
||||||
|
Pop $R6
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; Removes the Plugins directory and all top-level DLLs from $INSTDIR, then
|
||||||
|
; retries up to two more times (2 s apart) when files remain locked, so that
|
||||||
|
; transient holders such as antivirus scanners have a chance to release their
|
||||||
|
; handles before the installer gives up. Three attempts total. The read-only
|
||||||
|
; attribute is cleared before each attempt: a protected file is not a held one
|
||||||
|
; and would never clear on its own.
|
||||||
|
; Sets $R0 to 0 when the purge is clean, or to 1 when stale DLLs remain
|
||||||
|
; after all attempts.
|
||||||
|
Function RetryDllPurge
|
||||||
|
StrCpy $R5 0 ; counts retries after the initial attempt
|
||||||
|
rdp_loop:
|
||||||
|
Call ClearReadOnlyPurgeTargets
|
||||||
|
RMDir /r "$INSTDIR\Plugins"
|
||||||
|
Delete "$INSTDIR\*.dll"
|
||||||
|
Call HasStaleRuntimeFiles
|
||||||
|
; $R0 = 0: all clear, nothing left to do.
|
||||||
|
${If} $R0 = 0
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
IntOp $R5 $R5 + 1
|
||||||
|
; Allow two retries after the initial attempt ($R5 is the retry counter,
|
||||||
|
; so the loop runs at most 3 times: attempt + retry 1 + retry 2).
|
||||||
|
${If} $R5 > 2
|
||||||
|
!insertmacro CockLog "RetryDllPurge: stale files remain after 3 attempts, giving up"
|
||||||
|
Return ; $R0 is already 1
|
||||||
|
${EndIf}
|
||||||
|
!insertmacro CockLog "RetryDllPurge: retry $R5 of 2 after stale purge, sleeping 2 s"
|
||||||
|
Sleep 2000
|
||||||
|
Goto rdp_loop
|
||||||
|
FunctionEnd ; RetryDllPurge
|
||||||
|
; Turns a Win32 error code into the wording that goes into the log.
|
||||||
|
; usage: set $0 to the code, result in $R0.
|
||||||
|
;
|
||||||
|
; These are Windows' own names for the codes and deliberately nothing more. A
|
||||||
|
; code on its own cannot say whether an upgrade was blocked by a running instance
|
||||||
|
; or by a permission: deleting a file whose image is mapped by another process
|
||||||
|
; surfaces as ACCESS_DENIED, exactly like a file whose ACL denies delete does.
|
||||||
|
; LogProcessesUsingInstallDir is what separates the two, which is why it runs
|
||||||
|
; right after this and why the two are read together.
|
||||||
|
Function GetWin32ErrorText
|
||||||
|
${If} $0 = 2
|
||||||
|
StrCpy $R0 "already gone"
|
||||||
|
${ElseIf} $0 = 3
|
||||||
|
StrCpy $R0 "path not found"
|
||||||
|
${ElseIf} $0 = 5
|
||||||
|
StrCpy $R0 "access denied"
|
||||||
|
${ElseIf} $0 = 19
|
||||||
|
StrCpy $R0 "write-protected media"
|
||||||
|
${ElseIf} $0 = 32
|
||||||
|
StrCpy $R0 "sharing violation"
|
||||||
|
${ElseIf} $0 = 33
|
||||||
|
StrCpy $R0 "lock violation"
|
||||||
|
${ElseIf} $0 = 1224
|
||||||
|
StrCpy $R0 "file is mapped in memory"
|
||||||
|
${Else}
|
||||||
|
StrCpy $R0 "windows error"
|
||||||
|
${EndIf}
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; Records which runtime files the purge left behind, what Windows said about each
|
||||||
|
; one, and which processes are using the install directory.
|
||||||
|
;
|
||||||
|
; Diagnostic only, and only on the abort path: it deletes nothing, so it can never
|
||||||
|
; turn a failed upgrade into a half-removed old install, and it decides nothing -
|
||||||
|
; the abort that called it is already decided. It exists because NSIS Delete/RMDir
|
||||||
|
; only set the error flag: without this, a report of "files of a previous version
|
||||||
|
; survived the purge" said which files were present but never which one was held,
|
||||||
|
; and never whether the cause was a lock or a permission.
|
||||||
|
;
|
||||||
|
; Each survivor is opened for DELETE access and then asked the question DeleteFile
|
||||||
|
; asks: SetFileInformationByHandle(FileDispositionInfo) marks it for deletion on
|
||||||
|
; close, and the mark is cleared again right away, so nothing is deleted (and if
|
||||||
|
; that cancel ever failed, the file would go when the handle closes - a file the
|
||||||
|
; purge already tried to delete, and the line says so). That separates the three
|
||||||
|
; causes a failed Delete cannot tell apart: another handle that does not share
|
||||||
|
; delete refuses the mark, a read-only attribute refuses it, and a file nobody
|
||||||
|
; holds accepts it and would have been deleted. The mark is read through ?e, so a
|
||||||
|
; refusal reports Windows' own code; clearing LastError first keeps a zero code
|
||||||
|
; meaning "accepted". The wording below reports what Windows said and never claims
|
||||||
|
; a cause the code cannot prove. Which process is holding a survivor, if any, is
|
||||||
|
; what the holders line after the loop answers.
|
||||||
|
Function LogStaleRuntimeDiagnostics
|
||||||
|
FindFirst $StaleFindHandle $StaleFindName "$INSTDIR\*.dll"
|
||||||
|
${If} $StaleFindHandle != ""
|
||||||
|
stale_diag_loop:
|
||||||
|
; ClearErrors tells a plugin failure apart from an API failure: the System
|
||||||
|
; plugin sets the error flag only when it could not make the call at all,
|
||||||
|
; in which case it pushes its own message instead of the call's value and
|
||||||
|
; the pop below must not run.
|
||||||
|
ClearErrors
|
||||||
|
System::Call 'kernel32::SetLastError(i 0)'
|
||||||
|
System::Call 'kernel32::CreateFileW(w "$INSTDIR\$StaleFindName", i 0x00010000, i 0, p 0, i 3, i 0, p 0) p.R1 ?e'
|
||||||
|
${If} ${Errors}
|
||||||
|
StrCpy $R0 "the delete probe could not be run"
|
||||||
|
${Else}
|
||||||
|
Pop $R2
|
||||||
|
${If} $R2 = 0
|
||||||
|
; The open worked, which says nothing about Delete: a DLL a running
|
||||||
|
; process has mapped opens for DELETE and still refuses DeleteFile, and
|
||||||
|
; a read-only file does the same until its attribute is cleared. Ask
|
||||||
|
; Windows the question the purge actually asks - mark the file for
|
||||||
|
; deletion on close, then cancel that again. This is the same request
|
||||||
|
; DeleteFile makes on a temporary handle, so its answer says whether a
|
||||||
|
; delete would succeed now.
|
||||||
|
ClearErrors
|
||||||
|
System::Call '*(&i1 1) p.R4'
|
||||||
|
System::Call 'kernel32::SetFileInformationByHandle(p R1, i 4, p R4, i 1) i.R5 ?e'
|
||||||
|
${If} ${Errors}
|
||||||
|
StrCpy $R0 "a delete-access open now succeeds, but Windows could not be asked whether it would delete it"
|
||||||
|
${Else}
|
||||||
|
Pop $R6
|
||||||
|
${If} $R5 != 0
|
||||||
|
; Accepted: cancel the mark again, so this stays a question and
|
||||||
|
; does not turn into the deletion it is asking about. A cancel
|
||||||
|
; that fails leaves the file to go when the handle closes, which
|
||||||
|
; is what the purge wanted anyway; the line says so.
|
||||||
|
ClearErrors
|
||||||
|
System::Call '*(&i1 0) p.R4'
|
||||||
|
System::Call 'kernel32::SetFileInformationByHandle(p R1, i 4, p R4, i 1) i.R5 ?e'
|
||||||
|
${If} ${Errors}
|
||||||
|
StrCpy $R0 "a delete would succeed now, but the check could not undo the mark it set"
|
||||||
|
${Else}
|
||||||
|
Pop $R6
|
||||||
|
${If} $R5 != 0
|
||||||
|
StrCpy $R0 "a delete would succeed now - the earlier failure was transient"
|
||||||
|
${Else}
|
||||||
|
StrCpy $R0 "a delete would succeed now, and it is marked for deletion from here on"
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
${Else}
|
||||||
|
StrCpy $0 $R6
|
||||||
|
Call GetWin32ErrorText
|
||||||
|
StrCpy $R0 "a delete-access open now succeeds, but Windows refused to mark it for deletion: win32 error $R6: $R0"
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
System::Call 'kernel32::CloseHandle(p R1)'
|
||||||
|
; The read-only attribute is a cause on its own, and the purge clears it
|
||||||
|
; before every attempt, so a survivor that is read-only had it set again.
|
||||||
|
System::Call 'kernel32::GetFileAttributesW(w "$INSTDIR\$StaleFindName") i.R3'
|
||||||
|
${If} $R3 != -1
|
||||||
|
IntOp $R3 $R3 & 1
|
||||||
|
${If} $R3 = 1
|
||||||
|
StrCpy $R0 "$R0; it is read-only again"
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
${Else}
|
||||||
|
StrCpy $0 $R2
|
||||||
|
Call GetWin32ErrorText
|
||||||
|
StrCpy $R0 "win32 error $R2: $R0"
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
!insertmacro CockLog "stale-runtime: '$StaleFindName' is still present: $R0"
|
||||||
|
ClearErrors
|
||||||
|
FindNext $StaleFindHandle $StaleFindName
|
||||||
|
IfErrors 0 stale_diag_loop
|
||||||
|
FindClose $StaleFindHandle
|
||||||
|
${EndIf}
|
||||||
|
Call LogProcessesUsingInstallDir
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; Names the applications and services that still have the payload open or mapped.
|
||||||
|
; Writes one line to the log and sets $HolderFound (1 when anything was named), so the
|
||||||
|
; abort message can repeat that line to the user.
|
||||||
|
;
|
||||||
|
; The lookup is tools/installer-holder-probe, the same executable the guard runs before
|
||||||
|
; anything is replaced; here it answers the other question, about files a purge could not
|
||||||
|
; remove. It asks the Restart Manager (RmRegisterResources/RmGetList), which names a
|
||||||
|
; process holding a plain handle on a file - an antivirus scanner - and names services as
|
||||||
|
; well as user programs, and it adds every payload program running from the directory
|
||||||
|
; itself - not one in a folder below it, which is another application the user installed
|
||||||
|
; there - because a running instance holds its payload as mapped images rather than as open
|
||||||
|
; handles and the Restart Manager does not reliably report those. Its remaining blind spot
|
||||||
|
; is a handle taken in kernel mode, where there is no process to name; a payload executable
|
||||||
|
; among the files then still refuses to be replaced, and the probe answers with that file
|
||||||
|
; instead of naming nothing.
|
||||||
|
;
|
||||||
|
; $INSTDIR travels through the same COCKATRICE_GUARD_DIR environment variable the guard
|
||||||
|
; uses, so a path with spaces or apostrophes never has to be embedded in a command line.
|
||||||
|
Function LogProcessesUsingInstallDir
|
||||||
|
StrCpy $HolderExit 0
|
||||||
|
StrCpy $HolderOutput ""
|
||||||
|
StrCpy $HolderFound 0
|
||||||
|
; The directory that just failed the purge, not whatever a check before the directory
|
||||||
|
; page looked at: a portable install into a custom folder reaches here with the two
|
||||||
|
; being different.
|
||||||
|
StrCpy $GuardDir $INSTDIR
|
||||||
|
Call RunHolderProbe
|
||||||
|
StrCpy $HolderExit $GuardExit
|
||||||
|
StrCpy $HolderOutput $GuardOutput
|
||||||
|
; The exit code, not the text, decides what happened: it cannot be mangled by what
|
||||||
|
; nsExec leaves on the end of the captured output. 1 = the line below names what is
|
||||||
|
; holding the files, 0 = nothing holds them any more, anything else = the probe could
|
||||||
|
; not answer, and says why in its own output.
|
||||||
|
${If} $HolderExit == "1"
|
||||||
|
StrCpy $HolderFound 1
|
||||||
|
${ElseIf} $HolderExit == "0"
|
||||||
|
StrCpy $HolderOutput "nothing is holding them and no payload program is running from the directory"
|
||||||
|
${Else}
|
||||||
|
StrCpy $HolderOutput "the holder probe could not name what holds them: $HolderOutput"
|
||||||
|
${EndIf}
|
||||||
|
!insertmacro CockLog "holders: $HolderOutput"
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; Called after a purge left files behind and LogStaleRuntimeDiagnostics named who is
|
||||||
|
; holding them. Instead of aborting straight away - which on /R strands the user, since
|
||||||
|
; the old version is already uninstalled - it closes once, purges again, and only then
|
||||||
|
; asks: end what is still holding the files, or try the removal again by hand. Sets
|
||||||
|
; $StaleRuntimeAfterPurge to the last purge's verdict, so the caller still aborts when the
|
||||||
|
; files survive.
|
||||||
|
;
|
||||||
|
; The shape is the guard's, deliberately: one automatic attempt at a graceful close, then a
|
||||||
|
; prompt whose Yes ends the payload programs and whose No retries. A client that was still
|
||||||
|
; shutting down releases the files in the automatic round and never sees a question, and an
|
||||||
|
; Oracle or Servatrice - which a close cannot reach - is what the prompt exists for, so the
|
||||||
|
; user is never left with a dialog whose answers cannot free anything.
|
||||||
|
;
|
||||||
|
; The automatic close is a request (WM_CLOSE) and the client keeps the last word: it may be
|
||||||
|
; waiting for an answer to its own "really quit?" confirmation (a running card database
|
||||||
|
; update, an open game, an unsaved deck), and that stays its decision. The instance is
|
||||||
|
; brought forward first, so that confirmation is on screen rather than behind this
|
||||||
|
; installer's prompt.
|
||||||
|
;
|
||||||
|
; Ending is the same last resort the guard offers, with the same cost spelled out, and only
|
||||||
|
; ever the programs the payload installs - see EndHolders.
|
||||||
|
;
|
||||||
|
; Restart Manager's RmShutdown was the alternative and was rejected: it shuts down
|
||||||
|
; services as well as applications, and the only way to bring them back is RmRestart,
|
||||||
|
; which would restart this very application into files that are about to be replaced.
|
||||||
|
;
|
||||||
|
; Always offered, in every mode: a /S install without /R is the only case without someone
|
||||||
|
; at the keyboard, and this installer does not promise unattended runs - if an error needs
|
||||||
|
; a decision, it asks for one. No offer when the probe named nobody: there is nothing to
|
||||||
|
; end, and the abort's per-file diagnostics already record what Windows said.
|
||||||
|
;
|
||||||
|
; One question after the automatic close, and never a loop: Yes ends the programs the
|
||||||
|
; payload installs - and says so, because anything else the probe named is left to the user -
|
||||||
|
; and purges again; the other answer gives up, and the caller's abort names what survived.
|
||||||
|
; The listing is re-read before the question, so it says what is left now rather than
|
||||||
|
; repeating the first one.
|
||||||
|
Function RecoverFromHolders
|
||||||
|
${If} $HolderFound != 1
|
||||||
|
!insertmacro CockLog "recover-holders: no holder was named, nothing to offer"
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
; The automatic attempt, the same one the guard makes before it asks anything.
|
||||||
|
!insertmacro CockLog "recover-holders: closing cockatrice, then trying the removal again"
|
||||||
|
StrCpy $GuardDir $INSTDIR
|
||||||
|
Call CloseHolders
|
||||||
|
Call RetryDllPurge
|
||||||
|
StrCpy $StaleRuntimeAfterPurge $R0
|
||||||
|
!insertmacro CockLogListing "after-retry-purge" "*.dll"
|
||||||
|
!insertmacro CockLogListing "after-retry-plugins" "Plugins\*.*"
|
||||||
|
!insertmacro CockLog "recover-holders: stale-runtime after retry=$StaleRuntimeAfterPurge"
|
||||||
|
${If} $StaleRuntimeAfterPurge = 0
|
||||||
|
DetailPrint "Files of the previous version were removed; continuing the installation."
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
; Still stuck: refresh the names, so the prompt and the log describe what survived the
|
||||||
|
; last attempt and not what was there before it.
|
||||||
|
Call LogStaleRuntimeDiagnostics
|
||||||
|
${If} $HolderFound != 1
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
; One question, the same one the guard asks and with the same two answers. No retry loop:
|
||||||
|
; the purge has already been tried after the close, so the only thing that can still free
|
||||||
|
; the files is ending them, and the caller's abort names whatever survives that.
|
||||||
|
!insertmacro CockEndPrompt "Files of the previous version of Cockatrice are still in use by:" $HolderOutput "installation"
|
||||||
|
MessageBox MB_YESNO|MB_ICONEXCLAMATION|MB_DEFBUTTON2 "$GuardPrompt" IDYES rfh_end
|
||||||
|
!insertmacro CockLog "recover-holders: cancelled by user"
|
||||||
|
Return
|
||||||
|
; Only reached through Yes: Return does not come back here.
|
||||||
|
rfh_end:
|
||||||
|
!insertmacro CockLog "recover-holders: ending the payload programs on request, then trying the removal again"
|
||||||
|
Call EndHolders
|
||||||
|
Call RetryDllPurge
|
||||||
|
StrCpy $StaleRuntimeAfterPurge $R0
|
||||||
|
!insertmacro CockLog "recover-holders: stale-runtime after end=$StaleRuntimeAfterPurge"
|
||||||
|
${If} $StaleRuntimeAfterPurge = 0
|
||||||
|
DetailPrint "Files of the previous version were removed; continuing the installation."
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
; Refresh the names one last time so the abort that follows describes what survived the
|
||||||
|
; end, and then let it stop the run: the same dialog again would only repeat itself.
|
||||||
|
Call LogStaleRuntimeDiagnostics
|
||||||
|
FunctionEnd
|
||||||
492
cmake/nsis/user-data.nsh
Normal file
492
cmake/nsis/user-data.nsh
Normal file
|
|
@ -0,0 +1,492 @@
|
||||||
|
; User data backup and restore around the previous version's uninstaller.
|
||||||
|
;
|
||||||
|
; Kept apart from the upgrade orchestration in the template because all of it is
|
||||||
|
; about the previous version's uninstaller: it waits for that uninstaller's
|
||||||
|
; payload to disappear, removes what it left behind and appends its log, and it
|
||||||
|
; copies the user's data out of $OldInstallDir before the uninstaller runs and
|
||||||
|
; puts it back afterwards. The reasoning for the shared %TEMP% backup directory
|
||||||
|
; and for the copy-not-move relocation is in the comment above each function.
|
||||||
|
;
|
||||||
|
; Registers: $OldUninstallExit/$OldUninstallExe/$OldPayloadRemoved for the
|
||||||
|
; leftover handling, $DataBackupDir, $DataBackupDone, $DataBackupForDir and the
|
||||||
|
; $OldInstallDir/$INSTDIR pair for the backup. Removing leftovers aborts the
|
||||||
|
; install when it cannot free the files; the backup and restore never do.
|
||||||
|
|
||||||
|
; Waits for the previous installation's payload to be gone, so the sections below never
|
||||||
|
; write into a directory an old uninstaller is still emptying.
|
||||||
|
;
|
||||||
|
; ExecWait only waits for as long as the uninstaller runs in place: an NSIS uninstaller
|
||||||
|
; that did not get a usable _?= copies itself to %TEMP%, starts that copy and exits, so
|
||||||
|
; ExecWait returns at once while the copy keeps deleting - and it finishes after this
|
||||||
|
; window has closed, removing the version that was just installed. Waiting for
|
||||||
|
; cockatrice.exe to disappear covers both cases, and stops with a message instead of
|
||||||
|
;
|
||||||
|
; When the payload is still there after the wait, $OldUninstallExit decides: a non-zero
|
||||||
|
; code means the uninstaller was cancelled or failed, and the install aborts saying so.
|
||||||
|
; A zero code means it finished but skipped files someone was holding (NSIS Delete does
|
||||||
|
; that silently), so RemoveOldPayloadLeftovers frees and removes them, and the install
|
||||||
|
; only aborts - naming the holders - when even that fails.
|
||||||
|
; installing over a directory that is still being emptied.
|
||||||
|
Function WaitForOldPayloadToDisappear
|
||||||
|
${If} $OldInstallDir == ""
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
; The registered uninstaller could not be run at all (missing, or ExecWait could not
|
||||||
|
; start it): nothing is going to empty the directory, so waiting is pointless and
|
||||||
|
; aborting as "cancelled" was wrong. Remove the previous payload the same way the
|
||||||
|
; skipped-files recovery below does, and abort - naming why - only if that fails.
|
||||||
|
${If} $OldUninstallExit == "missing"
|
||||||
|
${OrIf} $OldUninstallExit == "launch-failed"
|
||||||
|
${IfNot} ${FileExists} "$OldInstallDir\*.*"
|
||||||
|
!insertmacro CockLog "old uninstall: uninstaller $OldUninstallExit and $OldInstallDir is already gone"
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
!insertmacro CockLog "old uninstall: uninstaller $OldUninstallExit ('$OldUninstallString'); removing $OldInstallDir payload directly"
|
||||||
|
Call AppendUninstallerLog
|
||||||
|
Call RemoveOldPayloadLeftovers
|
||||||
|
${If} $OldPayloadRemoved != "1"
|
||||||
|
StrCpy $AbortReason \
|
||||||
|
"The previous version in $OldInstallDir could not be removed: its uninstaller ($OldUninstallExe) could not be run ($OldUninstallExit), and removing the files directly failed.$\r$\n$\r$\nStill in use by: $HolderOutput$\r$\n$\r$\nClose anything still using those files and run this installer again."
|
||||||
|
Call RestoreUserDataAfterUninstall
|
||||||
|
Call AbortInstall
|
||||||
|
${EndIf}
|
||||||
|
!insertmacro CockLog "old uninstall: continuing after removing the payload of an uninstaller that could not run"
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
StrCpy $R2 0
|
||||||
|
old_uninstall_wait:
|
||||||
|
IfFileExists "$OldInstallDir\cockatrice.exe" 0 old_uninstall_gone
|
||||||
|
IntOp $R2 $R2 + 1
|
||||||
|
${If} $R2 > 60
|
||||||
|
!insertmacro CockLog "old uninstall: $OldInstallDir still holds cockatrice.exe after 30 s (uninstaller exit=$OldUninstallExit)"
|
||||||
|
; The uninstaller's own account of what it skipped or why it stopped lives in its
|
||||||
|
; separate log; copy it here so the report sent from AbortInstall carries it.
|
||||||
|
Call AppendUninstallerLog
|
||||||
|
${If} $OldUninstallExit != "0"
|
||||||
|
; Cancelled or failed with a non-zero exit code (an uninstaller that could not be
|
||||||
|
; started is handled above): the old version was not meant to be removed, so
|
||||||
|
; nothing is deleted on its behalf.
|
||||||
|
StrCpy $AbortReason \
|
||||||
|
"The previous version in $OldInstallDir is still there because its uninstaller was cancelled or failed (exit code '$OldUninstallExit').$\r$\n$\r$\nClose anything still using those files and run this installer again."
|
||||||
|
; The backup was taken before the uninstaller ran; an abort must not leave the
|
||||||
|
; only copy of the user data in %TEMP%, where the next run clears it.
|
||||||
|
Call RestoreUserDataAfterUninstall
|
||||||
|
Call AbortInstall
|
||||||
|
${EndIf}
|
||||||
|
; The uninstaller finished but skipped files - a running instance or a scanner holding
|
||||||
|
; them, which NSIS Delete passes over silently. Free them and remove what is left.
|
||||||
|
Call RemoveOldPayloadLeftovers
|
||||||
|
; The removal reports success only when every payload executable is gone and the
|
||||||
|
; DLL/Plugins purge came back clean; cockatrice.exe alone would hide a locked
|
||||||
|
; oracle.exe or a stale DLL the new payload is about to be written over.
|
||||||
|
${If} $OldPayloadRemoved != "1"
|
||||||
|
StrCpy $AbortReason \
|
||||||
|
"The previous version in $OldInstallDir could not be fully removed after its uninstaller ran.$\r$\n$\r$\nStill in use by: $HolderOutput$\r$\n$\r$\nClose anything still using those files and run this installer again."
|
||||||
|
Call RestoreUserDataAfterUninstall
|
||||||
|
Call AbortInstall
|
||||||
|
${EndIf}
|
||||||
|
; The payload is gone and ExecWait already waited for the uninstaller; keep the
|
||||||
|
; short settle the other paths use, then hand back to the caller's restore.
|
||||||
|
Sleep 1000
|
||||||
|
!insertmacro CockLog "old uninstall: continuing after removing the leftovers the uninstaller skipped"
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
Sleep 500
|
||||||
|
Goto old_uninstall_wait
|
||||||
|
old_uninstall_gone:
|
||||||
|
${If} $R2 > 0
|
||||||
|
; ExecWait returned while the old payload was still there: that uninstaller
|
||||||
|
; copied itself to %TEMP%, and the copy is still working through its file list.
|
||||||
|
; cockatrice.exe is gone before the rest of it, so let the copy settle before
|
||||||
|
; the sections below start writing files it would otherwise remove.
|
||||||
|
Sleep 1000
|
||||||
|
!insertmacro CockLog "old uninstall: ExecWait returned early, settled 1 s after $R2 * 500 ms"
|
||||||
|
${Else}
|
||||||
|
!insertmacro CockLog "old uninstall: $OldInstallDir was already clear when ExecWait returned"
|
||||||
|
${EndIf}
|
||||||
|
Return
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; Called when the old uninstaller exited successfully but left its payload behind, or
|
||||||
|
; could not be run at all (missing or not startable) and the payload is still there.
|
||||||
|
; Frees the files the same way the guard does (close, then offer to end), deletes what
|
||||||
|
; the uninstaller skipped, and on failure records which files survived and who holds
|
||||||
|
; them in $HolderOutput, for the abort message. Sets $OldPayloadRemoved to 1 only when
|
||||||
|
; every payload executable is gone and the DLL/Plugins purge is clean.
|
||||||
|
Function RemoveOldPayloadLeftovers
|
||||||
|
!insertmacro CockLog "old uninstall: removing leftovers of $OldInstallDir"
|
||||||
|
StrCpy $OldPayloadRemoved 0
|
||||||
|
StrCpy $HolderOutput "nothing could be named"
|
||||||
|
StrCpy $GuardDir $OldInstallDir
|
||||||
|
Call GuardAppsNotRunning
|
||||||
|
; The purge helpers work on $INSTDIR; point them at the old directory for the
|
||||||
|
; duration (on /R the two are the same anyway).
|
||||||
|
Push $INSTDIR
|
||||||
|
StrCpy $INSTDIR $OldInstallDir
|
||||||
|
StrCpy $R2 0
|
||||||
|
rop_loop:
|
||||||
|
SetFileAttributes "$INSTDIR\cockatrice.exe" NORMAL
|
||||||
|
SetFileAttributes "$INSTDIR\oracle.exe" NORMAL
|
||||||
|
SetFileAttributes "$INSTDIR\servatrice.exe" NORMAL
|
||||||
|
Delete "$INSTDIR\cockatrice.exe"
|
||||||
|
Delete "$INSTDIR\oracle.exe"
|
||||||
|
Delete "$INSTDIR\servatrice.exe"
|
||||||
|
Delete "$INSTDIR\qt.conf"
|
||||||
|
Call RetryDllPurge
|
||||||
|
; $R0 is RetryDllPurge's verdict (1 = a DLL or the Plugins directory survived).
|
||||||
|
; Every payload executable has to be gone as well: a locked oracle.exe or
|
||||||
|
; servatrice.exe is just as fatal to the install that follows as cockatrice.exe.
|
||||||
|
IfFileExists "$INSTDIR\cockatrice.exe" rop_retry
|
||||||
|
IfFileExists "$INSTDIR\oracle.exe" rop_retry
|
||||||
|
IfFileExists "$INSTDIR\servatrice.exe" rop_retry
|
||||||
|
${If} $R0 == 0
|
||||||
|
Goto rop_done
|
||||||
|
${EndIf}
|
||||||
|
rop_retry:
|
||||||
|
IntOp $R2 $R2 + 1
|
||||||
|
${If} $R2 < 3
|
||||||
|
!insertmacro CockLog "old uninstall: leftovers of $OldInstallDir survived removal attempt $R2, retrying in 2 s"
|
||||||
|
Sleep 2000
|
||||||
|
Goto rop_loop
|
||||||
|
${EndIf}
|
||||||
|
!insertmacro CockLog "old uninstall: leftovers of $OldInstallDir still present after 3 removal attempts"
|
||||||
|
!insertmacro CockLogListing "old-leftovers" "*.*"
|
||||||
|
; Per-file Windows codes for the DLLs plus the holder probe's line ($HolderOutput).
|
||||||
|
Call LogStaleRuntimeDiagnostics
|
||||||
|
Pop $INSTDIR
|
||||||
|
Return
|
||||||
|
rop_done:
|
||||||
|
StrCpy $OldPayloadRemoved 1
|
||||||
|
; Only now that the recovery has succeeded: deleting the old uninstaller on a
|
||||||
|
; failed attempt would leave a directory whose UninstallString points at nothing,
|
||||||
|
; so no later run could remove it. WriteUninstaller recreates it on success anyway.
|
||||||
|
SetFileAttributes "$INSTDIR\uninstall.exe" NORMAL
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
!insertmacro CockLog "old uninstall: leftovers of $OldInstallDir removed"
|
||||||
|
!insertmacro CockLogListing "old-leftovers-after-removal" "*.*"
|
||||||
|
Pop $INSTDIR
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; Copies the old uninstaller's log ($TEMP\Cockatrice-uninstaller.log) into this
|
||||||
|
; installer's log, so the report AbortInstall sends shows what the uninstaller did.
|
||||||
|
; Uninstallers older than this log simply have no file; that is logged too.
|
||||||
|
Function AppendUninstallerLog
|
||||||
|
ClearErrors
|
||||||
|
FileOpen $UninstLogHandle "$TEMP\Cockatrice-uninstaller.log" r
|
||||||
|
${If} ${Errors}
|
||||||
|
${OrIf} $UninstLogHandle == ""
|
||||||
|
!insertmacro CockLog "uninstaller log: not found at $TEMP\Cockatrice-uninstaller.log"
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
!insertmacro CockLog "uninstaller log begin ----------------------------------------"
|
||||||
|
aul_loop:
|
||||||
|
ClearErrors
|
||||||
|
FileRead $UninstLogHandle $UninstLogLine
|
||||||
|
IfErrors aul_done
|
||||||
|
; Strip the line break FileRead keeps, so CockLog does not double it.
|
||||||
|
aul_trim:
|
||||||
|
StrCpy $R9 $UninstLogLine 1 -1
|
||||||
|
${If} $R9 == "$\n"
|
||||||
|
${OrIf} $R9 == "$\r"
|
||||||
|
StrCpy $UninstLogLine $UninstLogLine -1
|
||||||
|
Goto aul_trim
|
||||||
|
${EndIf}
|
||||||
|
!insertmacro CockLog "uninstaller: $UninstLogLine"
|
||||||
|
Goto aul_loop
|
||||||
|
aul_done:
|
||||||
|
FileClose $UninstLogHandle
|
||||||
|
!insertmacro CockLog "uninstaller log end ------------------------------------------"
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; BackupUserDataBeforeUninstall / RestoreUserDataAfterUninstall
|
||||||
|
;
|
||||||
|
; The uninstallers shipped in 3.1.0/3.1.1 run a blanket "RMDir /r $INSTDIR".
|
||||||
|
; For a portable install that deletes $INSTDIR\data (decks, settings, pictures)
|
||||||
|
; along with the payload, and for a normal install it deletes anything the user
|
||||||
|
; pointed into $INSTDIR. The fixed uninstaller preserves foreign files, but the
|
||||||
|
; destructive one is already on disk on affected machines, so stash a copy
|
||||||
|
; outside $INSTDIR before invoking it and put it back afterwards. Best effort
|
||||||
|
; only: a failed backup must never abort the install.
|
||||||
|
;
|
||||||
|
; The backup is left in %TEMP% afterwards on a best-effort basis only: the next
|
||||||
|
; installer run that takes a new backup wipes it first (see below), so it is not
|
||||||
|
; a durable second copy. Anything this backup does not cover - e.g. loose files
|
||||||
|
; with other extensions - relies on the client's startup safety backup
|
||||||
|
; (warnAboutDataPathsInProgramDir), which only exists after the first launch of
|
||||||
|
; the new client.
|
||||||
|
;
|
||||||
|
; The 32-bit and 64-bit uninstall blocks below share $TEMP\CockatriceDataBackup.
|
||||||
|
; When both are registered with different InstallLocations, backing up the second
|
||||||
|
; directory into the first one's backup would restore the first dir's files into
|
||||||
|
; the second one. BackupUserDataBeforeUninstall therefore starts over when
|
||||||
|
; $OldInstallDir changes: the previous pass already restored its own backup, so
|
||||||
|
; only its leftover %TEMP% copy is lost. RelocateRestoredDataToInstDir only sees
|
||||||
|
; the last pass, so with two different old dirs plus a new target dir the first
|
||||||
|
; dir's restored data stays in place instead of moving along - no loss, but the
|
||||||
|
; user has to copy it manually.
|
||||||
|
;
|
||||||
|
; The portable-data branch below is defensive: both callers (AutoUninstallIfNeeded
|
||||||
|
; on /R, componentsPagePre interactively) only run for non-portable installs, and
|
||||||
|
; portable installs never write the registry uninstall keys that trigger them.
|
||||||
|
; It fires only when $OldInstallDir nevertheless contains portable.dat - e.g. a
|
||||||
|
; stale registry InstallLocation pointing at a portable dir, or portable.dat added
|
||||||
|
; to a normal install dir. The loose-decks and install-subdirs branches are the
|
||||||
|
; live ones for normal installs.
|
||||||
|
Function BackupUserDataBeforeUninstall
|
||||||
|
; $OldInstallDir is the directory being uninstalled: on the /R path it equals
|
||||||
|
; $INSTDIR (updater passes /D=<appDir>), on the interactive path componentsPagePre
|
||||||
|
; sets it from the registry InstallLocation via $GuardDir, because $INSTDIR is
|
||||||
|
; still the default there (directory page comes later).
|
||||||
|
${If} $OldInstallDir == ""
|
||||||
|
StrCpy $OldInstallDir $INSTDIR
|
||||||
|
${EndIf}
|
||||||
|
; %TEMP% survives across installer runs, so a previous run's safety copy would
|
||||||
|
; otherwise merge into this run: CopyFiles merges/overwrites, resurrecting files
|
||||||
|
; the user deleted since (e.g. deck A from run 1 reappearing alongside deck B
|
||||||
|
; from run 2) or restoring stale content after a failed copy. Wipe once per
|
||||||
|
; installer run - $DataBackupDir is empty until the first backup of this run,
|
||||||
|
; so the second call in sequence (32-bit then 64-bit uninstall) still merges.
|
||||||
|
${If} $DataBackupDir == ""
|
||||||
|
RMDir /r "$TEMP\CockatriceDataBackup"
|
||||||
|
${EndIf}
|
||||||
|
; The 32-bit and 64-bit uninstall blocks share one backup dir. When the second
|
||||||
|
; block points at a different directory, merging would restore the first dir's
|
||||||
|
; files into the second one, so start over instead. The previous pass already
|
||||||
|
; restored its own backup; only its leftover %TEMP% copy is discarded.
|
||||||
|
; StrCmp is case-sensitive while Windows paths are not, so a casing-only
|
||||||
|
; difference triggers a harmless redundant re-backup of the same content.
|
||||||
|
${If} $DataBackupDir != ""
|
||||||
|
${AndIf} $DataBackupForDir != ""
|
||||||
|
${AndIf} $OldInstallDir != $DataBackupForDir
|
||||||
|
!insertmacro CockLog "pre-uninstall backup: old dir changed ($DataBackupForDir -> $OldInstallDir), starting a fresh backup"
|
||||||
|
RMDir /r "$TEMP\CockatriceDataBackup"
|
||||||
|
StrCpy $DataBackupDir ""
|
||||||
|
StrCpy $DataBackupDone ""
|
||||||
|
StrCpy $LooseDecksBackupDone ""
|
||||||
|
StrCpy $InstallSubdirsBackupDone ""
|
||||||
|
${EndIf}
|
||||||
|
${If} ${FileExists} "$OldInstallDir\portable.dat"
|
||||||
|
${AndIf} ${FileExists} "$OldInstallDir\data"
|
||||||
|
StrCpy $DataBackupDir "$TEMP\CockatriceDataBackup"
|
||||||
|
StrCpy $DataBackupDone "1"
|
||||||
|
!insertmacro CockLog "pre-uninstall backup: copying $OldInstallDir\data (downloaded pics excluded)"
|
||||||
|
DetailPrint "Backing up portable user data before removing previous version..."
|
||||||
|
CreateDirectory "$DataBackupDir\data"
|
||||||
|
; Copy every top-level entry except pics (downloaded pictures are large and
|
||||||
|
; re-downloadable): copying everything first and deleting pics afterwards
|
||||||
|
; would duplicate gigabytes into %TEMP% just to throw them away.
|
||||||
|
FindFirst $DataBackupFindHandle $DataBackupFindName "$OldInstallDir\data\*.*"
|
||||||
|
${If} $DataBackupFindHandle != ""
|
||||||
|
backup_data_loop:
|
||||||
|
${If} $DataBackupFindName != "."
|
||||||
|
${AndIf} $DataBackupFindName != ".."
|
||||||
|
${AndIf} $DataBackupFindName != "pics"
|
||||||
|
${If} ${FileExists} "$OldInstallDir\data\$DataBackupFindName\*.*"
|
||||||
|
CreateDirectory "$DataBackupDir\data\$DataBackupFindName"
|
||||||
|
CopyFiles /SILENT "$OldInstallDir\data\$DataBackupFindName\*.*" "$DataBackupDir\data\$DataBackupFindName\"
|
||||||
|
${Else}
|
||||||
|
CopyFiles /SILENT "$OldInstallDir\data\$DataBackupFindName" "$DataBackupDir\data\"
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
ClearErrors
|
||||||
|
FindNext $DataBackupFindHandle $DataBackupFindName
|
||||||
|
IfErrors 0 backup_data_loop
|
||||||
|
FindClose $DataBackupFindHandle
|
||||||
|
${EndIf}
|
||||||
|
; Keep user-made custom pics (data\pics\CUSTOM).
|
||||||
|
${If} ${FileExists} "$OldInstallDir\data\pics\CUSTOM"
|
||||||
|
CreateDirectory "$DataBackupDir\data\pics\CUSTOM"
|
||||||
|
CopyFiles /SILENT "$OldInstallDir\data\pics\CUSTOM\*.*" "$DataBackupDir\data\pics\CUSTOM\"
|
||||||
|
${EndIf}
|
||||||
|
DetailPrint "Portable user data backup complete."
|
||||||
|
${EndIf}
|
||||||
|
; Deck extensions mirror DeckLoader::ACCEPTED_FILE_EXTENSIONS, minus *.txt: a
|
||||||
|
; blanket *.txt copy would also grab payload-adjacent text files, while the
|
||||||
|
; deck-specific extensions are unambiguous user content.
|
||||||
|
${If} ${FileExists} "$OldInstallDir\*.cod"
|
||||||
|
${OrIf} ${FileExists} "$OldInstallDir\*.dec"
|
||||||
|
${OrIf} ${FileExists} "$OldInstallDir\*.dek"
|
||||||
|
${OrIf} ${FileExists} "$OldInstallDir\*.mwDeck"
|
||||||
|
${If} $DataBackupDir == ""
|
||||||
|
StrCpy $DataBackupDir "$TEMP\CockatriceDataBackup"
|
||||||
|
${EndIf}
|
||||||
|
StrCpy $LooseDecksBackupDone "1"
|
||||||
|
!insertmacro CockLog "pre-uninstall backup: copying loose deck files"
|
||||||
|
DetailPrint "Backing up deck files stored in the install directory..."
|
||||||
|
CreateDirectory "$DataBackupDir\loose_decks"
|
||||||
|
CopyFiles /SILENT "$OldInstallDir\*.cod" "$DataBackupDir\loose_decks\"
|
||||||
|
CopyFiles /SILENT "$OldInstallDir\*.dec" "$DataBackupDir\loose_decks\"
|
||||||
|
CopyFiles /SILENT "$OldInstallDir\*.dek" "$DataBackupDir\loose_decks\"
|
||||||
|
CopyFiles /SILENT "$OldInstallDir\*.mwDeck" "$DataBackupDir\loose_decks\"
|
||||||
|
${EndIf}
|
||||||
|
; Data folders below the top level (e.g. $OldInstallDir\decks) are deleted by the
|
||||||
|
; old "RMDir /r $INSTDIR" just like loose decks, and on a 3.1.1 -> new upgrade the
|
||||||
|
; client's startup backup has never run yet, so stash every top-level directory
|
||||||
|
; that is not part of the payload. Payload dirs stay in place: they are rewritten
|
||||||
|
; by the section below, and restoring them could put stale DLLs next to the new
|
||||||
|
; executables. "data" is skipped once the portable branch above already took it;
|
||||||
|
; without portable.dat it is user content like any other unknown dir.
|
||||||
|
FindFirst $DataBackupFindHandle $DataBackupFindName "$OldInstallDir\*.*"
|
||||||
|
${If} $DataBackupFindHandle != ""
|
||||||
|
backup_subdirs_loop:
|
||||||
|
${If} $DataBackupFindName != "."
|
||||||
|
${AndIf} $DataBackupFindName != ".."
|
||||||
|
${AndIf} $DataBackupFindName != "plugins"
|
||||||
|
${AndIf} $DataBackupFindName != "Plugins"
|
||||||
|
${AndIf} $DataBackupFindName != "translations"
|
||||||
|
${AndIf} $DataBackupFindName != "sounds"
|
||||||
|
${AndIf} $DataBackupFindName != "themes"
|
||||||
|
${If} $DataBackupFindName == "data"
|
||||||
|
${AndIf} $DataBackupDone == "1"
|
||||||
|
Goto backup_subdirs_next
|
||||||
|
${EndIf}
|
||||||
|
${If} ${FileExists} "$OldInstallDir\$DataBackupFindName\*.*"
|
||||||
|
${If} $DataBackupDir == ""
|
||||||
|
StrCpy $DataBackupDir "$TEMP\CockatriceDataBackup"
|
||||||
|
${EndIf}
|
||||||
|
StrCpy $InstallSubdirsBackupDone "1"
|
||||||
|
!insertmacro CockLog "pre-uninstall backup: copying subdir $OldInstallDir\$DataBackupFindName"
|
||||||
|
DetailPrint "Backing up $DataBackupFindName stored in the install directory..."
|
||||||
|
CreateDirectory "$DataBackupDir\install_subdirs\$DataBackupFindName"
|
||||||
|
CopyFiles /SILENT "$OldInstallDir\$DataBackupFindName\*.*" "$DataBackupDir\install_subdirs\$DataBackupFindName\"
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
backup_subdirs_next:
|
||||||
|
ClearErrors
|
||||||
|
FindNext $DataBackupFindHandle $DataBackupFindName
|
||||||
|
IfErrors 0 backup_subdirs_loop
|
||||||
|
FindClose $DataBackupFindHandle
|
||||||
|
${EndIf}
|
||||||
|
; Remember which directory the current backup belongs to, so a later pass for a
|
||||||
|
; different directory starts over instead of merging (see above).
|
||||||
|
${If} $DataBackupDir != ""
|
||||||
|
StrCpy $DataBackupForDir $OldInstallDir
|
||||||
|
${EndIf}
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Function RestoreUserDataAfterUninstall
|
||||||
|
StrCmp $DataBackupDir "" done
|
||||||
|
${If} $OldInstallDir == ""
|
||||||
|
StrCpy $OldInstallDir $INSTDIR
|
||||||
|
${EndIf}
|
||||||
|
${If} $DataBackupDone == "1"
|
||||||
|
${AndIf} ${FileExists} "$DataBackupDir\data"
|
||||||
|
${If} ${FileExists} "$OldInstallDir\data"
|
||||||
|
; Existence-only: a partially-deleted data dir (locked files during the old
|
||||||
|
; uninstall) looks the same as a survived one, so the backup is always kept.
|
||||||
|
; Merge the backup back over it to repair partial deletes; CopyFiles only
|
||||||
|
; adds/overwrites, so a fully-survived dir is unaffected.
|
||||||
|
!insertmacro CockLog "post-uninstall restore: $OldInstallDir\data still exists, merging backup and keeping it"
|
||||||
|
CopyFiles /SILENT "$DataBackupDir\data\*.*" "$OldInstallDir\data\"
|
||||||
|
DetailPrint "Previous user data is still in place; pre-uninstall backup merged back and kept at $DataBackupDir\data."
|
||||||
|
${Else}
|
||||||
|
!insertmacro CockLog "post-uninstall restore: restoring $OldInstallDir\data"
|
||||||
|
CreateDirectory "$OldInstallDir\data"
|
||||||
|
CopyFiles /SILENT "$DataBackupDir\data\*.*" "$OldInstallDir\data\"
|
||||||
|
DetailPrint "Restored portable user data from pre-uninstall backup."
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
${If} $LooseDecksBackupDone == "1"
|
||||||
|
${AndIf} ${FileExists} "$DataBackupDir\loose_decks"
|
||||||
|
!insertmacro CockLog "post-uninstall restore: restoring loose deck files"
|
||||||
|
CreateDirectory "$OldInstallDir"
|
||||||
|
CopyFiles /SILENT "$DataBackupDir\loose_decks\*.cod" "$OldInstallDir\"
|
||||||
|
CopyFiles /SILENT "$DataBackupDir\loose_decks\*.dec" "$OldInstallDir\"
|
||||||
|
CopyFiles /SILENT "$DataBackupDir\loose_decks\*.dek" "$OldInstallDir\"
|
||||||
|
CopyFiles /SILENT "$DataBackupDir\loose_decks\*.mwDeck" "$OldInstallDir\"
|
||||||
|
DetailPrint "Restored deck files stored in the install directory."
|
||||||
|
${EndIf}
|
||||||
|
${If} $InstallSubdirsBackupDone == "1"
|
||||||
|
${AndIf} ${FileExists} "$DataBackupDir\install_subdirs"
|
||||||
|
!insertmacro CockLog "post-uninstall restore: restoring install subdirectories"
|
||||||
|
CreateDirectory "$OldInstallDir"
|
||||||
|
FindFirst $DataBackupFindHandle $DataBackupFindName "$DataBackupDir\install_subdirs\*.*"
|
||||||
|
${If} $DataBackupFindHandle != ""
|
||||||
|
restore_subdirs_loop:
|
||||||
|
${If} $DataBackupFindName != "."
|
||||||
|
${AndIf} $DataBackupFindName != ".."
|
||||||
|
${AndIf} ${FileExists} "$DataBackupDir\install_subdirs\$DataBackupFindName\*.*"
|
||||||
|
!insertmacro CockLog "post-uninstall restore: restoring subdir $OldInstallDir\$DataBackupFindName"
|
||||||
|
CreateDirectory "$OldInstallDir\$DataBackupFindName"
|
||||||
|
CopyFiles /SILENT "$DataBackupDir\install_subdirs\$DataBackupFindName\*.*" "$OldInstallDir\$DataBackupFindName\"
|
||||||
|
${EndIf}
|
||||||
|
ClearErrors
|
||||||
|
FindNext $DataBackupFindHandle $DataBackupFindName
|
||||||
|
IfErrors 0 restore_subdirs_loop
|
||||||
|
FindClose $DataBackupFindHandle
|
||||||
|
${EndIf}
|
||||||
|
DetailPrint "Restored folders stored in the install directory."
|
||||||
|
${EndIf}
|
||||||
|
; Runs once per uninstall block, and there are two (32-bit, 64-bit): show the
|
||||||
|
; notice a single time per installer run, covering both loose decks and
|
||||||
|
; restored subdirectories.
|
||||||
|
${IfNot} ${Silent}
|
||||||
|
${AndIf} $LooseDeckNoticeShown != "1"
|
||||||
|
${If} $LooseDecksBackupDone == "1"
|
||||||
|
${OrIf} $InstallSubdirsBackupDone == "1"
|
||||||
|
StrCpy $LooseDeckNoticeShown "1"
|
||||||
|
MessageBox MB_OK|MB_ICONINFORMATION "Your decks or data folders were stored inside the Cockatrice install folder ($OldInstallDir), which the previous version's uninstaller deletes.$\n$\nThey have been restored from a backup to that folder (loose deck files ending in .txt are not backed up - move those manually). If you install to a different folder, they are moved there automatically. Please move them out of the install folder afterwards (Settings -> General -> Paths) so a future uninstall cannot delete them again."
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
done:
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; RelocateRestoredDataToInstDir
|
||||||
|
;
|
||||||
|
; componentsPagePre backs up and restores around the old uninstaller before the
|
||||||
|
; directory page, so $OldInstallDir (registry InstallLocation) is authoritative
|
||||||
|
; there while $INSTDIR is still the default. When the user then picks a different
|
||||||
|
; target directory, the restored user data would otherwise stay orphaned in the
|
||||||
|
; old directory while the new install starts empty. Copy it across when they differ
|
||||||
|
; (a copy, not a move: the source is user data and is deliberately left in place).
|
||||||
|
; Only the last uninstall pass's $OldInstallDir is relocated (see the shared-backup
|
||||||
|
; comment above); with two different old dirs the first one's data stays restored
|
||||||
|
; in place instead of moving along.
|
||||||
|
; StrCmp is case-sensitive while Windows paths are not; a casing-only difference
|
||||||
|
; triggers a harmless redundant copy onto itself.
|
||||||
|
Function RelocateRestoredDataToInstDir
|
||||||
|
StrCmp $DataBackupDir "" done
|
||||||
|
StrCmp $OldInstallDir "" done
|
||||||
|
StrCmp $OldInstallDir $INSTDIR done
|
||||||
|
${If} $DataBackupDone == "1"
|
||||||
|
${AndIf} ${FileExists} "$OldInstallDir\data"
|
||||||
|
!insertmacro CockLog "relocate: copying restored data $OldInstallDir\data -> $INSTDIR\data"
|
||||||
|
CreateDirectory "$INSTDIR\data"
|
||||||
|
CopyFiles /SILENT "$OldInstallDir\data\*.*" "$INSTDIR\data\"
|
||||||
|
DetailPrint "Copied restored user data to the new install folder."
|
||||||
|
${EndIf}
|
||||||
|
${If} $LooseDecksBackupDone == "1"
|
||||||
|
${If} ${FileExists} "$OldInstallDir\*.cod"
|
||||||
|
${OrIf} ${FileExists} "$OldInstallDir\*.dec"
|
||||||
|
${OrIf} ${FileExists} "$OldInstallDir\*.dek"
|
||||||
|
${OrIf} ${FileExists} "$OldInstallDir\*.mwDeck"
|
||||||
|
!insertmacro CockLog "relocate: copying restored decks $OldInstallDir -> $INSTDIR"
|
||||||
|
CreateDirectory "$INSTDIR"
|
||||||
|
CopyFiles /SILENT "$OldInstallDir\*.cod" "$INSTDIR\"
|
||||||
|
CopyFiles /SILENT "$OldInstallDir\*.dec" "$INSTDIR\"
|
||||||
|
CopyFiles /SILENT "$OldInstallDir\*.dek" "$INSTDIR\"
|
||||||
|
CopyFiles /SILENT "$OldInstallDir\*.mwDeck" "$INSTDIR\"
|
||||||
|
DetailPrint "Copied restored deck files to the new install folder."
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
${If} $InstallSubdirsBackupDone == "1"
|
||||||
|
${AndIf} ${FileExists} "$DataBackupDir\install_subdirs"
|
||||||
|
FindFirst $DataBackupFindHandle $DataBackupFindName "$DataBackupDir\install_subdirs\*.*"
|
||||||
|
${If} $DataBackupFindHandle != ""
|
||||||
|
relocate_subdirs_loop:
|
||||||
|
${If} $DataBackupFindName != "."
|
||||||
|
${AndIf} $DataBackupFindName != ".."
|
||||||
|
${AndIf} ${FileExists} "$OldInstallDir\$DataBackupFindName\*.*"
|
||||||
|
!insertmacro CockLog "relocate: copying restored subdir $OldInstallDir\$DataBackupFindName -> $INSTDIR\$DataBackupFindName"
|
||||||
|
CreateDirectory "$INSTDIR\$DataBackupFindName"
|
||||||
|
CopyFiles /SILENT "$OldInstallDir\$DataBackupFindName\*.*" "$INSTDIR\$DataBackupFindName\"
|
||||||
|
DetailPrint "Copied restored folder $DataBackupFindName to the new install folder."
|
||||||
|
${EndIf}
|
||||||
|
ClearErrors
|
||||||
|
FindNext $DataBackupFindHandle $DataBackupFindName
|
||||||
|
IfErrors 0 relocate_subdirs_loop
|
||||||
|
FindClose $DataBackupFindHandle
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
done:
|
||||||
|
FunctionEnd
|
||||||
Loading…
Add table
Add a link
Reference in a new issue