mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
[CrashReporting] Name the files and processes behind a failed installer purge (#7590)
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
* [CrashReporting] Log which files and processes blocked the installer purge A purge that fails aborts the upgrade with a message that can only guess at the cause, and the log only listed which DLLs survived. Record what Windows reports for each survivor and which processes still have the install directory mapped, then repeat the holder list in the abort message. The diagnostics are additive and run only on the abort path: they open each survivor for DELETE access and close it again without deleting, so a failed upgrade can never end up half-removed. The purge itself is unchanged. * [CrashReporting] Name the holders of a failed purge with Restart Manager The process-and-module scan could only name a holder whose image lives in the install directory or that loaded one of its DLLs, so the case this diagnostic exists for - an antivirus scanner holding a plain handle - was reported as "no holder named, only a code". Reaching the Restart Manager from the NSIS script would have meant marshalling RM_PROCESS_INFO through System::Call as a table of hand-computed offsets, which cannot be checked without a Windows toolchain, and that is why it was dropped when this diagnostics was written. Move the call into tools/installer-holder-probe: a small executable the same Windows build compiles, linking nothing from this project so it still starts when the installed runtime is what the purge could not replace. It rides inside the installer and is extracted to $PLUGINSDIR only when the purge has already failed, so the successful path is unchanged and nothing new is installed. .ci/compile.sh runs it against a file held with FileShare.None on the Windows runners, so the lookup is exercised on real Windows instead of only compiled. * [CrashReporting] Fix the holder probe check and cap the holder line The probe block ran after the package steps, where the working directory is already the build dir, so its $BUILD_DIR-prefixed paths named build/build and the PowerShell file could not be written. Move it ahead of install/package, where the build dir is still the cwd, and name both files relative to it. Guard it on MAKE_NO_CLIENT, since the probe is only built with the client. Cap the rendered holder line at HOLDER_LINE_CHAR_MAX: the count limit alone does not keep long service names inside the NSIS string that also carries the abort text and the log path. Whole entries are kept and the rest counted, with the first holder always shown. Also run cmake-format on the probe's CMakeLists so the lint job accepts it. * [CrashReporting] Report a read-only purge survivor and bound the probe check --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
ad4320cd7b
commit
bbc6003f0a
12 changed files with 793 additions and 1 deletions
160
tools/installer-holder-probe/restart_manager_holders.cpp
Normal file
160
tools/installer-holder-probe/restart_manager_holders.cpp
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
#include "restart_manager_holders.h"
|
||||
|
||||
// clang-format off
|
||||
// restartmanager.h uses the types from windows.h without including it, so the order
|
||||
// here is a requirement; Regroup would sort them the other way round.
|
||||
#include <windows.h>
|
||||
#include <restartmanager.h>
|
||||
// clang-format on
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
|
||||
namespace
|
||||
{
|
||||
/// Stop registering after this many files. The install directory carries around 150
|
||||
/// files in practice; the cap only exists so a directory someone has filled with files
|
||||
/// cannot turn the abort path into a long Restart Manager call.
|
||||
constexpr std::size_t MAX_PROBED_FILES = 2000;
|
||||
|
||||
/// How deep the Plugins walk goes. Its real shape is Plugins\<category>\<plugin>.dll.
|
||||
constexpr int MAX_PLUGIN_DEPTH = 4;
|
||||
|
||||
void collectFilesOf(const std::wstring &directory, int depth, int maxDepth, std::vector<std::wstring> &files)
|
||||
{
|
||||
if (files.size() >= MAX_PROBED_FILES) {
|
||||
return;
|
||||
}
|
||||
WIN32_FIND_DATAW entry{};
|
||||
const HANDLE find = FindFirstFileW((directory + L"\\*").c_str(), &entry);
|
||||
if (find == INVALID_HANDLE_VALUE) {
|
||||
return;
|
||||
}
|
||||
do {
|
||||
if (files.size() >= MAX_PROBED_FILES) {
|
||||
break;
|
||||
}
|
||||
const std::wstring name = entry.cFileName;
|
||||
if (name == L"." || name == L".." || (entry.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) {
|
||||
continue;
|
||||
}
|
||||
const std::wstring path = directory + L"\\" + name;
|
||||
if ((entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
|
||||
if (depth < maxDepth) {
|
||||
collectFilesOf(path, depth + 1, maxDepth, files);
|
||||
}
|
||||
} else {
|
||||
files.push_back(path);
|
||||
}
|
||||
} while (FindNextFileW(find, &entry) != 0);
|
||||
FindClose(find);
|
||||
}
|
||||
|
||||
std::wstring imagePathOf(unsigned long processId)
|
||||
{
|
||||
// PROCESS_QUERY_LIMITED_INFORMATION is the right to ask, not to debug: it reads the
|
||||
// path of a process this one does not own, which is what keeps a SYSTEM-owned holder
|
||||
// from turning the whole list into an error. One that refuses even this is still
|
||||
// named, by whatever Restart Manager called it.
|
||||
const HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId);
|
||||
if (process == nullptr) {
|
||||
return {};
|
||||
}
|
||||
wchar_t path[MAX_PATH * 2] = {};
|
||||
DWORD length = static_cast<DWORD>(std::size(path));
|
||||
std::wstring imagePath;
|
||||
if (QueryFullProcessImageNameW(process, 0, path, &length) != 0) {
|
||||
imagePath.assign(path, length);
|
||||
}
|
||||
CloseHandle(process);
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
std::wstring fileNameOf(const std::wstring &path)
|
||||
{
|
||||
const std::size_t separator = path.find_last_of(L"\\/");
|
||||
return separator == std::wstring::npos ? path : path.substr(separator + 1);
|
||||
}
|
||||
|
||||
/// Copies a fixed-size, NUL-terminated WCHAR array the API filled in.
|
||||
template <std::size_t N> std::wstring fixedString(const wchar_t (&text)[N])
|
||||
{
|
||||
return std::wstring(text, std::find(text, text + N, L'\0'));
|
||||
}
|
||||
|
||||
Holder holderOf(const RM_PROCESS_INFO &info)
|
||||
{
|
||||
Holder holder;
|
||||
holder.processId = info.Process.dwProcessId;
|
||||
holder.isService = info.ApplicationType == RmService;
|
||||
holder.appName = fixedString(info.strAppName);
|
||||
if (holder.isService) {
|
||||
holder.serviceShortName = fixedString(info.strServiceShortName);
|
||||
}
|
||||
holder.imageName = fileNameOf(imagePathOf(holder.processId));
|
||||
return holder;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::vector<std::wstring> collectProbeFiles(const std::wstring &installDir)
|
||||
{
|
||||
std::vector<std::wstring> files;
|
||||
// depth 0, maxDepth 0: the install root's files and nothing below it. Its data
|
||||
// directory holds downloaded pictures, which the purge does not touch and which
|
||||
// would turn this into a walk over thousands of files.
|
||||
collectFilesOf(installDir, 0, 0, files);
|
||||
if (files.size() < MAX_PROBED_FILES) {
|
||||
collectFilesOf(installDir + L"\\Plugins", 0, MAX_PLUGIN_DEPTH, files);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
unsigned long findProcessesUsingFiles(const std::vector<std::wstring> &files, std::vector<Holder> &holders)
|
||||
{
|
||||
holders.clear();
|
||||
if (files.empty()) {
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
DWORD session = 0;
|
||||
wchar_t sessionKey[CCH_RM_SESSION_KEY + 1] = {};
|
||||
DWORD error = RmStartSession(&session, 0, sessionKey);
|
||||
if (error != ERROR_SUCCESS) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// One call for the whole set, as the API asks: RmRegisterResources does registry
|
||||
// work per file and is documented as too expensive to call once per file.
|
||||
std::vector<LPCWSTR> names;
|
||||
names.reserve(files.size());
|
||||
for (const std::wstring &file : files) {
|
||||
names.push_back(file.c_str());
|
||||
}
|
||||
error = RmRegisterResources(session, static_cast<UINT>(names.size()), names.data(), 0, nullptr, 0, nullptr);
|
||||
|
||||
if (error == ERROR_SUCCESS) {
|
||||
UINT needed = 0;
|
||||
UINT count = 0;
|
||||
DWORD rebootReasons = 0;
|
||||
// The first call asks how large the answer is; Restart Manager reports
|
||||
// ERROR_MORE_DATA rather than truncating a list that does not fit.
|
||||
error = RmGetList(session, &needed, &count, nullptr, &rebootReasons);
|
||||
// Bounded: each retry more than covers the list growing between the two calls,
|
||||
// and an abort path cannot chase a process that keeps opening files. Giving up
|
||||
// reports the code, which is a truthful answer instead of a truncated list.
|
||||
for (int attempt = 0; attempt < 3 && error == ERROR_MORE_DATA; ++attempt) {
|
||||
count = needed;
|
||||
std::vector<RM_PROCESS_INFO> infos(count);
|
||||
error = RmGetList(session, &needed, &count, infos.data(), &rebootReasons);
|
||||
if (error == ERROR_SUCCESS) {
|
||||
holders.reserve(count);
|
||||
for (UINT index = 0; index < count; ++index) {
|
||||
holders.push_back(holderOf(infos[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RmEndSession(session);
|
||||
return error;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue