mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
[Installer] Graceful close and a way out of a stuck update (#7687)
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
* [Installer] Stop calling the payload free while an executable is locked The guard answered "nothing is holding the payload" from two lookups that can both miss a running instance: Restart Manager does not reliably report a program's own image or the modules it mapped, and the image path of a process running under another account cannot be read from the probe at all. A user log showed exactly that: the first guard passed while cockatrice.exe was running from the directory, the old uninstaller silently skipped the file it could not delete, and the update aborted half an uninstall later with "cancelled by user" after a close attempt the running window ignored. Ask the file itself. A running image is opened without write sharing, so the write open replacing it needs is refused with ERROR_SHARING_VIOLATION for as long as the program runs, while a file that is merely being read still accepts it. When neither lookup named anybody and a payload executable refuses that open, the probe answers that the file is in use and that the program holding it could not be identified - the guard's "could not confirm" dialog, whose Retry works once the user has closed it - instead of passing the payload as free. Only the payload executables are tested, not every file the purge removes: a locked executable proves a program is running and a mapped DLL belongs to a running executable, while a locked DLL or qt.conf can be the asking process itself, which is the loop the guard already had. The open is retried once after 300 ms, and only for the files that refused, so a scanner holding a file for a moment cannot stop an install for nothing. The Windows probe check now covers that answer: a payload executable held by the asking process, so that it cannot be named, has to come back as in use rather than as free. Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de> * [Installer] Close running clients over a local socket The installer could only ask a running client to close with WM_CLOSE, which an instance hidden in the notification area or sitting on its own confirmation cannot answer usefully. Give every client a per-process local socket and have the holder probe ask it there first, falling back to WM_CLOSE for clients from before the socket. * [Installer] Offer to check again or update by hand when files stay locked Replace the two-answer guard prompt with force close, close by hand and check again, or cancel. Retry the automatic close while the set of holders changes, and point a cancelled run at the latest release so a user who does not want anything killed has a way forward that is not cancelling the update again. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
720adf5410
commit
bbc7e462ff
25 changed files with 897 additions and 130 deletions
96
tools/installer-holder-probe/installer_close_request.cpp
Normal file
96
tools/installer-holder-probe/installer_close_request.cpp
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
#include "installer_close_request.h"
|
||||
|
||||
#include "installer_close_name.h"
|
||||
|
||||
// clang-format off
|
||||
// windows.h has to come before anything that pulls in a Windows type, as everywhere in
|
||||
// this tool.
|
||||
#include <windows.h>
|
||||
// clang-format on
|
||||
|
||||
#include <cstddef>
|
||||
#include <libcockatrice/utility/installer_close_protocol.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
/// How long a busy pipe may take before this gives up. The client answers in milliseconds;
|
||||
/// the wait only covers a machine under load.
|
||||
constexpr DWORD PIPE_WAIT_MILLISECONDS = 1000;
|
||||
|
||||
std::wstring environmentVariable(const wchar_t *name)
|
||||
{
|
||||
for (DWORD size = MAX_PATH; size <= 32768U; size *= 2) {
|
||||
std::vector<wchar_t> buffer(size);
|
||||
const DWORD length = GetEnvironmentVariableW(name, buffer.data(), size);
|
||||
if (length == 0) {
|
||||
return {};
|
||||
}
|
||||
if (length < size) {
|
||||
return std::wstring(buffer.data(), length);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/// The account name scopedLocalServerName() appends: USER, then USERNAME, then the last
|
||||
/// component of the profile path - the same order and the same fallbacks the client uses.
|
||||
std::wstring currentUserName()
|
||||
{
|
||||
for (const wchar_t *name : {L"USER", L"USERNAME"}) {
|
||||
const std::wstring value = environmentVariable(name);
|
||||
if (!value.empty()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
const std::wstring profile = environmentVariable(L"USERPROFILE");
|
||||
const std::size_t separator = profile.find_last_of(L"\\/");
|
||||
if (separator == std::wstring::npos) {
|
||||
return profile;
|
||||
}
|
||||
return profile.substr(separator + 1);
|
||||
}
|
||||
|
||||
/// The named pipe QLocalServer creates for the client's socket, or empty when the account
|
||||
/// name could not be read and there is nothing to connect to.
|
||||
std::wstring pipePath(unsigned long processId)
|
||||
{
|
||||
const std::wstring userName = currentUserName();
|
||||
if (userName.empty()) {
|
||||
return {};
|
||||
}
|
||||
return L"\\\\.\\pipe\\" + installerCloseSocketName(processId, userName);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool requestInstallerClose(unsigned long processId)
|
||||
{
|
||||
const std::wstring path = pipePath(processId);
|
||||
if (path.empty()) {
|
||||
return false;
|
||||
}
|
||||
// A client that is still serving the previous connection answers ERROR_PIPE_BUSY; one
|
||||
// retry covers that, while a client that is not listening at all (an older version, or
|
||||
// one that has already exited) is answered with a plain failure and the caller falls
|
||||
// back to WM_CLOSE.
|
||||
for (int attempt = 0; attempt < 2; ++attempt) {
|
||||
const HANDLE pipe = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (pipe == INVALID_HANDLE_VALUE) {
|
||||
if (GetLastError() == ERROR_PIPE_BUSY && WaitNamedPipeW(path.c_str(), PIPE_WAIT_MILLISECONDS) != FALSE) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
DWORD written = 0;
|
||||
const BOOL ok =
|
||||
WriteFile(pipe, INSTALLER_CLOSE_COMMAND, sizeof(INSTALLER_CLOSE_COMMAND) - 1, &written, nullptr);
|
||||
CloseHandle(pipe);
|
||||
// The request is in the pipe buffer, so the client reads it even though this handle
|
||||
// is closed immediately. No acknowledgment is waited for: the caller only needs to
|
||||
// know whether there was a listening instance to ask, and a hung client must not
|
||||
// stall the guard.
|
||||
return ok != FALSE && written == sizeof(INSTALLER_CLOSE_COMMAND) - 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue