mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
[Updater] Close a running Cockatrice when the installer asks (#7672)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Updater] Refuse a close that arrives while one is already asking MainWindow::closeEvent() had a re-entrancy guard that returned without touching the event. A QCloseEvent is accepted unless something ignores it, so a second close request - an installer replacing our files posts one every 30 s - accepted the close while the first one was still blocked on its own confirmation. close() then hid this window, hid the confirmation dialog with it, and left the process running inside the nested loop that dialog had opened: no window, no way to answer it, and every later close request accepted into the same dead end. That is what an update looked like when it could not close a running client, and it is why the installer could name the process, bring it forward and ask it to close for minutes without anything happening. Track how far the shutdown has got instead. While a confirmation is up, a close from anywhere else is refused, which leaves the question in front of the user; once the shutdown has been accepted, a duplicate close (Qt delivers the event twice on some platforms) is accepted again so it cannot cancel the quit. * [Installer] Leave an instance that is asking a question alone An instance that answered a close by hiding rather than quitting - the state the client used to get into - has no visible window left, and the confirmation it is waiting on is hidden with the window that owns it. The close probe only ever looked at visible windows, so it restored the main window, posted WM_CLOSE into the same dead end, and reported "still running" without saying why. Show every window the instance left hidden before choosing the one to close, and skip the close when the window it would reach is disabled: a modal dialog of the instance's own is up, and posting the close then only hides the question again. Name what is left of the window in the report as well - gone, still open, hidden again, and whether the thread answers at all - so the log tells a refused close from one that was never delivered. * [Installer] Offer to end a Cockatrice that will not close A close is a request, and an instance that does not answer it - waiting on a question nobody is looking at, or stuck where a close cannot reach - leaves the user with no way forward: the files cannot be replaced while it runs, and clicking Yes again only repeats the request. After a second close that changed nothing, offer the only thing that can free the files: end it. The prompt says what it costs - an unsaved deck, a running game or a setting it has not written yet - the default button is the safe one, and it is a separate answer rather than a relabelled "Yes", so it is not reached by clicking through the close prompt. Only cockatrice.exe is ever ended: Oracle writes the card database in place and Servatrice is a service, so both are named and left to the user, exactly as they are for the close. The probe gains an --end-holders mode for it, and the uninstaller's guard gets the same offer. * [Installer] Never name the process that is doing the asking The old uninstaller is started with _?= so it runs in place, from the very directory it is emptying, and an installer can be downloaded into the folder it installs to. Both are then processes running from the directory, so the probe named them - and the guard asked the user to close the program that was asking. That question cannot be answered: a close and the end offer both skip anything that is not cockatrice.exe, so the guard repeated itself until the update or the uninstall was cancelled. The probe and the processes that started it are now never named, by either lookup. The walk up the parent chain stops at the first program the payload installs, because in an in-app update the installer's own parent is the Cockatrice instance being replaced - the one the guard exists to name. The Windows check held its locked file in the script that starts the probe, so that holder is excluded by design now and the check read it as "nothing found". It holds the file from a process of its own instead, and asserts the exclusion itself: a file that only the asking process holds comes back as nothing. * [Installer] Restore only the windows a user could answer Showing every hidden window an instance owns put Qt's own bookkeeping windows on screen: the tray icon keeps one to receive the TaskbarCreated message, styled WS_OVERLAPPED and titled QTrayIconMessageWindow. Restored, it is an empty white rectangle in the taskbar with no title bar to close it - it has no window procedure that would hide it again, and the process is busy in the very loop the restore is meant to break, so it stayed until the process was force-ended. Only framed, non-empty windows are restored and considered for the close now. That also keeps the close off Qt's tray window, which answers WM_CLOSE by asking the application to terminate - a quit that skips the confirmation the close is supposed to reach. * [Installer] Uninstall the previous version before installing over it A regular install over an existing one asked "a previous version must be uninstalled", then ran the old uninstaller interactively. That uninstaller put its confirmation page behind the installer window, where it could not be answered, and it only removed the installation after the installer had already written the new one over it. Run the previous version's uninstaller silently instead. The installer has already asked the question the user answered, so a second, hidden confirmation is pure risk: with /S there is no window to be behind, and ExecWait is then waiting for something that cannot outlive it. QuietUninstallString is used when the old installation has one, otherwise /S is appended. ExecWait only waits while the uninstaller runs in place: one that did not get a usable _?= copies itself to %TEMP% and returns at once, while the copy keeps deleting - which is how an uninstall can finish after the install it was supposed to precede. Log the exit code and wait for the old cockatrice.exe to disappear before restoring user data or writing anything, and abort with a message instead of installing into a directory that is still being emptied. Cancelling the "must be uninstalled" prompt now quits the installer; it used to skip the page and install over the old version anyway. * [Installer] Do not show windows the application has hidden The guard restored every hidden window an instance owned before asking it to close, so that a confirmation hidden together with its window would come back. Windows does as it is told and shows them; the application does not, because Qt still believes they are hidden and never paints them. What the user gets is a blank white rectangle, on screen and on the taskbar, that answers nothing - and the question it was meant to bring back is not answerable in it either. An instance with no window a user could answer is named and left to the user instead, and the prompt says what to do about it: open it from the notification area. Ending it stays the last resort, offered once a close has been asked for twice. The abort after a failed uninstall now says what it saw - the old version is still there after its uninstaller ran - instead of promising a removal that is not happening, which is the state a cancelled old uninstaller leaves behind. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
92d082e735
commit
30d3afc7b4
9 changed files with 606 additions and 90 deletions
|
|
@ -474,6 +474,10 @@ if [[ $RUNNER_OS == Windows && ! $MAKE_NO_CLIENT ]]; then
|
||||||
# FileShare.None is exactly that case, so this asserts the reason the probe was
|
# FileShare.None is exactly that case, so this asserts the reason the probe was
|
||||||
# added instead of only that the Restart Manager answers at all. It needs no NSIS
|
# added instead of only that the Restart Manager answers at all. It needs no NSIS
|
||||||
# and no packaging, so it runs on every Windows build that gets this far.
|
# and no packaging, so it runs on every Windows build that gets this far.
|
||||||
|
# The holder is a process of its own: the probe never names the process that started it,
|
||||||
|
# which is what keeps an in-place uninstaller from being named as a holder of the
|
||||||
|
# directory it is emptying. The same DLL held by the script that starts the probe
|
||||||
|
# therefore has to come back as nothing, and that is the second case below.
|
||||||
probe_check="./holder-probe-check.ps1"
|
probe_check="./holder-probe-check.ps1"
|
||||||
cat >"$probe_check" <<'POWERSHELL'
|
cat >"$probe_check" <<'POWERSHELL'
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
@ -500,17 +504,61 @@ function Invoke-Probe([string] $guardDir) {
|
||||||
return @{ Code = $code; Output = $output }
|
return @{ Code = $code; Output = $output }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Wait-UntilLocked([string] $path) {
|
||||||
|
$deadline = (Get-Date).AddSeconds(15)
|
||||||
|
while ((Get-Date) -lt $deadline) {
|
||||||
|
try {
|
||||||
|
$open = [IO.File]::Open($path, "Open", "ReadWrite", "None")
|
||||||
|
$open.Dispose()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Start-Sleep -Milliseconds 100
|
||||||
|
}
|
||||||
|
throw "the holder never locked $path"
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$file = Join-Path $dir "locked.dll"
|
$file = Join-Path $dir "locked.dll"
|
||||||
[IO.File]::WriteAllText($file, "x")
|
[IO.File]::WriteAllText($file, "x")
|
||||||
|
|
||||||
|
# The holder is a process of its own, not this script: the probe never names the process
|
||||||
|
# that started it (the case below asserts that), so a file held here would be left out on
|
||||||
|
# purpose and the check would pass for the wrong reason.
|
||||||
|
$holderScript = Join-Path $dir "hold.ps1"
|
||||||
|
Set-Content -LiteralPath $holderScript -Value @'
|
||||||
|
param([string] $Path)
|
||||||
|
$handle = [IO.File]::Open($Path, "Open", "ReadWrite", "None")
|
||||||
|
Start-Sleep -Seconds 120
|
||||||
|
'@
|
||||||
|
$powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||||
|
$holder = Start-Process -FilePath $powershell -PassThru -ArgumentList @(
|
||||||
|
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$holderScript`"", "-Path", "`"$file`"")
|
||||||
|
try {
|
||||||
|
Wait-UntilLocked $file
|
||||||
|
$locked = Invoke-Probe $dir
|
||||||
|
if ($locked.Code -ne 1) { throw "expected exit 1 for a locked file, got $($locked.Code) : $($locked.Output)" }
|
||||||
|
if ($locked.Output -notmatch "pid\s+$($holder.Id)\b") {
|
||||||
|
throw "the lookup did not name the process holding the file: $($locked.Output)"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try { $holder.Kill() } catch { }
|
||||||
|
try { $holder.WaitForExit(5000) | Out-Null } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
# The probe is started by the installer, or by an uninstaller that runs in place from the
|
||||||
|
# directory it is emptying, so the asking process holds files there itself. Naming it would
|
||||||
|
# ask the user to close the program that is asking - a question neither the close nor the
|
||||||
|
# end offer can answer, so the guard repeated it until the update was cancelled. A holder
|
||||||
|
# that is only the asking process has to come back as nothing.
|
||||||
$handle = [IO.File]::Open($file, "Open", "ReadWrite", "None")
|
$handle = [IO.File]::Open($file, "Open", "ReadWrite", "None")
|
||||||
try {
|
try {
|
||||||
$locked = Invoke-Probe $dir
|
$self = Invoke-Probe $dir
|
||||||
} finally {
|
} finally {
|
||||||
$handle.Dispose()
|
$handle.Dispose()
|
||||||
}
|
}
|
||||||
if ($locked.Code -ne 1) { throw "expected exit 1 for a locked file, got $($locked.Code) : $($locked.Output)" }
|
if ($self.Code -ne 0) { throw "expected exit 0 when the asking process is the only holder, got $($self.Code) : $($self.Output)" }
|
||||||
if ($locked.Output -notmatch "pid\s+$PID\b") { throw "the lookup did not name this process: $($locked.Output)" }
|
if ($self.Output -match "pid\s+$PID\b") { throw "the lookup named the process that started it: $($self.Output)" }
|
||||||
$empty = Join-Path $dir "empty"
|
$empty = Join-Path $dir "empty"
|
||||||
New-Item -ItemType Directory -Force $empty | Out-Null
|
New-Item -ItemType Directory -Force $empty | Out-Null
|
||||||
$nothing = Invoke-Probe $empty
|
$nothing = Invoke-Probe $empty
|
||||||
|
|
@ -535,7 +583,7 @@ try {
|
||||||
if ($byImage.Output -notmatch "fake-cockatrice\.exe \(pid\s+$($running.Id)") {
|
if ($byImage.Output -notmatch "fake-cockatrice\.exe \(pid\s+$($running.Id)") {
|
||||||
throw "the lookup did not name the process running from the directory: $($byImage.Output)"
|
throw "the lookup did not name the process running from the directory: $($byImage.Output)"
|
||||||
}
|
}
|
||||||
Write-Host "holder lookup named pid $PID for a locked file, named nothing for an empty directory, and named pid $($running.Id) for a process running from the directory"
|
Write-Host "holder lookup named pid $($holder.Id) for a file another process holds, named nothing for a file only the asking process holds, named nothing for an empty directory, and named pid $($running.Id) for a process running from the directory"
|
||||||
} finally {
|
} finally {
|
||||||
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
|
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,9 @@ Var HolderFound
|
||||||
; whether the payload is free, while the abort dialog reports what survived.
|
; whether the payload is free, while the abort dialog reports what survived.
|
||||||
Var GuardExit
|
Var GuardExit
|
||||||
Var GuardOutput
|
Var GuardOutput
|
||||||
; Whether the guard already asked Cockatrice to close once (see GuardAppsNotRunning)
|
; How many closes the guard has asked for: 0 before the automatic one, 1 after it, and one
|
||||||
|
; more for every "Yes" at the prompt. From the second one on, the prompt offers to end the
|
||||||
|
; instance instead (see GuardAppsNotRunning).
|
||||||
Var GuardCloseTried
|
Var GuardCloseTried
|
||||||
Var WaitPid
|
Var WaitPid
|
||||||
Var LauncherExited
|
Var LauncherExited
|
||||||
|
|
@ -391,7 +393,9 @@ StrCpy $OldInstallDir $INSTDIR
|
||||||
StrCmp $R0 "" done32
|
StrCmp $R0 "" done32
|
||||||
DetailPrint "Removing previous version (32-bit)..."
|
DetailPrint "Removing previous version (32-bit)..."
|
||||||
Call BackupUserDataBeforeUninstall
|
Call BackupUserDataBeforeUninstall
|
||||||
ExecWait '$R0 _?=$OldInstallDir'
|
ExecWait '$R0 _?=$OldInstallDir' $R5
|
||||||
|
!insertmacro CockLog "uninstall (32-bit): exit=$R5"
|
||||||
|
Call WaitForOldPayloadToDisappear
|
||||||
Call RestoreUserDataAfterUninstall
|
Call RestoreUserDataAfterUninstall
|
||||||
|
|
||||||
done32:
|
done32:
|
||||||
|
|
@ -406,7 +410,9 @@ ${If} ${RunningX64}
|
||||||
StrCmp $R0 "" done64
|
StrCmp $R0 "" done64
|
||||||
DetailPrint "Removing previous version (64-bit)..."
|
DetailPrint "Removing previous version (64-bit)..."
|
||||||
Call BackupUserDataBeforeUninstall
|
Call BackupUserDataBeforeUninstall
|
||||||
ExecWait '$R0 _?=$OldInstallDir'
|
ExecWait '$R0 _?=$OldInstallDir' $R5
|
||||||
|
!insertmacro CockLog "uninstall (64-bit): exit=$R5"
|
||||||
|
Call WaitForOldPayloadToDisappear
|
||||||
Call RestoreUserDataAfterUninstall
|
Call RestoreUserDataAfterUninstall
|
||||||
|
|
||||||
done64:
|
done64:
|
||||||
|
|
@ -414,6 +420,44 @@ ${EndIf}
|
||||||
|
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
|
; 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
|
||||||
|
; installing over a directory that is still being emptied.
|
||||||
|
Function WaitForOldPayloadToDisappear
|
||||||
|
${If} $OldInstallDir == ""
|
||||||
|
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"
|
||||||
|
StrCpy $AbortReason \
|
||||||
|
"The previous version in $OldInstallDir is still there after its uninstaller ran.$\r$\n$\r$\nIf that uninstaller was cancelled or reported an error, close anything still using those files and run this installer again."
|
||||||
|
Call AbortInstall
|
||||||
|
${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}
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
; BackupUserDataBeforeUninstall / RestoreUserDataAfterUninstall
|
; BackupUserDataBeforeUninstall / RestoreUserDataAfterUninstall
|
||||||
;
|
;
|
||||||
; The uninstallers shipped in 3.1.0/3.1.1 run a blanket "RMDir /r $INSTDIR".
|
; The uninstallers shipped in 3.1.0/3.1.1 run a blanket "RMDir /r $INSTDIR".
|
||||||
|
|
@ -748,13 +792,15 @@ FunctionEnd
|
||||||
; the image lookup matches on the directory a process runs from, so it does not
|
; the image lookup matches on the directory a process runs from, so it does not
|
||||||
; widen that set.
|
; widen that set.
|
||||||
;
|
;
|
||||||
; Nothing is force-stopped anywhere: Cockatrice may be holding an unsaved deck
|
; Nothing is force-stopped without the user asking for it: Cockatrice may be holding
|
||||||
; or showing a "really leave this game?" confirmation, Oracle writes the card
|
; an unsaved deck or showing a "really leave this game?" confirmation, Oracle writes
|
||||||
; database in place and killing it mid-write would corrupt it, and Servatrice
|
; the card database in place and killing it mid-write would corrupt it, and Servatrice
|
||||||
; has no window to ask. A close is a request (WM_CLOSE), made automatically once
|
; has no window to ask. A close is a request (WM_CLOSE), made automatically once
|
||||||
; because that is what the user started the installer to do, and again whenever
|
; because that is what the user started the installer to do, and again whenever
|
||||||
; they ask for it from the prompt; when it is not answered the user is told which
|
; they ask for it from the prompt. When it is not answered the user is told which
|
||||||
; program is in the way instead of having it killed.
|
; program is in the way, and once a close has been asked for twice without freeing
|
||||||
|
; the files they are offered to end Cockatrice themselves - the last resort, with
|
||||||
|
; what it costs spelled out, and never applied to Oracle or Servatrice.
|
||||||
|
|
||||||
; Emitted once for the installer and once for the uninstaller: the uninstaller
|
; 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
|
; cannot call installer functions, and its own copy of the probe keeps a running
|
||||||
|
|
@ -806,8 +852,9 @@ FunctionEnd
|
||||||
; log as it stands: it names every holder, including the ones left to the user, so a close
|
; 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.
|
; that did nothing can be told apart from one that was never sent.
|
||||||
;
|
;
|
||||||
; Never a force-close: the application decides whether to quit, and one that does not answer
|
; Never a force-close here: the application decides whether to quit. Ending it is a separate
|
||||||
; is left to the user instead of being killed.
|
; answer the prompt offers once a close has been asked for twice and ignored, spelled out
|
||||||
|
; with what it costs - see EndHolders - and it is only ever applied to cockatrice.exe.
|
||||||
Function ${prefix}CloseHolders
|
Function ${prefix}CloseHolders
|
||||||
Call ${prefix}ExtractHolderProbe
|
Call ${prefix}ExtractHolderProbe
|
||||||
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
||||||
|
|
@ -822,6 +869,27 @@ Function ${prefix}CloseHolders
|
||||||
Call ${prefix}CockLogWrite
|
Call ${prefix}CockLogWrite
|
||||||
DetailPrint "Closing Cockatrice: $R4"
|
DetailPrint "Closing Cockatrice: $R4"
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
|
; usage: set $GuardDir to the directory being replaced, then call this. Ends the running
|
||||||
|
; Cockatrice 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. The probe
|
||||||
|
; ends cockatrice.exe and nothing else - Oracle writes the card database in place and
|
||||||
|
; Servatrice is a service, so both stay the user's job, exactly as they do for the close.
|
||||||
|
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: $R4"
|
||||||
|
FunctionEnd
|
||||||
!macroend
|
!macroend
|
||||||
|
|
||||||
!insertmacro CockHolderProbeFunctions "" install
|
!insertmacro CockHolderProbeFunctions "" install
|
||||||
|
|
@ -835,7 +903,8 @@ FunctionEnd
|
||||||
; remains when the close cannot reach everything: Oracle writes the card database in
|
; remains when the close cannot reach everything: Oracle writes the card database in
|
||||||
; place, Servatrice has no window, and Cockatrice may still be waiting for an answer in
|
; place, Servatrice has no window, and Cockatrice may still be waiting for an answer in
|
||||||
; its own confirmation. It offers one more close (Yes), a plain re-check for after the
|
; its own confirmation. It offers one more close (Yes), a plain re-check for after the
|
||||||
; user closed them by hand (No), and Cancel.
|
; user closed them by hand (No), and Cancel. Once a close has been asked for twice without
|
||||||
|
; freeing the files, it offers to end Cockatrice instead - the last resort, see EndHolders.
|
||||||
;
|
;
|
||||||
; An error is always shown, in every mode. A /S install without /R has nobody at
|
; 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
|
; the keyboard, but this installer does not promise unattended runs: when an
|
||||||
|
|
@ -867,13 +936,35 @@ Function GuardAppsNotRunning
|
||||||
Goto guard_check
|
Goto guard_check
|
||||||
${EndIf}
|
${EndIf}
|
||||||
DetailPrint "The install directory is in use by: $GuardOutput"
|
DetailPrint "The install directory is in use by: $GuardOutput"
|
||||||
|
; A close that has been asked for twice and changed nothing is where asking again
|
||||||
|
; stops being a plan: the instance is either waiting on a question the user has not
|
||||||
|
; answered, or stuck where a close cannot reach it. Offer to end it - the only thing
|
||||||
|
; left that can free the files - with the cost in the text, because a client that is
|
||||||
|
; still running may have an unsaved deck, a game or a settings write in flight.
|
||||||
|
${If} $GuardCloseTried >= 2
|
||||||
|
MessageBox MB_YESNO|MB_ICONEXCLAMATION|MB_DEFBUTTON2 \
|
||||||
|
"Cockatrice is still running after being asked to close twice.$\r$\n$\r$\nChoose Yes to end it now - an unsaved deck, a running game or a setting it has not written yet will be lost - or No to ask it to close again.$\r$\n$\r$\n$GuardOutput" \
|
||||||
|
IDYES guard_end IDNO guard_close
|
||||||
|
!insertmacro CockLog "guard: cancelled by user"
|
||||||
|
StrCpy $AbortReason \
|
||||||
|
"The installation was cancelled because Cockatrice, Oracle or Servatrice is still running and holding files of the previous version.$\r$\n$\r$\nClose them and run the installer again."
|
||||||
|
Call AbortInstall
|
||||||
|
; Only reached through Yes: AbortInstall does not return.
|
||||||
|
guard_end:
|
||||||
|
!insertmacro CockLog "guard: ending cockatrice on request, then checking again"
|
||||||
|
Call EndHolders
|
||||||
|
; Back to the ordinary prompt afterwards: Oracle and Servatrice are never ended,
|
||||||
|
; so what is left may still have to be closed by hand.
|
||||||
|
StrCpy $GuardCloseTried 1
|
||||||
|
Goto guard_check
|
||||||
|
${EndIf}
|
||||||
; Three answers, and the close is one of them: the automatic attempt above ran
|
; Three answers, and the close is one of them: the automatic attempt above ran
|
||||||
; before this prompt and could not have answered Cockatrice's own confirmation, so a
|
; before this prompt and could not have answered Cockatrice's own confirmation, so a
|
||||||
; second close is often exactly what is needed once the user has dealt with that
|
; second close is often exactly what is needed once the user has dealt with that
|
||||||
; window. NSIS does not let a MessageBox relabel its buttons, so the text says what
|
; window. NSIS does not let a MessageBox relabel its buttons, so the text says what
|
||||||
; Yes, No and Cancel do.
|
; Yes, No and Cancel do.
|
||||||
MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
|
MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
|
||||||
"Cockatrice, Oracle or Servatrice is still using files of the installation:$\r$\n$\r$\n$GuardOutput$\r$\n$\r$\nChoose Yes to close Cockatrice and check again. It may ask you to confirm quitting - it does while a card database update is running, a game is open or a deck is unsaved - so answer that window.$\r$\n$\r$\nClose Oracle, Servatrice and any other program listed above by hand. Choose No to check again after that, or Cancel to cancel the installation." \
|
"Cockatrice, Oracle or Servatrice is still using files of the installation:$\r$\n$\r$\n$GuardOutput$\r$\n$\r$\nChoose Yes to close Cockatrice and check again. It may ask you to confirm quitting - it does while a card database update is running, a game is open or a deck is unsaved - so answer that window. If it has no window on screen, open it from the notification area first.$\r$\n$\r$\nClose Oracle, Servatrice and any other program listed above by hand. Choose No to check again after that, or Cancel to cancel the installation." \
|
||||||
IDYES guard_close IDNO guard_check
|
IDYES guard_close IDNO guard_check
|
||||||
!insertmacro CockLog "guard: cancelled by user"
|
!insertmacro CockLog "guard: cancelled by user"
|
||||||
StrCpy $AbortReason \
|
StrCpy $AbortReason \
|
||||||
|
|
@ -881,6 +972,7 @@ Function GuardAppsNotRunning
|
||||||
Call AbortInstall
|
Call AbortInstall
|
||||||
; Only reached through Yes: AbortInstall does not return.
|
; Only reached through Yes: AbortInstall does not return.
|
||||||
guard_close:
|
guard_close:
|
||||||
|
IntOp $GuardCloseTried $GuardCloseTried + 1
|
||||||
!insertmacro CockLog "guard: closing cockatrice on request, then checking again"
|
!insertmacro CockLog "guard: closing cockatrice on request, then checking again"
|
||||||
Call CloseHolders
|
Call CloseHolders
|
||||||
Goto guard_check
|
Goto guard_check
|
||||||
|
|
@ -1517,16 +1609,33 @@ Function un.GuardAppsNotRunning
|
||||||
Goto un_guard_check
|
Goto un_guard_check
|
||||||
${EndIf}
|
${EndIf}
|
||||||
DetailPrint "The install directory is in use by: $GuardOutput"
|
DetailPrint "The install directory is in use by: $GuardOutput"
|
||||||
|
; Same escalation as in the installer's guard: after a second close that changed
|
||||||
|
; nothing, offer to end the instance, with what it costs in the text.
|
||||||
|
${If} $GuardCloseTried >= 2
|
||||||
|
MessageBox MB_YESNO|MB_ICONEXCLAMATION|MB_DEFBUTTON2 \
|
||||||
|
"Cockatrice is still running after being asked to close twice.$\r$\n$\r$\nChoose Yes to end it now - an unsaved deck, a running game or a setting it has not written yet will be lost - or No to ask it to close again.$\r$\n$\r$\n$GuardOutput" \
|
||||||
|
IDYES un_guard_end IDNO un_guard_close
|
||||||
|
!insertmacro CockLogUn "uninst-guard: cancelled by user"
|
||||||
|
SetErrorLevel 1
|
||||||
|
Abort
|
||||||
|
; Only reached through Yes: Abort does not return.
|
||||||
|
un_guard_end:
|
||||||
|
!insertmacro CockLogUn "uninst-guard: ending cockatrice on request, then checking again"
|
||||||
|
Call un.EndHolders
|
||||||
|
StrCpy $GuardCloseTried 1
|
||||||
|
Goto un_guard_check
|
||||||
|
${EndIf}
|
||||||
; Same three answers as the installer's guard; the automatic close above already
|
; Same three answers as the installer's guard; the automatic close above already
|
||||||
; ran once before this prompt.
|
; ran once before this prompt.
|
||||||
MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
|
MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
|
||||||
"Cockatrice, Oracle or Servatrice is still using files of the installation:$\r$\n$\r$\n$GuardOutput$\r$\n$\r$\nChoose Yes to close Cockatrice and check again. It may ask you to confirm quitting - it does while a card database update is running, a game is open or a deck is unsaved - so answer that window.$\r$\n$\r$\nClose Oracle, Servatrice and any other program listed above by hand. Choose No to check again after that, or Cancel to cancel the uninstallation." \
|
"Cockatrice, Oracle or Servatrice is still using files of the installation:$\r$\n$\r$\n$GuardOutput$\r$\n$\r$\nChoose Yes to close Cockatrice and check again. It may ask you to confirm quitting - it does while a card database update is running, a game is open or a deck is unsaved - so answer that window. If it has no window on screen, open it from the notification area first.$\r$\n$\r$\nClose Oracle, Servatrice and any other program listed above by hand. Choose No to check again after that, or Cancel to cancel the uninstallation." \
|
||||||
IDYES un_guard_close IDNO un_guard_check
|
IDYES un_guard_close IDNO un_guard_check
|
||||||
!insertmacro CockLogUn "uninst-guard: cancelled by user"
|
!insertmacro CockLogUn "uninst-guard: cancelled by user"
|
||||||
SetErrorLevel 1
|
SetErrorLevel 1
|
||||||
Abort
|
Abort
|
||||||
; Only reached through Yes: Abort does not return.
|
; Only reached through Yes: Abort does not return.
|
||||||
un_guard_close:
|
un_guard_close:
|
||||||
|
IntOp $GuardCloseTried $GuardCloseTried + 1
|
||||||
!insertmacro CockLogUn "uninst-guard: closing cockatrice on request, then checking again"
|
!insertmacro CockLogUn "uninst-guard: closing cockatrice on request, then checking again"
|
||||||
Call un.CloseHolders
|
Call un.CloseHolders
|
||||||
Goto un_guard_check
|
Goto un_guard_check
|
||||||
|
|
@ -1601,7 +1710,10 @@ ${If} $PortableMode = 0
|
||||||
|
|
||||||
${If} $ReinstallMode = 0
|
${If} $ReinstallMode = 0
|
||||||
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst32
|
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst32
|
||||||
Abort
|
; Abort would only skip this page and install over the old version anyway;
|
||||||
|
; the message said the old one must go first, so Cancel means cancel.
|
||||||
|
!insertmacro CockLog "componentsPagePre: cancelled at the previous-version prompt"
|
||||||
|
Quit
|
||||||
${Else}
|
${Else}
|
||||||
Goto uninst32
|
Goto uninst32
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
@ -1618,10 +1730,24 @@ ${If} $PortableMode = 0
|
||||||
StrCpy $OldUninstallString $R0
|
StrCpy $OldUninstallString $R0
|
||||||
Call EnsureOldInstallNotRunning
|
Call EnsureOldInstallNotRunning
|
||||||
StrCpy $OldInstallDir $GuardDir
|
StrCpy $OldInstallDir $GuardDir
|
||||||
|
; Run it silently. The question above is the one the user answered; the
|
||||||
|
; uninstaller's own confirmation page would be a second one, and that one opens
|
||||||
|
; behind this window, where nobody can answer it. The installer then looks hung,
|
||||||
|
; the user clicks Next on it, the new version is installed, and the uninstaller
|
||||||
|
; removes everything once this window closes. /S leaves no window to be behind,
|
||||||
|
; so ExecWait is waiting for something that cannot outlive it.
|
||||||
|
ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
|
||||||
|
${If} $R1 == ""
|
||||||
|
StrCpy $OldUninstallString "$OldUninstallString /S"
|
||||||
|
${Else}
|
||||||
|
StrCpy $OldUninstallString $R1
|
||||||
|
${EndIf}
|
||||||
!insertmacro CockLog "componentsPagePre: old 32-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)"
|
!insertmacro CockLog "componentsPagePre: old 32-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)"
|
||||||
ClearErrors
|
ClearErrors
|
||||||
Call BackupUserDataBeforeUninstall
|
Call BackupUserDataBeforeUninstall
|
||||||
ExecWait "$OldUninstallString _?=$OldInstallDir"
|
ExecWait "$OldUninstallString _?=$OldInstallDir" $R5
|
||||||
|
!insertmacro CockLog "componentsPagePre: old 32-bit uninstall exit=$R5"
|
||||||
|
Call WaitForOldPayloadToDisappear
|
||||||
Call RestoreUserDataAfterUninstall
|
Call RestoreUserDataAfterUninstall
|
||||||
|
|
||||||
done32:
|
done32:
|
||||||
|
|
@ -1635,7 +1761,8 @@ ${If} $PortableMode = 0
|
||||||
|
|
||||||
${If} $ReinstallMode = 0
|
${If} $ReinstallMode = 0
|
||||||
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst64
|
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "A previous version of Cockatrice must be uninstalled before installing the new one." IDOK uninst64
|
||||||
Abort
|
!insertmacro CockLog "componentsPagePre: cancelled at the previous-version prompt"
|
||||||
|
Quit
|
||||||
${Else}
|
${Else}
|
||||||
Goto uninst64
|
Goto uninst64
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
@ -1644,10 +1771,19 @@ ${If} $PortableMode = 0
|
||||||
StrCpy $OldUninstallString $R0
|
StrCpy $OldUninstallString $R0
|
||||||
Call EnsureOldInstallNotRunning
|
Call EnsureOldInstallNotRunning
|
||||||
StrCpy $OldInstallDir $GuardDir
|
StrCpy $OldInstallDir $GuardDir
|
||||||
|
; Silent, for the reason given in the 32-bit branch above.
|
||||||
|
ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "QuietUninstallString"
|
||||||
|
${If} $R1 == ""
|
||||||
|
StrCpy $OldUninstallString "$OldUninstallString /S"
|
||||||
|
${Else}
|
||||||
|
StrCpy $OldUninstallString $R1
|
||||||
|
${EndIf}
|
||||||
!insertmacro CockLog "componentsPagePre: old 64-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)"
|
!insertmacro CockLog "componentsPagePre: old 64-bit uninstall '$OldUninstallString _?=$OldInstallDir' (oldDir=$OldInstallDir InstDir=$INSTDIR)"
|
||||||
ClearErrors
|
ClearErrors
|
||||||
Call BackupUserDataBeforeUninstall
|
Call BackupUserDataBeforeUninstall
|
||||||
ExecWait "$OldUninstallString _?=$OldInstallDir"
|
ExecWait "$OldUninstallString _?=$OldInstallDir" $R5
|
||||||
|
!insertmacro CockLog "componentsPagePre: old 64-bit uninstall exit=$R5"
|
||||||
|
Call WaitForOldPayloadToDisappear
|
||||||
Call RestoreUserDataAfterUninstall
|
Call RestoreUserDataAfterUninstall
|
||||||
|
|
||||||
done64:
|
done64:
|
||||||
|
|
|
||||||
|
|
@ -923,11 +923,28 @@ void MainWindow::actShow()
|
||||||
|
|
||||||
void MainWindow::closeEvent(QCloseEvent *event)
|
void MainWindow::closeEvent(QCloseEvent *event)
|
||||||
{
|
{
|
||||||
// workaround Qt bug where closeEvent gets called twice
|
switch (shutdownState) {
|
||||||
if (bClosingDown) {
|
case ShutdownState::Accepted:
|
||||||
|
// A duplicate close for a shutdown that already went through (Qt delivers the event
|
||||||
|
// twice on some platforms). Accepting it again keeps the quit that is already under
|
||||||
|
// way; ignoring it would cancel it.
|
||||||
|
event->accept();
|
||||||
return;
|
return;
|
||||||
|
case ShutdownState::Confirming:
|
||||||
|
// A second close request arrived while one of our own confirmations is up. Another
|
||||||
|
// program asking us to close - an installer replacing our files, for one - looks
|
||||||
|
// exactly like this, and accepting it would be destructive: close() hides this
|
||||||
|
// window, hides the dialog with it, and leaves the process running inside the nested
|
||||||
|
// loop that dialog opened. Nothing can reach that loop afterwards, so the process
|
||||||
|
// stays alive with no window and every later close request is accepted into the same
|
||||||
|
// dead end. Refuse this one and leave the question in front of the user; the shutdown
|
||||||
|
// that is already running finishes as soon as they answer it.
|
||||||
|
event->ignore();
|
||||||
|
return;
|
||||||
|
case ShutdownState::Idle:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
bClosingDown = true;
|
shutdownState = ShutdownState::Confirming;
|
||||||
|
|
||||||
if (cardUpdateProcess && cardUpdateProcess->state() != QProcess::NotRunning) {
|
if (cardUpdateProcess && cardUpdateProcess->state() != QProcess::NotRunning) {
|
||||||
const QString runningFor = cardUpdateStatusBar ? cardUpdateStatusBar->statusText() : QString();
|
const QString runningFor = cardUpdateStatusBar ? cardUpdateStatusBar->statusText() : QString();
|
||||||
|
|
@ -939,19 +956,20 @@ void MainWindow::closeEvent(QCloseEvent *event)
|
||||||
.arg(runningFor);
|
.arg(runningFor);
|
||||||
if (!QtUtils::confirm(this, tr("Are you sure?"), message, QMessageBox::No)) {
|
if (!QtUtils::confirm(this, tr("Are you sure?"), message, QMessageBox::No)) {
|
||||||
event->ignore();
|
event->ignore();
|
||||||
bClosingDown = false;
|
shutdownState = ShutdownState::Idle;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tabSupervisor->close()) {
|
if (!tabSupervisor->close()) {
|
||||||
event->ignore();
|
event->ignore();
|
||||||
bClosingDown = false;
|
shutdownState = ShutdownState::Idle;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tip->close();
|
tip->close();
|
||||||
|
|
||||||
event->accept();
|
event->accept();
|
||||||
|
shutdownState = ShutdownState::Accepted;
|
||||||
SettingsCache::instance().layouts().setMainWindowGeometry(saveGeometry());
|
SettingsCache::instance().layouts().setMainWindowGeometry(saveGeometry());
|
||||||
tabSupervisor->deleteLater();
|
tabSupervisor->deleteLater();
|
||||||
}
|
}
|
||||||
|
|
@ -962,7 +980,7 @@ bool MainWindow::closeForUpdate()
|
||||||
// closeEvent() is blocked on a user prompt). close() would hit the re-entrancy guard and return
|
// closeEvent() is blocked on a user prompt). close() would hit the re-entrancy guard and return
|
||||||
// true without any shutdown having happened, so report faithfully instead: the caller must keep
|
// true without any shutdown having happened, so report faithfully instead: the caller must keep
|
||||||
// the process alive until the user resolves whatever is blocking the close.
|
// the process alive until the user resolves whatever is blocking the close.
|
||||||
if (bClosingDown) {
|
if (shutdownState != ShutdownState::Idle) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return close();
|
return close();
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,17 @@ private:
|
||||||
LagMonitor lagMonitor; ///< watches the main thread for event loop stalls
|
LagMonitor lagMonitor; ///< watches the main thread for event loop stalls
|
||||||
LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph
|
LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph
|
||||||
bool bHasActivated, askedForDbUpdater;
|
bool bHasActivated, askedForDbUpdater;
|
||||||
bool bClosingDown = false; ///< guards closeEvent() against re-entrancy
|
/**
|
||||||
|
* How far a shutdown that was asked for has got. A close request that arrives while one is
|
||||||
|
* already asking the user something must not be accepted - see closeEvent().
|
||||||
|
*/
|
||||||
|
enum class ShutdownState
|
||||||
|
{
|
||||||
|
Idle, ///< no shutdown has been asked for
|
||||||
|
Confirming, ///< asking about a card database update, open games or an unsaved deck
|
||||||
|
Accepted, ///< the close was accepted and the window is hiding
|
||||||
|
};
|
||||||
|
ShutdownState shutdownState = ShutdownState::Idle;
|
||||||
bool skipStartupAutoConnect = false;
|
bool skipStartupAutoConnect = false;
|
||||||
bool startupAutoConnectAttempted = false;
|
bool startupAutoConnectAttempted = false;
|
||||||
bool firstRunWizardActive = false;
|
bool firstRunWizardActive = false;
|
||||||
|
|
|
||||||
|
|
@ -44,39 +44,123 @@ std::wstring processLabel(const Holder &holder)
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Joins the per-holder lines into the one line the installer logs and cuts it to what one
|
||||||
|
/// NSIS string holds.
|
||||||
|
std::wstring joinReport(const std::vector<std::wstring> &lines)
|
||||||
|
{
|
||||||
|
std::wstring report;
|
||||||
|
for (const std::wstring &line : lines) {
|
||||||
|
if (!report.empty()) {
|
||||||
|
report += L"; ";
|
||||||
|
}
|
||||||
|
report += line;
|
||||||
|
}
|
||||||
|
if (report.size() > REPORT_CHAR_MAX) {
|
||||||
|
report.erase(REPORT_CHAR_MAX);
|
||||||
|
report += L"...";
|
||||||
|
}
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
struct WindowSearch
|
struct WindowSearch
|
||||||
{
|
{
|
||||||
DWORD processId = 0;
|
DWORD processId = 0;
|
||||||
HWND topmost = nullptr; ///< first visible window in Z-order: a dialog when one is up
|
std::vector<HWND> windows; ///< every top-level window of the process, in Z-order
|
||||||
HWND main = nullptr; ///< first visible window without an owner: the one to close
|
|
||||||
HWND any = nullptr; ///< first window at all, used when nothing is visible
|
|
||||||
};
|
};
|
||||||
|
|
||||||
BOOL CALLBACK findProcessWindow(HWND window, LPARAM parameter)
|
BOOL CALLBACK collectProcessWindow(HWND window, LPARAM parameter)
|
||||||
{
|
{
|
||||||
auto &search = *reinterpret_cast<WindowSearch *>(parameter);
|
auto &search = *reinterpret_cast<WindowSearch *>(parameter);
|
||||||
DWORD processId = 0;
|
DWORD processId = 0;
|
||||||
GetWindowThreadProcessId(window, &processId);
|
GetWindowThreadProcessId(window, &processId);
|
||||||
if (processId != search.processId) {
|
if (processId == search.processId) {
|
||||||
return TRUE;
|
search.windows.push_back(window);
|
||||||
}
|
|
||||||
if (search.any == nullptr) {
|
|
||||||
search.any = window;
|
|
||||||
}
|
|
||||||
// A tray icon leaves the main window hidden, so a window that is not visible is still
|
|
||||||
// kept as a fallback. EnumWindows walks the Z-order top down, so the first visible
|
|
||||||
// window is the one the user would see in front.
|
|
||||||
if (IsWindowVisible(window) != FALSE) {
|
|
||||||
if (search.topmost == nullptr) {
|
|
||||||
search.topmost = window;
|
|
||||||
}
|
|
||||||
if (search.main == nullptr && GetWindow(window, GW_OWNER) == nullptr) {
|
|
||||||
search.main = window;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a window is one the user could be asked to answer.
|
||||||
|
///
|
||||||
|
/// Only a framed, non-empty window qualifies. Qt keeps hidden top-level windows of its own
|
||||||
|
/// next to the application's: the tray icon creates one to receive the TaskbarCreated
|
||||||
|
/// message, styled WS_OVERLAPPED - which is no frame at all - and titled
|
||||||
|
/// QTrayIconMessageWindow. Showing one of those puts an empty white rectangle on the
|
||||||
|
/// taskbar that nothing ever closes again: it has no window procedure of its own to hide it,
|
||||||
|
/// and the process is busy in the loop this is meant to break, so it stays until the process
|
||||||
|
/// dies. Posting a close to one is worse than useless too: Qt's tray window answers WM_CLOSE
|
||||||
|
/// by asking the application to terminate, without going through the confirmation the close
|
||||||
|
/// is supposed to reach.
|
||||||
|
bool isAnswerableWindow(HWND window)
|
||||||
|
{
|
||||||
|
if ((GetWindowLongPtr(window, GWL_EXSTYLE) & WS_EX_TOOLWINDOW) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((GetWindowLongPtr(window, GWL_STYLE) & WS_CAPTION) == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RECT bounds{};
|
||||||
|
if (GetWindowRect(window, &bounds) == FALSE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return bounds.right > bounds.left && bounds.bottom > bounds.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Picks the window to post the close to and the one the user should end up looking at.
|
||||||
|
struct CloseTarget
|
||||||
|
{
|
||||||
|
HWND main = nullptr; ///< first visible window without an owner: the application's own
|
||||||
|
HWND toRaise = nullptr; ///< topmost visible window: a dialog when one is up
|
||||||
|
bool modalOpen = false; ///< the window a close would reach is disabled, so a dialog of its own is up
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Only the windows the user could answer are candidates - see isAnswerableWindow. An
|
||||||
|
/// instance left with nothing but Qt's own bookkeeping windows visible has no window to ask:
|
||||||
|
/// the caller reports that instead of posting a close the application would answer with a
|
||||||
|
/// terminate that skips its own confirmation.
|
||||||
|
CloseTarget chooseCloseTarget(const std::vector<HWND> &windows)
|
||||||
|
{
|
||||||
|
CloseTarget target;
|
||||||
|
HWND topmost = nullptr;
|
||||||
|
for (const HWND window : windows) {
|
||||||
|
if (IsWindowVisible(window) == FALSE || !isAnswerableWindow(window)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (topmost == nullptr) {
|
||||||
|
topmost = window;
|
||||||
|
}
|
||||||
|
if (target.main == nullptr && GetWindow(window, GW_OWNER) == nullptr) {
|
||||||
|
target.main = window;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target.toRaise = topmost != nullptr ? topmost : target.main;
|
||||||
|
// A modal dialog disables the windows behind it, so a disabled main window is the
|
||||||
|
// instance asking a question of its own.
|
||||||
|
target.modalOpen =
|
||||||
|
target.toRaise != nullptr && IsWindowEnabled(target.main != nullptr ? target.main : target.toRaise) == FALSE;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What is left of the window the close was posted to, so a log line tells an instance that
|
||||||
|
/// answered by hiding from one that never answered at all. A process that hid its window and
|
||||||
|
/// stayed alive is the one that needs the next close to bring it back into view.
|
||||||
|
std::wstring describeWindowState(HWND window)
|
||||||
|
{
|
||||||
|
if (window == nullptr || IsWindow(window) == FALSE) {
|
||||||
|
return L", its window is gone";
|
||||||
|
}
|
||||||
|
// WM_NULL is the cheapest way to ask whether the thread behind the window still dispatches.
|
||||||
|
// A hung instance does not answer at all, which is a different problem from one that
|
||||||
|
// answered the close and stayed open, and the two want different words in the prompt.
|
||||||
|
DWORD_PTR ignored = 0;
|
||||||
|
const bool responding =
|
||||||
|
SendMessageTimeoutW(window, WM_NULL, 0, 0, SMTO_ABORTIFHUNG | SMTO_BLOCK, 1000, &ignored) != 0;
|
||||||
|
std::wstring state = IsWindowVisible(window) != FALSE ? L", window still open" : L", window hidden again";
|
||||||
|
if (!responding) {
|
||||||
|
state += L" and not responding";
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
/// Restores a minimized window and takes the foreground, so a confirmation Cockatrice
|
/// Restores a minimized window and takes the foreground, so a confirmation Cockatrice
|
||||||
/// answers the close with is in front of the installer instead of behind it.
|
/// answers the close with is in front of the installer instead of behind it.
|
||||||
///
|
///
|
||||||
|
|
@ -133,6 +217,7 @@ CloseHoldersResult closeCockatriceHolders(const std::vector<Holder> &holders, un
|
||||||
std::vector<std::wstring> lines;
|
std::vector<std::wstring> lines;
|
||||||
std::vector<HANDLE> asked;
|
std::vector<HANDLE> asked;
|
||||||
std::vector<std::size_t> askedLines;
|
std::vector<std::size_t> askedLines;
|
||||||
|
std::vector<HWND> askedWindows;
|
||||||
|
|
||||||
for (const Holder &holder : holders) {
|
for (const Holder &holder : holders) {
|
||||||
if (holder.isService || !isCockatriceImage(holder.imageName)) {
|
if (holder.isService || !isCockatriceImage(holder.imageName)) {
|
||||||
|
|
@ -144,23 +229,41 @@ CloseHoldersResult closeCockatriceHolders(const std::vector<Holder> &holders, un
|
||||||
|
|
||||||
WindowSearch search;
|
WindowSearch search;
|
||||||
search.processId = holder.processId;
|
search.processId = holder.processId;
|
||||||
EnumWindows(findProcessWindow, reinterpret_cast<LPARAM>(&search));
|
EnumWindows(collectProcessWindow, reinterpret_cast<LPARAM>(&search));
|
||||||
if (search.any == nullptr) {
|
if (search.windows.empty()) {
|
||||||
lines.push_back(processLabel(holder) + L" has no window to close, left to you");
|
lines.push_back(processLabel(holder) + L" has no window to close, left to you");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HWND target =
|
const CloseTarget target = chooseCloseTarget(search.windows);
|
||||||
search.main != nullptr ? search.main : (search.topmost != nullptr ? search.topmost : search.any);
|
if (target.main == nullptr) {
|
||||||
const HWND toRaise = search.topmost != nullptr ? search.topmost : target;
|
// Nothing visible that a user could answer: the instance is hidden - in the
|
||||||
|
// notification area, say - or has nothing but Qt's own bookkeeping windows on
|
||||||
|
// screen. Neither is a window a close could reach, and showing one from here does
|
||||||
|
// not help: Windows would display it while the application still believes it is
|
||||||
|
// hidden, so it stays blank and answers nothing. The instance is named and left to
|
||||||
|
// the user, who can open it from the notification area.
|
||||||
|
lines.push_back(processLabel(holder) + L" has no window to ask, left to you");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
std::wstring line = processLabel(holder);
|
std::wstring line = processLabel(holder);
|
||||||
line += bringForward(toRaise) ? L" brought forward" : L" could not be brought forward";
|
line += bringForward(target.toRaise) ? L" brought forward" : L" could not be brought forward";
|
||||||
|
|
||||||
|
// Posting the close to an instance that is asking a question of its own is what made
|
||||||
|
// an earlier attempt useless: the client accepts it, hides this window and the dialog
|
||||||
|
// with it, and stays alive inside the nested loop the dialog opened, where nothing can
|
||||||
|
// reach it any more. The question is the request now, so it is brought forward and
|
||||||
|
// left alone.
|
||||||
|
if (target.modalOpen) {
|
||||||
|
lines.push_back(line + L", and is waiting for an answer to a question of its own");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Kept open across the wait so a pid reused after the process exits cannot be
|
// Kept open across the wait so a pid reused after the process exits cannot be
|
||||||
// mistaken for the instance that was asked to close.
|
// mistaken for the instance that was asked to close.
|
||||||
const HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, holder.processId);
|
const HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, holder.processId);
|
||||||
const DWORD openError = process == nullptr ? GetLastError() : ERROR_SUCCESS;
|
const DWORD openError = process == nullptr ? GetLastError() : ERROR_SUCCESS;
|
||||||
if (PostMessageW(target, WM_CLOSE, 0, 0) == FALSE) {
|
if (PostMessageW(target.main, WM_CLOSE, 0, 0) == FALSE) {
|
||||||
// Read the error before CloseHandle, which is free to reset it.
|
// Read the error before CloseHandle, which is free to reset it.
|
||||||
const DWORD postError = GetLastError();
|
const DWORD postError = GetLastError();
|
||||||
if (process != nullptr) {
|
if (process != nullptr) {
|
||||||
|
|
@ -177,6 +280,7 @@ CloseHoldersResult closeCockatriceHolders(const std::vector<Holder> &holders, un
|
||||||
} else {
|
} else {
|
||||||
askedLines.push_back(lines.size());
|
askedLines.push_back(lines.size());
|
||||||
asked.push_back(process);
|
asked.push_back(process);
|
||||||
|
askedWindows.push_back(target.main);
|
||||||
}
|
}
|
||||||
lines.push_back(std::move(line));
|
lines.push_back(std::move(line));
|
||||||
}
|
}
|
||||||
|
|
@ -186,8 +290,12 @@ CloseHoldersResult closeCockatriceHolders(const std::vector<Holder> &holders, un
|
||||||
const bool allExited = waitForExit(asked, waitMilliseconds, elapsed);
|
const bool allExited = waitForExit(asked, waitMilliseconds, elapsed);
|
||||||
for (std::size_t index = 0; index < asked.size(); ++index) {
|
for (std::size_t index = 0; index < asked.size(); ++index) {
|
||||||
const bool exited = allExited || WaitForSingleObject(asked[index], 0) == WAIT_OBJECT_0;
|
const bool exited = allExited || WaitForSingleObject(asked[index], 0) == WAIT_OBJECT_0;
|
||||||
lines[askedLines[index]] += exited ? L", exited after " + std::to_wstring(elapsed) + L" ms"
|
if (exited) {
|
||||||
: L", still running after " + std::to_wstring(elapsed) + L" ms";
|
lines[askedLines[index]] += L", exited after " + std::to_wstring(elapsed) + L" ms";
|
||||||
|
} else {
|
||||||
|
lines[askedLines[index]] += L", still running after " + std::to_wstring(elapsed) + L" ms" +
|
||||||
|
describeWindowState(askedWindows[index]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const HANDLE process : asked) {
|
for (const HANDLE process : asked) {
|
||||||
|
|
@ -198,15 +306,61 @@ CloseHoldersResult closeCockatriceHolders(const std::vector<Holder> &holders, un
|
||||||
result.report = L"nothing is holding the files";
|
result.report = L"nothing is holding the files";
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
for (const std::wstring &line : lines) {
|
result.report = joinReport(lines);
|
||||||
if (!result.report.empty()) {
|
return result;
|
||||||
result.report += L"; ";
|
}
|
||||||
}
|
|
||||||
result.report += line;
|
EndHoldersResult endCockatriceHolders(const std::vector<Holder> &holders, unsigned long waitMilliseconds)
|
||||||
}
|
{
|
||||||
if (result.report.size() > REPORT_CHAR_MAX) {
|
EndHoldersResult result;
|
||||||
result.report.erase(REPORT_CHAR_MAX);
|
std::vector<std::wstring> lines;
|
||||||
result.report += L"...";
|
std::vector<HANDLE> ended;
|
||||||
}
|
std::vector<std::size_t> endedLines;
|
||||||
|
|
||||||
|
for (const Holder &holder : holders) {
|
||||||
|
if (holder.isService || !isCockatriceImage(holder.imageName)) {
|
||||||
|
// The same split as the close: Oracle writes the card database in place and a
|
||||||
|
// service has nothing to ask, so neither is ever ended here.
|
||||||
|
lines.push_back(processLabel(holder) + L" left to you");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HANDLE process = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, holder.processId);
|
||||||
|
if (process == nullptr) {
|
||||||
|
lines.push_back(processLabel(holder) + L" could not be ended (win32 " + std::to_wstring(GetLastError()) +
|
||||||
|
L")");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (TerminateProcess(process, 1) == FALSE) {
|
||||||
|
// Read the error before CloseHandle, which is free to reset it.
|
||||||
|
const DWORD error = GetLastError();
|
||||||
|
CloseHandle(process);
|
||||||
|
lines.push_back(processLabel(holder) + L" could not be ended (win32 " + std::to_wstring(error) + L")");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++result.ended;
|
||||||
|
endedLines.push_back(lines.size());
|
||||||
|
lines.push_back(processLabel(holder) + L" ended");
|
||||||
|
ended.push_back(process);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ended.empty() && waitMilliseconds > 0) {
|
||||||
|
unsigned long long elapsed = 0;
|
||||||
|
const bool allExited = waitForExit(ended, waitMilliseconds, elapsed);
|
||||||
|
for (std::size_t index = 0; index < ended.size(); ++index) {
|
||||||
|
const bool exited = allExited || WaitForSingleObject(ended[index], 0) == WAIT_OBJECT_0;
|
||||||
|
lines[endedLines[index]] += exited ? L", exited after " + std::to_wstring(elapsed) + L" ms"
|
||||||
|
: L", still running after " + std::to_wstring(elapsed) + L" ms";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const HANDLE process : ended) {
|
||||||
|
CloseHandle(process);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines.empty()) {
|
||||||
|
result.report = L"nothing is holding the files";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.report = joinReport(lines);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,19 +28,55 @@ struct CloseHoldersResult
|
||||||
* asked to close. Oracle and Servatrice are left to the user - Oracle writes the card
|
* asked to close. Oracle and Servatrice are left to the user - Oracle writes the card
|
||||||
* database in place - and so is every service.
|
* database in place - and so is every service.
|
||||||
*
|
*
|
||||||
* Two things happen per instance. Its top-level window is restored and brought to the
|
* One thing happens per instance: its main window is brought to the foreground, because
|
||||||
* foreground, because Cockatrice may answer the close with a confirmation of its own that
|
* Cockatrice may answer the close with a confirmation of its own that would otherwise open
|
||||||
* would otherwise open behind the installer's prompt, and then WM_CLOSE is posted to the
|
* behind the installer's prompt. Then WM_CLOSE is posted to the main window, the same
|
||||||
* main window, the same request as clicking its close button. Stopping the process is
|
* request as clicking its close button - unless the instance is asking a question of its
|
||||||
* never attempted.
|
* own, which disables that window: posting the close then only hides the question, so it is
|
||||||
|
* brought forward and left to the user. An instance with no window a user could answer -
|
||||||
|
* one hidden in the notification area, say - is named and left to the user as well; showing
|
||||||
|
* its windows from here does not help, because the application would still believe they are
|
||||||
|
* hidden and never paint them. Stopping the process is never attempted.
|
||||||
*
|
*
|
||||||
* Once the requests are out, this waits up to @c waitMilliseconds for the processes to
|
* Once the requests are out, this waits up to @c waitMilliseconds for the processes to
|
||||||
* exit, so the caller can measure the purge immediately afterwards instead of sleeping
|
* exit, so the caller can measure the purge immediately afterwards instead of sleeping
|
||||||
* for a fixed time or re-asking a question this call already answered.
|
* for a fixed time or re-asking a question this call already answered. A process that is
|
||||||
|
* still running is described further: whether its window is gone, still open, hidden
|
||||||
|
* again, and whether it answers at all, so the log can tell an instance that refused the
|
||||||
|
* close from one that never received it.
|
||||||
*
|
*
|
||||||
* @param holders the Restart Manager answer, one entry per application or service
|
* @param holders the Restart Manager answer, one entry per application or service
|
||||||
* @param waitMilliseconds how long to wait for the asked instances to exit, 0 to not wait
|
* @param waitMilliseconds how long to wait for the asked instances to exit, 0 to not wait
|
||||||
*/
|
*/
|
||||||
CloseHoldersResult closeCockatriceHolders(const std::vector<Holder> &holders, unsigned long waitMilliseconds);
|
CloseHoldersResult closeCockatriceHolders(const std::vector<Holder> &holders, unsigned long waitMilliseconds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief What ending the holders did, in the form the installer logs.
|
||||||
|
*
|
||||||
|
* @c report is one line naming every holder, including the ones this deliberately leaves
|
||||||
|
* alone, because the installer repeats it in its log.
|
||||||
|
*/
|
||||||
|
struct EndHoldersResult
|
||||||
|
{
|
||||||
|
unsigned int ended = 0; ///< holders TerminateProcess was accepted for
|
||||||
|
std::wstring report; ///< one line, narrowed to ASCII when written
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Ends every running Cockatrice the probe named.
|
||||||
|
*
|
||||||
|
* This is the last resort behind the installer's "end it now" answer, never a first move: a
|
||||||
|
* client that is still running may have an unsaved deck, an open game or a settings write in
|
||||||
|
* flight, so the caller only gets here after the user picked it from a prompt that says so.
|
||||||
|
* Only cockatrice.exe is ended - Oracle writes the card database in place and Servatrice is a
|
||||||
|
* service, so both are named and left to the user, exactly as they are for the close.
|
||||||
|
*
|
||||||
|
* Waits up to @c waitMilliseconds for what it ended to be gone, so the caller can measure the
|
||||||
|
* purge immediately afterwards instead of racing a process that is still exiting.
|
||||||
|
*
|
||||||
|
* @param holders what the probe named, one entry per application or service
|
||||||
|
* @param waitMilliseconds how long to wait for the ended instances to be gone, 0 to not wait
|
||||||
|
*/
|
||||||
|
EndHoldersResult endCockatriceHolders(const std::vector<Holder> &holders, unsigned long waitMilliseconds);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,11 @@ constexpr wchar_t INSTALL_DIR_VARIABLE[] = L"COCKATRICE_GUARD_DIR";
|
||||||
/// exit it is waiting for.
|
/// exit it is waiting for.
|
||||||
constexpr unsigned long CLOSE_WAIT_MILLISECONDS = 30000;
|
constexpr unsigned long CLOSE_WAIT_MILLISECONDS = 30000;
|
||||||
|
|
||||||
|
/// How long the end mode waits for what it terminated to be gone. TerminateProcess is
|
||||||
|
/// asynchronous, and the installer measures the purge right after this returns, so a process
|
||||||
|
/// that is still tearing down would otherwise be reported as a holder for one more round.
|
||||||
|
constexpr unsigned long END_WAIT_MILLISECONDS = 5000;
|
||||||
|
|
||||||
/// Exit codes the installer reads. "Something was found" is deliberately not 0, so an
|
/// Exit codes the installer reads. "Something was found" is deliberately not 0, so an
|
||||||
/// answer of "nothing holds the files" stays distinguishable from a probe that failed.
|
/// answer of "nothing holds the files" stays distinguishable from a probe that failed.
|
||||||
/// In the close mode 1 means a close was actually sent and 0 that every holder was left
|
/// In the close mode 1 means a close was actually sent and 0 that every holder was left
|
||||||
|
|
@ -34,22 +39,40 @@ enum ExitCode
|
||||||
EXIT_PROBE_FAILED = 3,
|
EXIT_PROBE_FAILED = 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The probe has two modes, selected by the first argument:
|
/// The probe has three modes, selected by the first argument:
|
||||||
///
|
///
|
||||||
/// (none) name what holds the payload, one line, for the guard prompt and the
|
/// (none) name what holds the payload, one line, for the guard prompt and the
|
||||||
/// abort dialog/log
|
/// abort dialog/log
|
||||||
/// --close-holders bring the running Cockatrice forward, ask it to close, and wait for
|
/// --close-holders bring the running Cockatrice forward, ask it to close, and wait for
|
||||||
/// it to exit, for the offer to close after a failed purge
|
/// it to exit, for the offer to close after a failed purge
|
||||||
|
/// --end-holders end the running Cockatrice and wait for it to be gone, for the last
|
||||||
|
/// resort the guard prompt offers after a close was asked for twice
|
||||||
///
|
///
|
||||||
/// Both answer from two lookups that cover each other's blind spot: the Restart Manager
|
/// All three answer from two lookups that cover each other's blind spot: the Restart Manager
|
||||||
/// names whoever has a payload file open, and every process running from the directory is
|
/// names whoever has a payload file open, and every process running from the directory is
|
||||||
/// named as well, because a running instance holds its own image and the modules it
|
/// named as well, because a running instance holds its own image and the modules it
|
||||||
/// loaded as mappings rather than open files and the Restart Manager does not always
|
/// loaded as mappings rather than open files and the Restart Manager does not always
|
||||||
/// report those. Neither lookup alone is the guard's authority: an instance the file
|
/// report those. Neither lookup alone is the guard's authority: an instance the file
|
||||||
/// handles do not name is exactly the case that left a half-replaced install behind.
|
/// handles do not name is exactly the case that left a half-replaced install behind.
|
||||||
bool closeModeRequested(int argumentCount, char **arguments)
|
enum class Mode
|
||||||
{
|
{
|
||||||
return argumentCount > 1 && std::string(arguments[1]) == "--close-holders";
|
Name, ///< name the holders
|
||||||
|
Close, ///< ask the holders to close
|
||||||
|
End, ///< end the holders
|
||||||
|
};
|
||||||
|
|
||||||
|
Mode requestedMode(int argumentCount, char **arguments)
|
||||||
|
{
|
||||||
|
if (argumentCount > 1) {
|
||||||
|
const std::string mode(arguments[1]);
|
||||||
|
if (mode == "--close-holders") {
|
||||||
|
return Mode::Close;
|
||||||
|
}
|
||||||
|
if (mode == "--end-holders") {
|
||||||
|
return Mode::End;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Mode::Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes one line to the pipe nsExec reads. Narrowed to ASCII on purpose: the bytes
|
/// Writes one line to the pipe nsExec reads. Narrowed to ASCII on purpose: the bytes
|
||||||
|
|
@ -96,10 +119,17 @@ int main(int argumentCount, char **arguments)
|
||||||
// to close, and the Restart Manager may not name any of them (see
|
// to close, and the Restart Manager may not name any of them (see
|
||||||
// findProcessesRunningFrom). The file holders follow, deduplicated by process, so an
|
// findProcessesRunningFrom). The file holders follow, deduplicated by process, so an
|
||||||
// instance that both runs from the directory and holds a payload file is named once.
|
// instance that both runs from the directory and holds a payload file is named once.
|
||||||
|
// Neither lookup may name the probe or the process that started it, whichever one found
|
||||||
|
// it: an installer or uninstaller running from the directory it is replacing would
|
||||||
|
// otherwise be named as a holder, and closing it is a request nobody can answer.
|
||||||
std::vector<Holder> holders = findProcessesRunningFrom(installDir);
|
std::vector<Holder> holders = findProcessesRunningFrom(installDir);
|
||||||
|
const std::vector<unsigned long> asking = askingProcessIds();
|
||||||
std::vector<Holder> fileHolders;
|
std::vector<Holder> fileHolders;
|
||||||
const unsigned long error = findProcessesUsingFiles(collectPayloadFiles(installDir), fileHolders);
|
const unsigned long error = findProcessesUsingFiles(collectPayloadFiles(installDir), fileHolders);
|
||||||
for (const Holder &holder : fileHolders) {
|
for (const Holder &holder : fileHolders) {
|
||||||
|
if (std::find(asking.begin(), asking.end(), holder.processId) != asking.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const bool alreadyNamed = std::any_of(holders.begin(), holders.end(), [&holder](const Holder &named) {
|
const bool alreadyNamed = std::any_of(holders.begin(), holders.end(), [&holder](const Holder &named) {
|
||||||
return named.processId == holder.processId;
|
return named.processId == holder.processId;
|
||||||
});
|
});
|
||||||
|
|
@ -116,20 +146,25 @@ int main(int argumentCount, char **arguments)
|
||||||
return EXIT_PROBE_FAILED;
|
return EXIT_PROBE_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool closeMode = closeModeRequested(argumentCount, arguments);
|
const Mode mode = requestedMode(argumentCount, arguments);
|
||||||
if (holders.empty()) {
|
if (holders.empty()) {
|
||||||
// No output in the naming mode: the installer words the "nothing is holding them"
|
// No output in the naming mode: the installer words the "nothing is holding them"
|
||||||
// case, and it owns the message either way. The close mode says it itself, because
|
// case, and it owns the message either way. The acting modes say it themselves,
|
||||||
// its line is logged verbatim next to what it was asked to do.
|
// because their line is logged verbatim next to what they were asked to do.
|
||||||
if (closeMode) {
|
if (mode != Mode::Name) {
|
||||||
writeLine(L"nothing is holding the files");
|
writeLine(L"nothing is holding the files");
|
||||||
}
|
}
|
||||||
return EXIT_NO_HOLDERS;
|
return EXIT_NO_HOLDERS;
|
||||||
}
|
}
|
||||||
if (!closeMode) {
|
if (mode == Mode::Name) {
|
||||||
writeLine(formatHolders(holders));
|
writeLine(formatHolders(holders));
|
||||||
return EXIT_HOLDERS_FOUND;
|
return EXIT_HOLDERS_FOUND;
|
||||||
}
|
}
|
||||||
|
if (mode == Mode::End) {
|
||||||
|
const EndHoldersResult result = endCockatriceHolders(holders, END_WAIT_MILLISECONDS);
|
||||||
|
writeLine(result.report);
|
||||||
|
return result.ended > 0 ? EXIT_HOLDERS_FOUND : EXIT_NO_HOLDERS;
|
||||||
|
}
|
||||||
|
|
||||||
const CloseHoldersResult result = closeCockatriceHolders(holders, CLOSE_WAIT_MILLISECONDS);
|
const CloseHoldersResult result = closeCockatriceHolders(holders, CLOSE_WAIT_MILLISECONDS);
|
||||||
writeLine(result.report);
|
writeLine(result.report);
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,28 @@ constexpr std::size_t MAX_PROBED_FILES = 2000;
|
||||||
/// How deep the Plugins walk goes. Its real shape is Plugins\<category>\<plugin>.dll.
|
/// How deep the Plugins walk goes. Its real shape is Plugins\<category>\<plugin>.dll.
|
||||||
constexpr int MAX_PLUGIN_DEPTH = 4;
|
constexpr int MAX_PLUGIN_DEPTH = 4;
|
||||||
|
|
||||||
|
/// How many parents askingProcessIds() walks up before it stops on its own. A real chain
|
||||||
|
/// is the probe, the installer or uninstaller, and the shell that started it; the bound
|
||||||
|
/// only keeps a snapshot with a cycle in it from walking forever.
|
||||||
|
constexpr int MAX_ASKER_LEVELS = 8;
|
||||||
|
|
||||||
/// The payload files the installer replaces by name; the DLLs are matched by mask below.
|
/// The payload files the installer replaces by name; the DLLs are matched by mask below.
|
||||||
constexpr const wchar_t *PAYLOAD_FILES[] = {L"cockatrice.exe", L"oracle.exe", L"servatrice.exe", L"qt.conf"};
|
constexpr const wchar_t *PAYLOAD_FILES[] = {L"cockatrice.exe", L"oracle.exe", L"servatrice.exe", L"qt.conf"};
|
||||||
|
|
||||||
|
/// The payload programs, by image name. A process running one of these is what the guard
|
||||||
|
/// exists to name, so askingProcessIds() stops its walk up the parent chain at one.
|
||||||
|
constexpr const wchar_t *PAYLOAD_PROGRAMS[] = {L"cockatrice.exe", L"oracle.exe", L"servatrice.exe"};
|
||||||
|
|
||||||
|
bool isPayloadProgram(const std::wstring &imageName)
|
||||||
|
{
|
||||||
|
for (const wchar_t *program : PAYLOAD_PROGRAMS) {
|
||||||
|
if (CompareStringOrdinal(imageName.c_str(), -1, program, -1, TRUE) == CSTR_EQUAL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void collectFilesOf(const std::wstring &directory, int depth, int maxDepth, std::vector<std::wstring> &files)
|
void collectFilesOf(const std::wstring &directory, int depth, int maxDepth, std::vector<std::wstring> &files)
|
||||||
{
|
{
|
||||||
if (files.size() >= MAX_PROBED_FILES) {
|
if (files.size() >= MAX_PROBED_FILES) {
|
||||||
|
|
@ -214,6 +233,45 @@ unsigned long findProcessesUsingFiles(const std::vector<std::wstring> &files, st
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned long> askingProcessIds()
|
||||||
|
{
|
||||||
|
std::vector<unsigned long> asking;
|
||||||
|
asking.push_back(GetCurrentProcessId());
|
||||||
|
|
||||||
|
// One snapshot for the whole walk: the parent ids come from it, so a level costs no
|
||||||
|
// second lookup.
|
||||||
|
std::vector<PROCESSENTRY32W> processes;
|
||||||
|
const HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||||
|
if (snapshot == INVALID_HANDLE_VALUE) {
|
||||||
|
return asking;
|
||||||
|
}
|
||||||
|
PROCESSENTRY32W entry{};
|
||||||
|
entry.dwSize = sizeof(entry);
|
||||||
|
if (Process32FirstW(snapshot, &entry) != FALSE) {
|
||||||
|
do {
|
||||||
|
processes.push_back(entry);
|
||||||
|
} while (Process32NextW(snapshot, &entry) != FALSE);
|
||||||
|
}
|
||||||
|
CloseHandle(snapshot);
|
||||||
|
|
||||||
|
unsigned long current = asking.back();
|
||||||
|
for (int level = 0; level < MAX_ASKER_LEVELS; ++level) {
|
||||||
|
const auto named = std::find_if(processes.begin(), processes.end(), [current](const PROCESSENTRY32W &process) {
|
||||||
|
return process.th32ProcessID == current;
|
||||||
|
});
|
||||||
|
if (named == processes.end() || named->th32ParentProcessID == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const unsigned long parent = named->th32ParentProcessID;
|
||||||
|
if (isPayloadProgram(fileNameOf(imagePathOf(parent)))) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
asking.push_back(parent);
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
return asking;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<Holder> findProcessesRunningFrom(const std::wstring &installDir)
|
std::vector<Holder> findProcessesRunningFrom(const std::wstring &installDir)
|
||||||
{
|
{
|
||||||
std::vector<Holder> holders;
|
std::vector<Holder> holders;
|
||||||
|
|
@ -222,10 +280,10 @@ std::vector<Holder> findProcessesRunningFrom(const std::wstring &installDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::wstring prefix = directoryPrefixOf(installDir);
|
const std::wstring prefix = directoryPrefixOf(installDir);
|
||||||
// Never the probe itself: it runs from the directory being replaced when the
|
// Never the probe, and never the installer or uninstaller that started it: either can
|
||||||
// uninstaller asks in place or the installer was copied next to the payload, and
|
// run from the directory being replaced, and naming one asks the user to close the
|
||||||
// naming it would ask the user to close the process that is doing the asking.
|
// program that is asking (see askingProcessIds).
|
||||||
const DWORD self = GetCurrentProcessId();
|
const std::vector<unsigned long> asking = askingProcessIds();
|
||||||
|
|
||||||
const HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
const HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||||
if (snapshot == INVALID_HANDLE_VALUE) {
|
if (snapshot == INVALID_HANDLE_VALUE) {
|
||||||
|
|
@ -235,7 +293,7 @@ std::vector<Holder> findProcessesRunningFrom(const std::wstring &installDir)
|
||||||
entry.dwSize = sizeof(entry);
|
entry.dwSize = sizeof(entry);
|
||||||
if (Process32FirstW(snapshot, &entry) != FALSE) {
|
if (Process32FirstW(snapshot, &entry) != FALSE) {
|
||||||
do {
|
do {
|
||||||
if (entry.th32ProcessID == self) {
|
if (std::find(asking.begin(), asking.end(), entry.th32ProcessID) != asking.end()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// One process at a time, because the path is per process; a process that
|
// One process at a time, because the path is per process; a process that
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,30 @@ unsigned long findProcessesUsingFiles(const std::vector<std::wstring> &files, st
|
||||||
* database server next to a Cockatrice install) is never named.
|
* database server next to a Cockatrice install) is never named.
|
||||||
*
|
*
|
||||||
* @param installDir the directory being replaced, as a full path.
|
* @param installDir the directory being replaced, as a full path.
|
||||||
* @return one entry per process running from it, in snapshot order, without the probe
|
* @return one entry per process running from it, in snapshot order, without the processes
|
||||||
* itself; empty when the directory is empty or no process runs from it.
|
* named by askingProcessIds(); empty when the directory is empty or no process
|
||||||
|
* runs from it.
|
||||||
*/
|
*/
|
||||||
std::vector<Holder> findProcessesRunningFrom(const std::wstring &installDir);
|
std::vector<Holder> findProcessesRunningFrom(const std::wstring &installDir);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The processes that must never be named as holders: the probe and the ones that
|
||||||
|
* started it.
|
||||||
|
*
|
||||||
|
* The installer or uninstaller doing the asking can legitimately run from the directory it
|
||||||
|
* is replacing - the old uninstaller is started with _?= so it stays in $INSTDIR, and an
|
||||||
|
* installer can be downloaded into the folder - and naming it asks the user to close the
|
||||||
|
* program that is asking. That cannot be answered: the close skips it, the end offer skips
|
||||||
|
* it, and the guard loops until the user cancels, which is what a run that named itself
|
||||||
|
* did.
|
||||||
|
*
|
||||||
|
* The walk up the parent chain stops at the first program the payload installs, because in
|
||||||
|
* an in-app update the installer's own parent is the Cockatrice instance being replaced -
|
||||||
|
* the one the guard exists to name.
|
||||||
|
*
|
||||||
|
* @return process ids, the probe first, then its ancestors while they are not payload
|
||||||
|
* programs; only the probe itself when the snapshot fails.
|
||||||
|
*/
|
||||||
|
std::vector<unsigned long> askingProcessIds();
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue