mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
[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
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:
parent
ded83aaab5
commit
0369aaffa9
11 changed files with 279 additions and 89 deletions
|
|
@ -567,8 +567,9 @@ Start-Sleep -Seconds 120
|
||||||
# instance holds its payload as mapped images, not as handles, and that is the state
|
# 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
|
# 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
|
# 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.
|
# Windows program, so it starts without its usual directory next to it, and it carries
|
||||||
$copy = Join-Path $dir "fake-cockatrice.exe"
|
# 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
|
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
|
$running = Start-Process -FilePath $copy -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru
|
||||||
try {
|
try {
|
||||||
|
|
@ -580,10 +581,49 @@ Start-Sleep -Seconds 120
|
||||||
try { $running.WaitForExit(5000) | Out-Null } catch { }
|
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.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)"
|
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 {
|
} finally {
|
||||||
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
|
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -998,7 +998,8 @@ FunctionEnd
|
||||||
; repository and embedded in the installer and the uninstaller alike. It names
|
; 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
|
; every process and service that has one of the payload files open or mapped
|
||||||
; (the Restart Manager, RmRegisterResources/RmGetList) and, separately, every
|
; (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
|
; 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
|
; 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
|
; 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.
|
; $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
|
; 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
|
; directory was given, 3 = the Restart Manager call failed and no payload program
|
||||||
; from the directory either, "timeout" = it did not return in time).
|
; runs from the directory either, "timeout" = it did not return in time).
|
||||||
Function ${prefix}RunHolderProbe
|
Function ${prefix}RunHolderProbe
|
||||||
Call ${prefix}ExtractHolderProbe
|
Call ${prefix}ExtractHolderProbe
|
||||||
; The directory travels in an environment variable, never on the command
|
; 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.
|
; 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
|
; 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
|
; 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
|
; 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.
|
; window to ask, so both are left alone here and only ended by the explicit answer.
|
||||||
|
|
@ -1248,14 +1249,15 @@ Function GuardAppsNotRunning
|
||||||
Call AbortInstall
|
Call AbortInstall
|
||||||
${EndIf}
|
${EndIf}
|
||||||
; Not an answer at all: the probe was not configured, the Restart Manager
|
; 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
|
; call failed and no payload program runs from the directory, or it timed
|
||||||
; on from that is a decision, so it is asked for rather than assumed - the
|
; out. Carrying on from that is a decision, so it is asked for rather than
|
||||||
; stale-runtime check after the purge still stands between a failed guard
|
; assumed - the stale-runtime check after the purge still stands between a
|
||||||
; and a mismatched install, but it only fires after files have been
|
; failed guard and a mismatched install, but it only fires after files have
|
||||||
; replaced, and on an upgrade the launcher asked for nobody is watching the
|
; been replaced, and on an upgrade the launcher asked for nobody is watching
|
||||||
; installer's exit code. Retry asks the probe again (a Restart Manager that
|
; the installer's exit code. Retry asks the probe again (a Restart Manager
|
||||||
; was busy answers the second time), Ignore installs anyway, Abort cancels
|
; that was busy answers the second time), Ignore installs anyway, Abort
|
||||||
; instead of writing a payload next to files the guard could not rule out.
|
; 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"
|
!insertmacro CockLog "guard: the holder probe could not answer ($GuardExit): $GuardOutput"
|
||||||
DetailPrint "Could not check whether Cockatrice is still running: $GuardOutput"
|
DetailPrint "Could not check whether Cockatrice is still running: $GuardOutput"
|
||||||
MessageBox MB_ABORTRETRYIGNORE|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
|
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
|
; 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
|
; 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
|
; 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
|
; well as user programs, and it adds every payload program running from the directory
|
||||||
; running instance holds its payload as mapped images rather than as open handles and the
|
; itself - not one in a folder below it, which is another application the user installed
|
||||||
; Restart Manager does not reliably report those. Its remaining blind spot is a handle
|
; there - because a running instance holds its payload as mapped images rather than as open
|
||||||
; taken in kernel mode, where there is no process to name; then the per-file codes logged
|
; handles and the Restart Manager does not reliably report those. Its remaining blind spot
|
||||||
; above are the whole story, and the probe says so by naming nothing rather than by
|
; is a handle taken in kernel mode, where there is no process to name; then the per-file
|
||||||
; failing.
|
; 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
|
; $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.
|
; 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"
|
${If} $HolderExit == "1"
|
||||||
StrCpy $HolderFound 1
|
StrCpy $HolderFound 1
|
||||||
${ElseIf} $HolderExit == "0"
|
${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}
|
${Else}
|
||||||
StrCpy $HolderOutput "the holder probe could not run: $HolderOutput"
|
StrCpy $HolderOutput "the holder probe could not run: $HolderOutput"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
|
||||||
|
|
@ -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 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 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)
|
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++
|
# The installer's holder probe is Windows-only, but the wording it produces and the
|
||||||
# and is what a stuck user has to act on, so it is tested everywhere.
|
# 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(
|
cockatrice_add_test(
|
||||||
NAME
|
NAME
|
||||||
holder_line_test
|
holder_line_test
|
||||||
|
|
@ -189,6 +190,15 @@ cockatrice_add_test(
|
||||||
INCLUDE_DIRS
|
INCLUDE_DIRS
|
||||||
${CMAKE_SOURCE_DIR}/tools/installer-holder-probe
|
${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(
|
cockatrice_add_test(
|
||||||
NAME
|
NAME
|
||||||
playmat_resolver_test
|
playmat_resolver_test
|
||||||
|
|
|
||||||
57
tests/payload_paths_test.cpp
Normal file
57
tests/payload_paths_test.cpp
Normal 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""));
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,9 @@
|
||||||
# installer and the uninstaller and is extracted to $PLUGINSDIR there, never installed, so
|
# 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
|
# 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.
|
# 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)
|
target_link_libraries(installer-holder-probe PRIVATE rstrtmgr)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "close_holders.h"
|
#include "close_holders.h"
|
||||||
|
|
||||||
|
#include "payload_paths.h"
|
||||||
#include "restart_manager_holders.h"
|
#include "restart_manager_holders.h"
|
||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ constexpr unsigned long END_WAIT_MILLISECONDS = 5000;
|
||||||
/// answer of "nothing holds the files" stays distinguishable from a probe that failed.
|
/// 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
|
/// 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
|
/// 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:
|
/// 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
|
/// 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.
|
/// 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
|
/// 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
|
/// 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
|
/// Restart Manager names whoever has a payload file open, and every payload program running
|
||||||
/// directory is named as well, because a running instance holds its own image and the modules
|
/// from the directory itself is named as well, because a running instance holds its own image
|
||||||
/// it loaded as mappings rather than open files and the Restart Manager does not always
|
/// and the modules it loaded as mappings rather than open files and the Restart Manager does
|
||||||
/// report those. Neither lookup alone is the guard's authority: an instance the file
|
/// not always report those. Neither lookup alone is the guard's authority: an instance the
|
||||||
/// handles do not name is exactly the case that left a half-replaced install behind.
|
/// 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
|
enum class Mode
|
||||||
{
|
{
|
||||||
Name, ///< name the holders
|
Name, ///< name the holders
|
||||||
|
|
@ -201,8 +203,8 @@ int main(int argumentCount, char **arguments)
|
||||||
return closeLauncher(installDir);
|
return closeLauncher(installDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processes running from the directory first: they are the instance the user is asked
|
// The payload programs running from the directory first: they are the instance the user
|
||||||
// to close, and the Restart Manager may not name any of them (see
|
// 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
|
// 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.
|
// 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
|
// 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 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
|
// a payload program running from the directory the installer still knows what to close,
|
||||||
// that is worth more to the guard than the code of the query that missed it.
|
// and that is worth more to the guard than the code of the query that missed it.
|
||||||
if (error != ERROR_SUCCESS && holders.empty()) {
|
if (error != ERROR_SUCCESS && holders.empty()) {
|
||||||
writeLine(L"the Restart Manager lookup failed with win32 error " + std::to_wstring(error));
|
writeLine(L"the Restart Manager lookup failed with win32 error " + std::to_wstring(error));
|
||||||
return EXIT_PROBE_FAILED;
|
return EXIT_PROBE_FAILED;
|
||||||
|
|
|
||||||
52
tools/installer-holder-probe/payload_paths.cpp
Normal file
52
tools/installer-holder-probe/payload_paths.cpp
Normal 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;
|
||||||
|
}
|
||||||
39
tools/installer-holder-probe/payload_paths.h
Normal file
39
tools/installer-holder-probe/payload_paths.h
Normal 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
|
||||||
|
|
@ -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.
|
/// 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"};
|
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)
|
void collectFilesOf(const std::wstring &directory, int depth, int maxDepth, std::vector<std::wstring> &files)
|
||||||
{
|
{
|
||||||
if (files.size() >= MAX_PROBED_FILES) {
|
if (files.size() >= MAX_PROBED_FILES) {
|
||||||
|
|
@ -84,15 +80,9 @@ std::wstring imagePathOf(unsigned long processId)
|
||||||
return imagePath;
|
return imagePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::wstring fileNameOf(const std::wstring &path)
|
/// @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
|
||||||
const std::size_t separator = path.find_last_of(L"\\/");
|
/// ("Cockatrice-old" next to "Cockatrice") cannot pass for it.
|
||||||
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.
|
|
||||||
std::wstring directoryPrefixOf(const std::wstring &directory)
|
std::wstring directoryPrefixOf(const std::wstring &directory)
|
||||||
{
|
{
|
||||||
std::wstring prefix = directory;
|
std::wstring prefix = directory;
|
||||||
|
|
@ -103,15 +93,17 @@ std::wstring directoryPrefixOf(const std::wstring &directory)
|
||||||
return prefix;
|
return prefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether @p path lies inside @p prefix, which is a directory with a trailing separator.
|
/// Whether @p left and @p right name the same directory, compared the way the file system
|
||||||
/// Ordinal and case-insensitive, as Windows compares paths.
|
/// does: a difference in case is not a difference in directory, and a trailing separator
|
||||||
bool isUnderDirectory(const std::wstring &path, const std::wstring &prefix)
|
/// changes nothing.
|
||||||
|
bool isSameDirectory(const std::wstring &left, const std::wstring &right)
|
||||||
{
|
{
|
||||||
if (path.size() < prefix.size()) {
|
if (left.empty() || right.empty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const int length = static_cast<int>(prefix.size());
|
const std::wstring leftPrefix = directoryPrefixOf(left);
|
||||||
return CompareStringOrdinal(path.c_str(), length, prefix.c_str(), length, TRUE) == CSTR_EQUAL;
|
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.
|
/// Copies a fixed-size, NUL-terminated WCHAR array the API filled in.
|
||||||
|
|
@ -134,16 +126,6 @@ Holder holderOf(const RM_PROCESS_INFO &info)
|
||||||
}
|
}
|
||||||
} // namespace
|
} // 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> collectPayloadFiles(const std::wstring &installDir)
|
||||||
{
|
{
|
||||||
std::vector<std::wstring> files;
|
std::vector<std::wstring> files;
|
||||||
|
|
@ -279,7 +261,6 @@ std::vector<Holder> findProcessesRunningFrom(const std::wstring &installDir)
|
||||||
return holders;
|
return holders;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::wstring prefix = directoryPrefixOf(installDir);
|
|
||||||
// Never the probe, and never the installer or uninstaller that started it: either can
|
// 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
|
// run from the directory being replaced, and naming one asks the user to close the
|
||||||
// program that is asking (see askingProcessIds).
|
// 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
|
// 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.
|
// Manager when it holds a handle. The idle process has no image at all.
|
||||||
const std::wstring imagePath = imagePathOf(entry.th32ProcessID);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
Holder holder;
|
Holder holder;
|
||||||
holder.processId = entry.th32ProcessID;
|
holder.processId = entry.th32ProcessID;
|
||||||
holder.imageName = fileNameOf(imagePath);
|
holder.imageName = imageName;
|
||||||
holders.push_back(std::move(holder));
|
holders.push_back(std::move(holder));
|
||||||
} while (Process32NextW(snapshot, &entry) != FALSE);
|
} while (Process32NextW(snapshot, &entry) != FALSE);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#define RESTART_MANAGER_HOLDERS_H
|
#define RESTART_MANAGER_HOLDERS_H
|
||||||
|
|
||||||
#include "holder_line.h"
|
#include "holder_line.h"
|
||||||
|
#include "payload_paths.h"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
@ -21,18 +22,6 @@
|
||||||
*/
|
*/
|
||||||
std::vector<std::wstring> collectPayloadFiles(const std::wstring &installDir);
|
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.
|
* @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);
|
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
|
* @brief Lists every payload program whose image lies in @p installDir itself.
|
||||||
* payload file open.
|
|
||||||
*
|
*
|
||||||
* Restart Manager answers the question "who has this file open", and a running instance
|
* 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
|
* 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
|
* 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.
|
* 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
|
* Reading the image path needs PROCESS_QUERY_LIMITED_INFORMATION and
|
||||||
* QueryFullProcessImageNameW, neither of which depends on the bitness of either process,
|
* 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
|
* so a 64-bit instance is named from this 32-bit stub. The comparison is on the directory,
|
||||||
* directory, not on an image name, so a process that merely shares a name (an Oracle
|
* not on an image name alone, so a program that merely shares a name with a payload program
|
||||||
* database server next to a Cockatrice install) is never named.
|
* is never named.
|
||||||
*
|
*
|
||||||
* @param installDir the directory being replaced, as a full path.
|
* @param installDir the directory being replaced, as a full path.
|
||||||
* @return one entry per process running from it, in snapshot order, without the processes
|
* @return one entry per payload program running from it, in snapshot order, without the
|
||||||
* named by askingProcessIds(); empty when the directory is empty or no process
|
* processes named by askingProcessIds(); empty when the directory is empty or no
|
||||||
* runs from it.
|
* payload program runs from it.
|
||||||
*/
|
*/
|
||||||
std::vector<Holder> findProcessesRunningFrom(const std::wstring &installDir);
|
std::vector<Holder> findProcessesRunningFrom(const std::wstring &installDir);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue