[Installer] Name only the payload programs installed in the directory (#7684)
Some checks failed
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Build Desktop / Configure (push) Has been cancelled
Build Desktop / Prune caches (push) Has been cancelled
Build Docker / Servatrice (arm) (push) Has been cancelled
Build Docker / Servatrice (x86) (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 44 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Servatrice_Debian 12 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / Ubuntu 26.04 cover (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 26 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
Build Docker / Publish multi-platform Servatrice image (push) Has been cancelled

The running-instance lookup named every process whose image path began with the
install directory, subfolders included. Installing into a folder that holds
another application in a subfolder therefore reported that application as
holding the previous version, and nothing the guard offers could clear it: the
close asks cockatrice.exe only, and the end answer refuses anything that is not
a payload program. Both answers were no-ops, so the update could only abort and
tell the user to close a program that was never touching the files.

Name only the programs the payload installs, and only when the image lies in the
install directory itself. A program below the directory that really does hold a
payload file is still named by the Restart Manager lookup, which is the lookup
for that question.

The Windows probe check now covers a payload-named program in a subfolder and a
program the payload does not install; both have to come back as nothing.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
sentry[bot] 2026-10-30 13:16:50 +01:00 • committed by GitHub
parent ded83aaab5
commit 0369aaffa9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 279 additions and 89 deletions

View file

@ -567,8 +567,9 @@ Start-Sleep -Seconds 120
# instance holds its payload as mapped images, not as handles, and that is the state
# the guard used to miss - it passed, the purge failed, and the install ended in a
# warning with no offer to close and try again. The image is a copy of a native
# Windows program, so it starts without its usual directory next to it.
$copy = Join-Path $dir "fake-cockatrice.exe"
# Windows program, so it starts without its usual directory next to it, and it carries
# one of the payload's own names because that is what the lookup names.
$copy = Join-Path $dir "cockatrice.exe"
Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $copy
$running = Start-Process -FilePath $copy -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru
try {
@ -580,10 +581,49 @@ Start-Sleep -Seconds 120
try { $running.WaitForExit(5000) | Out-Null } catch { }
}
if ($byImage.Code -ne 1) { throw "expected exit 1 for a process running from the directory, got $($byImage.Code) : $($byImage.Output)" }
if ($byImage.Output -notmatch "fake-cockatrice\.exe \(pid\s+$($running.Id)") {
if ($byImage.Output -notmatch "cockatrice\.exe \(pid\s+$($running.Id)") {
throw "the lookup did not name the process running from the directory: $($byImage.Output)"
}
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"
# A program in a folder below the directory is not an instance of the installation, even
# when it carries a payload program's name: the install directory can be a folder the
# user keeps other applications in, each in its own subfolder, and naming one of those
# asked for a close the probe never sends and an end it refuses to perform, so the guard
# could only abort the update. A program that really does hold a payload file is still
# named, by the Restart Manager lookup exercised above.
$nested = Join-Path $dir "other"
New-Item -ItemType Directory -Force $nested | Out-Null
$nestedImage = Join-Path $nested "cockatrice.exe"
Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $nestedImage
$nestedRunning = Start-Process -FilePath $nestedImage -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru
try {
Start-Sleep -Milliseconds 1500
$inSubfolder = Invoke-Probe $dir
} finally {
try { $nestedRunning.Kill() } catch { }
try { $nestedRunning.WaitForExit(5000) | Out-Null } catch { }
}
if ($inSubfolder.Code -ne 0) {
throw "expected exit 0 for a program in a folder below the directory, got $($inSubfolder.Code) : $($inSubfolder.Output)"
}
# A program the user keeps in the directory itself is not a payload program either: the
# guard ends only what the payload installs, and naming anything else asks for an answer
# that cannot free a single file.
$foreign = Join-Path $dir "tool.exe"
Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $foreign
$foreignRunning = Start-Process -FilePath $foreign -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru
try {
Start-Sleep -Milliseconds 1500
$ignored = Invoke-Probe $dir
} finally {
try { $foreignRunning.Kill() } catch { }
try { $foreignRunning.WaitForExit(5000) | Out-Null } catch { }
}
if ($ignored.Code -ne 0) {
throw "expected exit 0 for a program the payload does not install, got $($ignored.Code) : $($ignored.Output)"
}
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, named pid $($running.Id) for a payload program running from the directory, and named nothing for a payload program in a folder below it or for a program the payload does not install"
} finally {
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
}

View file

@ -998,7 +998,8 @@ FunctionEnd
; repository and embedded in the installer and the uninstaller alike. It names
; every process and service that has one of the payload files open or mapped
; (the Restart Manager, RmRegisterResources/RmGetList) and, separately, every
; process whose image lives in the directory being replaced.
; payload program whose image lies in the directory being replaced rather than
; in a folder below it.
;
; The second lookup is not redundant. Restart Manager answers "who has this
; file open", and a running instance does not have to have one open: its own
@ -1084,10 +1085,10 @@ FunctionEnd
; usage: set $GuardDir to the directory to check, then call this. Sets
; $GuardExit to the probe's exit code and $GuardOutput to its one-line answer.
; 0 = nothing holds the payload and nothing runs from the directory, 1 = the
; 0 = nothing holds the payload and no payload program runs from the directory, 1 = the
; line names what does, anything else = the probe could not answer (2 = no
; directory was given, 3 = the Restart Manager call failed and no process runs
; from the directory either, "timeout" = it did not return in time).
; directory was given, 3 = the Restart Manager call failed and no payload program
; runs from the directory either, "timeout" = it did not return in time).
Function ${prefix}RunHolderProbe
Call ${prefix}ExtractHolderProbe
; The directory travels in an environment variable, never on the command
@ -1106,7 +1107,7 @@ FunctionEnd
; Cockatrice to close, and waits for it to exit.
;
; The set is what the probe names: the processes holding a file of the version being
; replaced, plus the processes running from the directory. tools\installer-holder-probe
; replaced, plus the payload programs running from the directory itself. The probe
; closes that set by pid, which is why nothing here has to match an executable path. Only
; cockatrice.exe is asked: Oracle writes the card database in place and Servatrice has no
; window to ask, so both are left alone here and only ended by the explicit answer.
@ -1248,14 +1249,15 @@ Function GuardAppsNotRunning
Call AbortInstall
${EndIf}
; Not an answer at all: the probe was not configured, the Restart Manager
; call failed and nothing runs from the directory, or it timed out. Carrying
; on from that is a decision, so it is asked for rather than assumed - the
; stale-runtime check after the purge still stands between a failed guard
; and a mismatched install, but it only fires after files have been
; replaced, and on an upgrade the launcher asked for nobody is watching the
; installer's exit code. Retry asks the probe again (a Restart Manager that
; was busy answers the second time), Ignore installs anyway, Abort cancels
; instead of writing a payload next to files the guard could not rule out.
; call failed and no payload program runs from the directory, or it timed
; out. Carrying on from that is a decision, so it is asked for rather than
; assumed - the stale-runtime check after the purge still stands between a
; failed guard and a mismatched install, but it only fires after files have
; been replaced, and on an upgrade the launcher asked for nobody is watching
; the installer's exit code. Retry asks the probe again (a Restart Manager
; that was busy answers the second time), Ignore installs anyway, Abort
; cancels instead of writing a payload next to files the guard could not
; rule out.
!insertmacro CockLog "guard: the holder probe could not answer ($GuardExit): $GuardOutput"
DetailPrint "Could not check whether Cockatrice is still running: $GuardOutput"
MessageBox MB_ABORTRETRYIGNORE|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
@ -1862,12 +1864,13 @@ FunctionEnd
; anything is replaced; here it answers the other question, about files a purge could not
; remove. It asks the Restart Manager (RmRegisterResources/RmGetList), which names a
; process holding a plain handle on a file - an antivirus scanner - and names services as
; well as user programs, and it adds every process running from the directory, because a
; running instance holds its payload as mapped images rather than as open handles and the
; Restart Manager does not reliably report those. Its remaining blind spot is a handle
; taken in kernel mode, where there is no process to name; then the per-file codes logged
; above are the whole story, and the probe says so by naming nothing rather than by
; failing.
; well as user programs, and it adds every payload program running from the directory
; itself - not one in a folder below it, which is another application the user installed
; there - because a running instance holds its payload as mapped images rather than as open
; handles and the Restart Manager does not reliably report those. Its remaining blind spot
; is a handle taken in kernel mode, where there is no process to name; then the per-file
; codes logged above are the whole story, and the probe says so by naming nothing rather
; than by failing.
;
; $INSTDIR travels through the same COCKATRICE_GUARD_DIR environment variable the guard
; uses, so a path with spaces or apostrophes never has to be embedded in a command line.
@ -1889,7 +1892,7 @@ Function LogProcessesUsingInstallDir
${If} $HolderExit == "1"
StrCpy $HolderFound 1
${ElseIf} $HolderExit == "0"
StrCpy $HolderOutput "nothing is holding them and no program is running from the directory"
StrCpy $HolderOutput "nothing is holding them and no payload program is running from the directory"
${Else}
StrCpy $HolderOutput "the holder probe could not run: $HolderOutput"
${EndIf}

View file

@ -178,8 +178,9 @@ cockatrice_add_test(NAME user_data_backup_test SOURCES user_data_backup_test.cpp
cockatrice_add_test(NAME password_hash_test SOURCES password_hash_test.cpp LIBS libcockatrice_utility)
cockatrice_add_test(NAME report_categories_test SOURCES report_categories_test.cpp LIBS libcockatrice_utility)
cockatrice_add_test(NAME warning_categories_test SOURCES warning_categories_test.cpp LIBS libcockatrice_utility)
# The installer's holder probe is Windows-only, but the wording it produces is plain C++
# and is what a stuck user has to act on, so it is tested everywhere.
# The installer's holder probe is Windows-only, but the wording it produces and the
# question which programs are the payload's own are plain C++ and are what a stuck user
# has to act on, so they are tested everywhere.
cockatrice_add_test(
NAME
holder_line_test
@ -189,6 +190,15 @@ cockatrice_add_test(
INCLUDE_DIRS
${CMAKE_SOURCE_DIR}/tools/installer-holder-probe
)
cockatrice_add_test(
NAME
payload_paths_test
SOURCES
${CMAKE_SOURCE_DIR}/tools/installer-holder-probe/payload_paths.cpp
payload_paths_test.cpp
INCLUDE_DIRS
${CMAKE_SOURCE_DIR}/tools/installer-holder-probe
)
cockatrice_add_test(
NAME
playmat_resolver_test

View file

@ -0,0 +1,57 @@
#include "payload_paths.h"
#include <gtest/gtest.h>
#include <string>
namespace
{
/// The directory part of a path spelled with the same case as the install directory, which
/// is the comparison the probe makes once both sides are folded - see isSameDirectory in
/// restart_manager_holders.cpp. Kept as a plain string here so the shape of a path can be
/// tested without Windows.
bool runsFrom(const std::wstring &imagePath, const std::wstring &installDir)
{
return parentDirectoryOf(imagePath) == installDir;
}
} // namespace
TEST(PayloadPaths, SplitsAnImagePathIntoItsDirectoryAndName)
{
EXPECT_EQ(fileNameOf(L"D:\\Games\\Cockatrice\\cockatrice.exe"), std::wstring(L"cockatrice.exe"));
EXPECT_EQ(parentDirectoryOf(L"D:\\Games\\Cockatrice\\cockatrice.exe"), std::wstring(L"D:\\Games\\Cockatrice"));
EXPECT_EQ(fileNameOf(L"cockatrice.exe"), std::wstring(L"cockatrice.exe"));
EXPECT_EQ(parentDirectoryOf(L"cockatrice.exe"), std::wstring());
EXPECT_EQ(parentDirectoryOf(L"D:/Games/Cockatrice/cockatrice.exe"), std::wstring(L"D:/Games/Cockatrice"));
}
TEST(PayloadPaths, OnlyAnImageInTheInstallDirectoryRunsFromIt)
{
const std::wstring installDir = L"D:\\Games\\Cockatrice";
EXPECT_TRUE(runsFrom(L"D:\\Games\\Cockatrice\\cockatrice.exe", installDir));
// A folder the user keeps other applications in is not the installation, and neither is
// a sibling whose name merely begins with the same letters.
EXPECT_FALSE(runsFrom(L"D:\\Games\\Cockatrice\\old\\cockatrice.exe", installDir));
EXPECT_FALSE(runsFrom(L"D:\\Games\\Cockatrice-old\\cockatrice.exe", installDir));
EXPECT_FALSE(runsFrom(L"D:\\Games\\cockatrice.exe", installDir));
}
TEST(PayloadPaths, KnowsTheProgramsThePayloadInstalls)
{
EXPECT_TRUE(isPayloadProgram(L"cockatrice.exe"));
EXPECT_TRUE(isPayloadProgram(L"oracle.exe"));
EXPECT_TRUE(isPayloadProgram(L"servatrice.exe"));
EXPECT_TRUE(isPayloadProgram(L"COCKATRICE.EXE"));
EXPECT_TRUE(isPayloadProgram(L"Servatrice.Exe"));
}
TEST(PayloadPaths, LeavesEveryOtherProgramAlone)
{
// A program the user keeps in the install directory, the uninstaller, a service host and
// a name that only looks like a payload program are all not instances of the payload.
EXPECT_FALSE(isPayloadProgram(L"uninstall.exe"));
EXPECT_FALSE(isPayloadProgram(L"tool.exe"));
EXPECT_FALSE(isPayloadProgram(L"svchost.exe"));
EXPECT_FALSE(isPayloadProgram(L"cockatrice2.exe"));
EXPECT_FALSE(isPayloadProgram(L"mycockatrice.exe"));
EXPECT_FALSE(isPayloadProgram(L""));
}

View file

@ -5,7 +5,9 @@
# installer and the uninstaller and is extracted to $PLUGINSDIR there, never installed, so
# it links nothing from this project: no Qt, and a static C runtime because the installed
# runtime is exactly what the purge may have failed to replace.
add_executable(installer-holder-probe close_holders.cpp holder_line.cpp main.cpp restart_manager_holders.cpp)
add_executable(
installer-holder-probe close_holders.cpp holder_line.cpp main.cpp payload_paths.cpp restart_manager_holders.cpp
)
target_link_libraries(installer-holder-probe PRIVATE rstrtmgr)

View file

@ -1,5 +1,6 @@
#include "close_holders.h"
#include "payload_paths.h"
#include "restart_manager_holders.h"
// clang-format off

View file

@ -34,7 +34,7 @@ constexpr unsigned long END_WAIT_MILLISECONDS = 5000;
/// 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
/// a lookup that failed and named nobody: one that failed but still named a payload program
/// running from the directory is answered with that process. 4 is only the launcher mode:
/// the process the installer waits on has no window a user could answer, which is the one
/// answer the installer acts on by itself instead of asking the user.
@ -61,11 +61,13 @@ enum ExitCode
/// no window has answered its own close and is stuck in shutdown
///
/// The guard's three modes 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.
/// Restart Manager names whoever has a payload file open, and every payload program running
/// from the directory itself 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.
/// A program in a folder below the directory is not named by the second lookup: it is not an
/// instance of this payload, and the guard could neither close nor end it.
enum class Mode
{
Name, ///< name the holders
@ -201,8 +203,8 @@ int main(int argumentCount, char **arguments)
return closeLauncher(installDir);
}
// 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
// The payload programs 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
@ -225,8 +227,8 @@ int main(int argumentCount, char **arguments)
}
// 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.
// a payload program 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;

View file

@ -0,0 +1,52 @@
#include "payload_paths.h"
#include <algorithm>
#include <cstddef>
#include <iterator>
namespace
{
/// 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"};
/// Compares two file names the way the file system does, without a locale.
///
/// Only the names above are ever compared against, and all of them are ASCII, so folding
/// the ASCII letters is the whole answer. The Windows ordinal comparison is not used here
/// on purpose: this file is compiled into a test that runs everywhere, and the question
/// "is this image a program the payload installs?" must be answerable without Windows.
bool equalsIgnoringAsciiCase(const std::wstring &left, const std::wstring &right)
{
if (left.size() != right.size()) {
return false;
}
return std::equal(left.begin(), left.end(), right.begin(), [](wchar_t a, wchar_t b) {
const wchar_t foldedA = a >= L'A' && a <= L'Z' ? static_cast<wchar_t>(a - L'A' + L'a') : a;
const wchar_t foldedB = b >= L'A' && b <= L'Z' ? static_cast<wchar_t>(b - L'A' + L'a') : b;
return foldedA == foldedB;
});
}
} // namespace
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);
}
std::wstring parentDirectoryOf(const std::wstring &path)
{
const std::size_t separator = path.find_last_of(L"\\/");
return separator == std::wstring::npos ? std::wstring() : path.substr(0, separator);
}
bool isPayloadProgram(const std::wstring &imageName)
{
for (const wchar_t *program : PAYLOAD_PROGRAMS) {
if (equalsIgnoringAsciiCase(imageName, program)) {
return true;
}
}
return false;
}

View file

@ -0,0 +1,39 @@
#ifndef PAYLOAD_PATHS_H
#define PAYLOAD_PATHS_H
#include <string>
/**
* @brief The last name in @p path: what follows its last separator, or @p path itself when
* it has none.
*
* Empty for an empty path and for one that ends in a separator, which has no name of its
* own - a caller that hands in a directory there gets no name rather than the directory's
* own last component.
*/
std::wstring fileNameOf(const std::wstring &path);
/**
* @brief The directory @p path lies in, without a trailing separator.
*
* Empty when @p path has no directory part. Used to tell where a process was started from:
* an image running directly in the install directory has that directory as its parent,
* while one started from a folder below it has that folder, and the two are not the same
* answer to "does this process belong to the installation being replaced?".
*/
std::wstring parentDirectoryOf(const std::wstring &path);
/**
* @brief Whether @p imageName is one of the programs the payload installs.
*
* The single place that answers which images are Cockatrice's own, so the guard acts on the
* same set it names, and so a program a user happens to keep in the install directory is
* never treated as an instance of the application. Matched on the image name alone, without
* case: the names are the payload's own, and the comparison is deliberately free of Windows
* API so it can be exercised off Windows.
*
* @param imageName the file name of a process image, empty when it could not be read
*/
bool isPayloadProgram(const std::wstring &imageName);
#endif

View file

@ -30,10 +30,6 @@ constexpr int MAX_ASKER_LEVELS = 8;
/// 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"};
/// 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"};
void collectFilesOf(const std::wstring &directory, int depth, int maxDepth, std::vector<std::wstring> &files)
{
if (files.size() >= MAX_PROBED_FILES) {
@ -84,15 +80,9 @@ std::wstring imagePathOf(unsigned long processId)
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);
}
/// @p directory with exactly one trailing separator, so that a sibling whose name merely
/// begins with the same letters ("Cockatrice-old" next to "Cockatrice") cannot pass for a
/// subdirectory of it.
/// @p directory with exactly one trailing separator, so that two spellings of the same
/// directory compare equal and a sibling whose name merely begins with the same letters
/// ("Cockatrice-old" next to "Cockatrice") cannot pass for it.
std::wstring directoryPrefixOf(const std::wstring &directory)
{
std::wstring prefix = directory;
@ -103,15 +93,17 @@ std::wstring directoryPrefixOf(const std::wstring &directory)
return prefix;
}
/// Whether @p path lies inside @p prefix, which is a directory with a trailing separator.
/// Ordinal and case-insensitive, as Windows compares paths.
bool isUnderDirectory(const std::wstring &path, const std::wstring &prefix)
/// Whether @p left and @p right name the same directory, compared the way the file system
/// does: a difference in case is not a difference in directory, and a trailing separator
/// changes nothing.
bool isSameDirectory(const std::wstring &left, const std::wstring &right)
{
if (path.size() < prefix.size()) {
if (left.empty() || right.empty()) {
return false;
}
const int length = static_cast<int>(prefix.size());
return CompareStringOrdinal(path.c_str(), length, prefix.c_str(), length, TRUE) == CSTR_EQUAL;
const std::wstring leftPrefix = directoryPrefixOf(left);
const std::wstring rightPrefix = directoryPrefixOf(right);
return CompareStringOrdinal(leftPrefix.c_str(), -1, rightPrefix.c_str(), -1, TRUE) == CSTR_EQUAL;
}
/// Copies a fixed-size, NUL-terminated WCHAR array the API filled in.
@ -134,16 +126,6 @@ Holder holderOf(const RM_PROCESS_INFO &info)
}
} // namespace
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;
}
std::vector<std::wstring> collectPayloadFiles(const std::wstring &installDir)
{
std::vector<std::wstring> files;
@ -279,7 +261,6 @@ std::vector<Holder> findProcessesRunningFrom(const std::wstring &installDir)
return holders;
}
const std::wstring prefix = directoryPrefixOf(installDir);
// Never the probe, and never the installer or uninstaller that started it: either can
// run from the directory being replaced, and naming one asks the user to close the
// program that is asking (see askingProcessIds).
@ -300,12 +281,20 @@ std::vector<Holder> findProcessesRunningFrom(const std::wstring &installDir)
// refuses to be read is not named here and is still named by Restart
// Manager when it holds a handle. The idle process has no image at all.
const std::wstring imagePath = imagePathOf(entry.th32ProcessID);
if (!isUnderDirectory(imagePath, prefix)) {
const std::wstring imageName = fileNameOf(imagePath);
// Only the programs the payload installs, and only those started from the
// directory itself: a folder a user installs into can hold other applications,
// each in its own subfolder, and one of those is not an instance of this
// payload. Naming it would ask for a close the probe cannot send and an end it
// would refuse to perform, so the guard could only ever abort. A program below
// the directory that really does hold a payload file is named by
// findProcessesUsingFiles, which is the lookup for that question.
if (!isPayloadProgram(imageName) || !isSameDirectory(parentDirectoryOf(imagePath), installDir)) {
continue;
}
Holder holder;
holder.processId = entry.th32ProcessID;
holder.imageName = fileNameOf(imagePath);
holder.imageName = imageName;
holders.push_back(std::move(holder));
} while (Process32NextW(snapshot, &entry) != FALSE);
}

View file

@ -2,6 +2,7 @@
#define RESTART_MANAGER_HOLDERS_H
#include "holder_line.h"
#include "payload_paths.h"
#include <string>
#include <vector>
@ -21,18 +22,6 @@
*/
std::vector<std::wstring> collectPayloadFiles(const std::wstring &installDir);
/**
* @brief Whether @p imageName is one of the programs the payload installs.
*
* The single place that answers which images are Cockatrice's own, so the guard ends the
* same set it names. Matched case-insensitively on the image name alone: only holders are
* ever tested against it, and a holder is a process that has a payload file open or runs
* from the install directory.
*
* @param imageName the file name of a process image, empty when it could not be read.
*/
bool isPayloadProgram(const std::wstring &imageName);
/**
* @brief Names the applications and services that have one of @p files open or mapped.
*
@ -47,8 +36,7 @@ bool isPayloadProgram(const std::wstring &imageName);
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.
* @brief Lists every payload program whose image lies in @p installDir itself.
*
* 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
@ -57,16 +45,23 @@ unsigned long findProcessesUsingFiles(const std::vector<std::wstring> &files, st
* 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.
*
* Only the programs the payload installs are named, and only when the image lies directly
* in @p installDir. A folder a user installs into can hold other applications, each in its
* own subfolder, and those are not instances of this one: they hold none of the payload,
* neither closing nor ending them would free a file the purge removes, and naming them
* leaves the guard with an answer that can never be carried out. A program below the
* directory that does hold a payload file is still named, by findProcessesUsingFiles.
*
* 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.
* so a 64-bit instance is named from this 32-bit stub. The comparison is on the directory,
* not on an image name alone, so a program that merely shares a name with a payload program
* 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.
* @return one entry per payload program running from it, in snapshot order, without the
* processes named by askingProcessIds(); empty when the directory is empty or no
* payload program runs from it.
*/
std::vector<Holder> findProcessesRunningFrom(const std::wstring &installDir);