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
* [Installer] Offer to end running Cockatrice immediately when auto-close fails * [Installer] Keep the way out that costs nothing in the first prompt The prompt now offers to end Cockatrice, so what the deleted close-again text said has to survive in it: an instance the probe reports as waiting for an answer to a question of its own has been brought forward, and answering that window is the way out that ends nothing. Say so, and say where to go when Yes leaves it listed - Task Manager - so an end that cannot take effect does not loop on the same dialog. * [Installer] Bring the comments back in line with the code GuardCloseTried no longer counts closes, and the file header, CloseHolders and GuardAppsNotRunning still described an escalation after a second close, as did the probe's --end-holders note. * [Installer] End Oracle and Servatrice too when the guard's end answer is confirmed The guard's end answer only ever stopped cockatrice.exe, so an Oracle holding the card database or a Servatrice service kept the payload locked and the prompt came back with nothing left that could free it. The probe now ends every program the payload installs, services included: isPayloadProgram() - the list restart_manager_holders already used for its parent walk - decides the set, so a scanner holding a DLL or a program the user keeps in the install directory is still named and left to the user. A service is terminated rather than sent a stop through the SCM, which notices the process exit and marks it stopped afterwards. The guard prompt now says what ending costs: an unsaved deck, an unwritten setting, players on a running Servatrice, and an Oracle mid-way through the card database. It also keeps the notification-area hint, which the probe's "has no window to ask" answer depends on, and drops the claim that Oracle and Servatrice had been brought forward. * [Installer] Give every running-program prompt the same end-or-retry answers Four prompts meet a running program and each offered a different set: the guard ended it, the launcher wait only kept waiting, and the recovery prompt after a failed purge could only ask Cockatrice to close. With Oracle or Servatrice holding the files, that last one had no answer that could free anything - Yes and No both retried the purge and Cancel aborted an install whose old version was already gone. All four now follow the guard's shape: one automatic attempt, then a prompt whose Yes ends the programs it names and whose No retries, with Cancel to give up. - RecoverFromHolders closes once and purges before it asks anything, then offers EndHolders - the same end answer as the guard - instead of a close the user had to trigger himself. - WaitForLauncherToExit can end the launcher by pid, the one process it holds a pid for, rather than looping 60 s waits with Cancel as the only way out. - END_COST and AFTER_CLOSING_GUIDANCE are !defines, so the cost of ending and the guidance for the answers that end nothing read the same in every prompt. * [Installer] End the launcher through the handle it was waited on The "end it" answer used to look the launcher's pid up again after a dialog that can stay open as long as the user takes, so a pid that had been reused in the meantime - or a /PID= that was never the launcher - would be terminated instead. The handle the wait already holds names that process and cannot be handed to another, so the terminate answer now acts on it. PROCESS_TERMINATE is requested along with PROCESS_SYNCHRONIZE; when only the synchronize right is granted the launcher is still waited on and the answer falls back to asking it to close, instead of reporting a live launcher as gone and writing the payload over it. * [Installer] Say what the end answer ends, and report a failed end The prompt offered "end every program listed above", but EndHolders only ends the programs the payload installs: a scanner holding a payload DLL, or a service the Restart Manager names by its long name, was listed and then silently left alone, so Yes looked like it did nothing and the same dialog came back unchanged. The wording now says which programs Yes ends and that anything else has to be closed by hand, and the round after an end that did not free the files says so, so the same dialog no longer reappears word for word. The three prompts are one macro so they cannot drift apart again. The added sentences eat into the 1024 characters one NSIS string holds, so the holder line's budget drops from 400 to 320 characters to keep the longest message (cancel line included) inside it: 977 characters, measured against the worst case the probe can produce. --------- Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
173 lines
7.5 KiB
C++
173 lines
7.5 KiB
C++
#include "close_holders.h"
|
|
#include "holder_line.h"
|
|
#include "restart_manager_holders.h"
|
|
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <windows.h>
|
|
|
|
namespace
|
|
{
|
|
/// The installer sets this, the same variable the running-instance guard uses, instead
|
|
/// of passing the directory on the command line: a path with spaces, quotes or a
|
|
/// trailing backslash then never has to survive NSIS quoting or a command line.
|
|
constexpr wchar_t INSTALL_DIR_VARIABLE[] = L"COCKATRICE_GUARD_DIR";
|
|
|
|
/// How long the close mode waits for the instance it asked to exit before it reports the
|
|
/// process as still running. The installer measures the purge right after this call, so
|
|
/// the wait belongs here: it is the only place that holds a handle on the process whose
|
|
/// exit it is waiting for.
|
|
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
|
|
/// 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
|
|
/// to the user, which is what the report line next to the code spells out. 3 is only for
|
|
/// a lookup that failed and named nobody: one that failed but still named a process
|
|
/// running from the directory is answered with that process.
|
|
enum ExitCode
|
|
{
|
|
EXIT_NO_HOLDERS = 0,
|
|
EXIT_HOLDERS_FOUND = 1,
|
|
EXIT_NOT_CONFIGURED = 2,
|
|
EXIT_PROBE_FAILED = 3,
|
|
};
|
|
|
|
/// The probe has three modes, selected by the first argument:
|
|
///
|
|
/// (none) name what holds the payload, one line, for the guard prompt and the
|
|
/// abort dialog/log
|
|
/// --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
|
|
/// --end-holders end the running Cockatrice, Oracle and Servatrice and wait for them to
|
|
/// be gone, for the last resort the guard prompt offers when a close did
|
|
/// not free the files
|
|
///
|
|
/// 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
|
|
/// 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
|
|
/// 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.
|
|
enum class Mode
|
|
{
|
|
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
|
|
/// come back to the installer as an ANSI string, so a non-ASCII process name would be
|
|
/// mojibake in the log; a '?' at least stays honest. Control characters are dropped
|
|
/// too, so a name can never forge a second log line.
|
|
void writeLine(const std::wstring &text)
|
|
{
|
|
std::string narrow;
|
|
narrow.reserve(text.size() + 2);
|
|
for (const wchar_t character : text) {
|
|
narrow.push_back(character >= 0x20 && character < 0x7F ? static_cast<char>(character) : '?');
|
|
}
|
|
narrow += "\r\n";
|
|
DWORD written = 0;
|
|
WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), narrow.data(), static_cast<DWORD>(narrow.size()), &written, nullptr);
|
|
}
|
|
|
|
std::wstring installDirFromEnvironment()
|
|
{
|
|
for (DWORD size = MAX_PATH; size <= 32768U; size *= 2) {
|
|
std::vector<wchar_t> buffer(size);
|
|
const DWORD length = GetEnvironmentVariableW(INSTALL_DIR_VARIABLE, buffer.data(), size);
|
|
if (length == 0) {
|
|
return {};
|
|
}
|
|
if (length < size) {
|
|
return std::wstring(buffer.data(), length);
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
} // namespace
|
|
|
|
int main(int argumentCount, char **arguments)
|
|
{
|
|
const std::wstring installDir = installDirFromEnvironment();
|
|
if (installDir.empty()) {
|
|
writeLine(L"the probe was not given the install directory");
|
|
return EXIT_NOT_CONFIGURED;
|
|
}
|
|
|
|
// Processes running from the directory first: they are the instance the user is asked
|
|
// to close, and the Restart Manager may not name any of them (see
|
|
// 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.
|
|
// 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);
|
|
const std::vector<unsigned long> asking = askingProcessIds();
|
|
std::vector<Holder> fileHolders;
|
|
const unsigned long error = findProcessesUsingFiles(collectPayloadFiles(installDir), 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) {
|
|
return named.processId == holder.processId;
|
|
});
|
|
if (!alreadyNamed) {
|
|
holders.push_back(holder);
|
|
}
|
|
}
|
|
|
|
// A failed lookup only makes the probe unable to answer when it also named nobody: with
|
|
// a process running from the directory the installer still knows what to close, and
|
|
// that is worth more to the guard than the code of the query that missed it.
|
|
if (error != ERROR_SUCCESS && holders.empty()) {
|
|
writeLine(L"the Restart Manager lookup failed with win32 error " + std::to_wstring(error));
|
|
return EXIT_PROBE_FAILED;
|
|
}
|
|
|
|
const Mode mode = requestedMode(argumentCount, arguments);
|
|
if (holders.empty()) {
|
|
// No output in the naming mode: the installer words the "nothing is holding them"
|
|
// case, and it owns the message either way. The acting modes say it themselves,
|
|
// because their line is logged verbatim next to what they were asked to do.
|
|
if (mode != Mode::Name) {
|
|
writeLine(L"nothing is holding the files");
|
|
}
|
|
return EXIT_NO_HOLDERS;
|
|
}
|
|
if (mode == Mode::Name) {
|
|
writeLine(formatHolders(holders));
|
|
return EXIT_HOLDERS_FOUND;
|
|
}
|
|
if (mode == Mode::End) {
|
|
const EndHoldersResult result = endHolders(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);
|
|
writeLine(result.report);
|
|
return result.askedToClose > 0 ? EXIT_HOLDERS_FOUND : EXIT_NO_HOLDERS;
|
|
}
|