mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
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>
81 lines
4 KiB
C++
81 lines
4 KiB
C++
#ifndef RESTART_MANAGER_HOLDERS_H
|
|
#define RESTART_MANAGER_HOLDERS_H
|
|
|
|
#include "holder_line.h"
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
/**
|
|
* @brief Lists the payload files, as full paths.
|
|
*
|
|
* The set is what the installer replaces and the purge removes: the three executables,
|
|
* qt.conf, every top-level DLL 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,
|
|
* so it cannot be mistaken for a running instance and block an upgrade.
|
|
*
|
|
* Restart Manager refuses a directory with ERROR_ACCESS_DENIED, which is why the probe
|
|
* registers files and this has to name them first. Missing files and unreadable
|
|
* directories are skipped rather than reported, and the list is capped: this runs before
|
|
* the first page and on the abort path, and must not walk a tree of downloaded pictures.
|
|
*/
|
|
std::vector<std::wstring> collectPayloadFiles(const std::wstring &installDir);
|
|
|
|
/**
|
|
* @brief Names the applications and services that have one of @p files open or mapped.
|
|
*
|
|
* Restart Manager reports both a process that loaded one of the files as a module and
|
|
* one that merely holds a handle to it, and it reports services, which is the whole
|
|
* reason it is used instead of walking every process's module list.
|
|
*
|
|
* @param holders replaced with one entry per holder, in the order Windows reported them.
|
|
* @return 0 when the lookup ran, even if nothing is holding the files; otherwise the
|
|
* Win32 code of the Restart Manager call that failed.
|
|
*/
|
|
unsigned long findProcessesUsingFiles(const std::vector<std::wstring> &files, std::vector<Holder> &holders);
|
|
|
|
/**
|
|
* @brief Lists every process whose image lives in @p installDir, whether or not it has a
|
|
* payload file open.
|
|
*
|
|
* Restart Manager answers the question "who has this file open", and a running instance
|
|
* does not necessarily have one open: its own image and the modules it loaded are mapped
|
|
* into it, not opened by it, and RmGetList does not reliably report a mapping. 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 - so an answer built on file handles alone cannot be the guard's authority.
|
|
*
|
|
* Reading the image path needs PROCESS_QUERY_LIMITED_INFORMATION and
|
|
* QueryFullProcessImageNameW, neither of which depends on the bitness of either process,
|
|
* so a 64-bit instance is named from this 32-bit stub. The comparison is on the
|
|
* directory, not on an image name, so a process that merely shares a name (an Oracle
|
|
* database server next to a Cockatrice install) is never named.
|
|
*
|
|
* @param installDir the directory being replaced, as a full path.
|
|
* @return one entry per process running from it, in snapshot order, without the processes
|
|
* named by askingProcessIds(); empty when the directory is empty or no process
|
|
* runs from it.
|
|
*/
|
|
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
|