server/tools/playnite_launcher/playnite_process.cpp
Rama main · réplica de lectura del repositorio original.
#include "tools/playnite_launcher/playnite_process.h"
#include "src/logging.h"
#include "src/platform/windows/ipc/misc_utils.h"
#include <algorithm>
#include <array>
#include <cstdint>
#include <filesystem>
#include <optional>
#include <Psapi.h>
#include <shellapi.h>
#include <ShlObj.h>
#include <Shlwapi.h>
#include <string>
#include <system_error>
#include <utility>
#include <wincrypt.h>
#include <winsock2.h>
#include <windows.h>
namespace playnite_launcher::playnite {
namespace {
constexpr wchar_t cleanup_watchdog_filename[] = L"playnite-cleanup-watchdog.exe";
constexpr wchar_t cleanup_watchdog_cache_directory[] = L"playnite-watchdogs";
constexpr wchar_t cleanup_watchdog_cache_mutex[] = L"Local\\ArtLight ServerPlayniteWatchdogCache-v1";
constexpr wchar_t cleanup_lease_mutex_prefix[] = L"Local\\ArtLight ServerPlayniteCleanupLease-v1-";
std::optional<std::wstring> cleanup_lease_mutex_name(const std::string &install_dir_utf8) {
if (install_dir_utf8.empty()) {
return std::nullopt;
}
try {
auto normalized = platf::dxgi::utf8_to_wide(install_dir_utf8);
if (normalized.empty()) {
return std::nullopt;
}
for (auto &ch : normalized) {
if (ch == L'/') {
ch = L'\\';
} else if (ch >= L'A' && ch <= L'Z') {
ch = static_cast<wchar_t>(ch - L'A' + L'a');
}
}
while (
normalized.size() > 1 &&
normalized.back() == L'\\' &&
!(normalized.size() == 3 && normalized[1] == L':')
) {
normalized.pop_back();
}
std::uint64_t hash = 1469598103934665603ull;
for (const auto ch : normalized) {
hash ^= static_cast<std::uint16_t>(ch);
hash *= 1099511628211ull;
}
return std::wstring(cleanup_lease_mutex_prefix) + std::to_wstring(hash);
} catch (...) {
return std::nullopt;
}
}
struct staged_watchdog_t {
std::filesystem::path path;
std::wstring sha256;
};
class watchdog_cache_lock_t {
public:
watchdog_cache_lock_t() {
handle_ = CreateMutexW(nullptr, FALSE, cleanup_watchdog_cache_mutex);
if (!handle_) {
return;
}
const DWORD wait_result = WaitForSingleObject(handle_, 10000);
acquired_ = wait_result == WAIT_OBJECT_0 || wait_result == WAIT_ABANDONED;
if (!acquired_) {
CloseHandle(handle_);
handle_ = nullptr;
}
}
~watchdog_cache_lock_t() {
if (acquired_) {
ReleaseMutex(handle_);
}
if (handle_) {
CloseHandle(handle_);
}
}
watchdog_cache_lock_t(const watchdog_cache_lock_t &) = delete;
watchdog_cache_lock_t &operator=(const watchdog_cache_lock_t &) = delete;
explicit operator bool() const {
return acquired_;
}
private:
HANDLE handle_ = nullptr;
bool acquired_ = false;
};
std::optional<std::wstring> sha256_file(const std::filesystem::path &path) {
HANDLE file = CreateFileW(
path.c_str(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_DELETE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
nullptr
);
if (file == INVALID_HANDLE_VALUE) {
return std::nullopt;
}
HCRYPTPROV provider = 0;
HCRYPTHASH hash = 0;
if (!CryptAcquireContextW(&provider, nullptr, nullptr, PROV_RSA_AES, CRYPT_VERIFYCONTEXT) ||
!CryptCreateHash(provider, CALG_SHA_256, 0, 0, &hash)) {
if (hash) {
CryptDestroyHash(hash);
}
if (provider) {
CryptReleaseContext(provider, 0);
}
CloseHandle(file);
return std::nullopt;
}
bool succeeded = true;
std::array<BYTE, 64 * 1024> buffer {};
while (true) {
DWORD bytes_read = 0;
if (!ReadFile(file, buffer.data(), static_cast<DWORD>(buffer.size()), &bytes_read, nullptr)) {
succeeded = false;
break;
}
if (bytes_read == 0) {
break;
}
if (!CryptHashData(hash, buffer.data(), bytes_read, 0)) {
succeeded = false;
break;
}
}
std::array<BYTE, 32> digest {};
DWORD digest_size = static_cast<DWORD>(digest.size());
if (succeeded && !CryptGetHashParam(hash, HP_HASHVAL, digest.data(), &digest_size, 0)) {
succeeded = false;
}
CryptDestroyHash(hash);
CryptReleaseContext(provider, 0);
CloseHandle(file);
if (!succeeded || digest_size != digest.size()) {
return std::nullopt;
}
static constexpr wchar_t hex_digits[] = L"0123456789abcdef";
std::wstring hex;
hex.reserve(digest.size() * 2);
for (BYTE byte : digest) {
hex.push_back(hex_digits[byte >> 4]);
hex.push_back(hex_digits[byte & 0x0F]);
}
return hex;
}
std::optional<std::filesystem::path> cleanup_watchdog_cache_root() {
PWSTR local_app_data = nullptr;
if (FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_CREATE, nullptr, &local_app_data)) ||
!local_app_data) {
return std::nullopt;
}
std::filesystem::path root = std::filesystem::path(local_app_data) / L"ArtLight Server" / cleanup_watchdog_cache_directory;
CoTaskMemFree(local_app_data);
return root;
}
bool is_sha256_directory_name(const std::wstring &name) {
if (name.size() != 64) {
return false;
}
return std::all_of(name.begin(), name.end(), [](wchar_t ch) {
return (ch >= L'0' && ch <= L'9') || (ch >= L'a' && ch <= L'f');
});
}
void cleanup_stale_watchdog_versions(const std::filesystem::path &cache_root, const std::wstring &active_sha256) {
std::error_code ec;
std::filesystem::directory_iterator entries(cache_root, ec);
if (ec) {
return;
}
for (const auto &entry : entries) {
const std::wstring name = entry.path().filename().wstring();
if (name == active_sha256 || !is_sha256_directory_name(name)) {
continue;
}
std::error_code remove_ec;
const auto removed = std::filesystem::remove_all(entry.path(), remove_ec);
if (!remove_ec && removed > 0) {
BOOST_LOG(debug) << "Removed stale Playnite cleanup watchdog cache version '" << platf::dxgi::wide_to_utf8(name) << "'";
}
}
}
std::optional<staged_watchdog_t> stage_cleanup_watchdog(const std::filesystem::path &source_path) {
auto cache_root = cleanup_watchdog_cache_root();
if (!cache_root) {
BOOST_LOG(warning) << "Cleanup watcher staging failed: unable to resolve LocalAppData";
return std::nullopt;
}
for (int attempt = 0; attempt < 2; ++attempt) {
auto source_sha256 = sha256_file(source_path);
if (!source_sha256) {
BOOST_LOG(warning) << "Cleanup watcher staging failed: unable to hash source executable";
return std::nullopt;
}
const std::filesystem::path version_directory = *cache_root / *source_sha256;
const std::filesystem::path staged_path = version_directory / cleanup_watchdog_filename;
std::error_code ec;
std::filesystem::create_directories(version_directory, ec);
if (ec) {
BOOST_LOG(warning) << "Cleanup watcher staging failed: unable to create cache directory: " << ec.message();
return std::nullopt;
}
if (std::filesystem::exists(staged_path, ec) && !ec) {
auto staged_sha256 = sha256_file(staged_path);
if (staged_sha256 && *staged_sha256 == *source_sha256) {
cleanup_stale_watchdog_versions(*cache_root, *source_sha256);
return staged_watchdog_t {staged_path, *source_sha256};
}
std::filesystem::remove(staged_path, ec);
if (ec) {
BOOST_LOG(warning) << "Cleanup watcher staging failed: cached executable is invalid and cannot be replaced: " << ec.message();
return std::nullopt;
}
}
std::filesystem::path temporary_path = staged_path;
temporary_path += L".staging-" + std::to_wstring(GetCurrentProcessId()) +
L"-" + std::to_wstring(GetCurrentThreadId()) +
L"-" + std::to_wstring(GetTickCount64());
std::filesystem::copy_file(
source_path,
temporary_path,
std::filesystem::copy_options::overwrite_existing,
ec
);
if (ec) {
BOOST_LOG(warning) << "Cleanup watcher staging failed: unable to copy executable: " << ec.message();
return std::nullopt;
}
auto temporary_sha256 = sha256_file(temporary_path);
if (!temporary_sha256 || *temporary_sha256 != *source_sha256) {
std::filesystem::remove(temporary_path, ec);
if (attempt == 0) {
continue;
}
BOOST_LOG(warning) << "Cleanup watcher staging failed: copied executable hash did not match source";
return std::nullopt;
}
if (!MoveFileExW(
temporary_path.c_str(),
staged_path.c_str(),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH
)) {
const DWORD move_error = GetLastError();
auto raced_sha256 = sha256_file(staged_path);
std::filesystem::remove(temporary_path, ec);
if (!raced_sha256 || *raced_sha256 != *source_sha256) {
BOOST_LOG(warning) << "Cleanup watcher staging failed: atomic publish failed, error=" << move_error;
return std::nullopt;
}
}
auto staged_sha256 = sha256_file(staged_path);
if (!staged_sha256 || *staged_sha256 != *source_sha256) {
BOOST_LOG(warning) << "Cleanup watcher staging failed: published executable hash did not match source";
return std::nullopt;
}
cleanup_stale_watchdog_versions(*cache_root, *source_sha256);
return staged_watchdog_t {staged_path, *source_sha256};
}
return std::nullopt;
}
// Quote an argument for CommandLineToArgvW/CreateProcessW. Backslashes before
// a closing quote must be doubled, including the trailing slash common in
// Playnite install directories.
std::wstring quote_command_line_argument(const std::wstring &argument) {
std::wstring quoted;
quoted.push_back(L'"');
for (auto it = argument.begin();;) {
std::size_t backslash_count = 0;
while (it != argument.end() && *it == L'\\') {
++it;
++backslash_count;
}
if (it == argument.end()) {
quoted.append(backslash_count * 2, L'\\');
break;
}
if (*it == L'"') {
quoted.append(backslash_count * 2 + 1, L'\\');
} else {
quoted.append(backslash_count, L'\\');
}
quoted.push_back(*it);
++it;
}
quoted.push_back(L'"');
return quoted;
}
} // namespace
cleanup_lease_t::cleanup_lease_t(cleanup_lease_t &&other) noexcept : handle_(std::exchange(other.handle_, nullptr)) {}
cleanup_lease_t &cleanup_lease_t::operator=(cleanup_lease_t &&other) noexcept {
if (this != &other) {
if (handle_) {
ReleaseMutex(handle_);
CloseHandle(handle_);
}
handle_ = std::exchange(other.handle_, nullptr);
}
return *this;
}
cleanup_lease_t::~cleanup_lease_t() {
if (handle_) {
ReleaseMutex(handle_);
CloseHandle(handle_);
}
}
std::optional<cleanup_lease_t> cleanup_lease_t::acquire(const std::string &install_dir_utf8) {
const auto mutex_name = cleanup_lease_mutex_name(install_dir_utf8);
if (!mutex_name) {
return std::nullopt;
}
HANDLE handle = CreateMutexW(nullptr, FALSE, mutex_name->c_str());
if (!handle) {
BOOST_LOG(warning) << "Cleanup lease: failed to create mutex for install directory";
return std::nullopt;
}
const DWORD wait_result = WaitForSingleObject(handle, 0);
if (wait_result == WAIT_OBJECT_0 || wait_result == WAIT_ABANDONED) {
return cleanup_lease_t {handle};
}
if (wait_result != WAIT_TIMEOUT) {
BOOST_LOG(warning) << "Cleanup lease: failed to acquire mutex, error=" << wait_result;
}
CloseHandle(handle);
return std::nullopt;
}
bool cleanup_lease_t::held_by_another_launcher(const std::string &install_dir_utf8) {
const auto mutex_name = cleanup_lease_mutex_name(install_dir_utf8);
if (!mutex_name) {
return false;
}
HANDLE handle = CreateMutexW(nullptr, FALSE, mutex_name->c_str());
if (!handle) {
BOOST_LOG(warning) << "Cleanup lease: failed to inspect mutex for install directory";
return false;
}
const DWORD wait_result = WaitForSingleObject(handle, 0);
if (wait_result == WAIT_TIMEOUT) {
CloseHandle(handle);
return true;
}
if (wait_result == WAIT_OBJECT_0 || wait_result == WAIT_ABANDONED) {
ReleaseMutex(handle);
} else {
BOOST_LOG(warning) << "Cleanup lease: failed to inspect mutex, error=" << wait_result;
}
CloseHandle(handle);
return false;
}
bool is_playnite_running() {
try {
auto d = platf::dxgi::find_process_ids_by_name(L"Playnite.DesktopApp.exe");
if (!d.empty()) {
return true;
}
auto f = platf::dxgi::find_process_ids_by_name(L"Playnite.FullscreenApp.exe");
if (!f.empty()) {
return true;
}
} catch (...) {
}
return false;
}
std::wstring get_explorer_path() {
WCHAR winDir[MAX_PATH] = {};
if (GetWindowsDirectoryW(winDir, ARRAYSIZE(winDir)) > 0) {
std::filesystem::path p(winDir);
p /= L"explorer.exe";
if (std::filesystem::exists(p)) {
return p.wstring();
}
}
WCHAR out[MAX_PATH] = {};
DWORD rc = SearchPathW(nullptr, L"explorer.exe", nullptr, ARRAYSIZE(out), out, nullptr);
if (rc > 0 && rc < ARRAYSIZE(out)) {
return std::wstring(out);
}
return L"explorer.exe";
}
DWORD explorer_pid_from_windows() {
DWORD pid = 0;
HWND shell = GetShellWindow();
if (shell) {
GetWindowThreadProcessId(shell, &pid);
if (pid) {
return pid;
}
}
HWND tray = FindWindowW(L"Shell_TrayWnd", nullptr);
if (tray) {
GetWindowThreadProcessId(tray, &pid);
}
return pid;
}
DWORD explorer_pid_from_process_list() {
DWORD current_session = 0;
ProcessIdToSessionId(GetCurrentProcessId(), ¤t_session);
try {
auto pids = platf::dxgi::find_process_ids_by_name(L"explorer.exe");
for (DWORD candidate : pids) {
DWORD session = 0;
ProcessIdToSessionId(candidate, &session);
if (session == current_session) {
return candidate;
}
}
if (!pids.empty()) {
return pids.front();
}
} catch (...) {
}
return 0;
}
HANDLE open_explorer_parent_handle() {
DWORD pid = explorer_pid_from_windows();
if (!pid) {
pid = explorer_pid_from_process_list();
}
if (!pid) {
return nullptr;
}
return OpenProcess(PROCESS_CREATE_PROCESS | PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_DUP_HANDLE, FALSE, pid);
}
bool assign_parent_attributes(HANDLE parent, STARTUPINFOEXW &si, LPPROC_THREAD_ATTRIBUTE_LIST &attrList) {
if (!parent) {
return true;
}
SIZE_T size = 0;
InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
attrList = static_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(HeapAlloc(GetProcessHeap(), 0, size));
if (!attrList) {
BOOST_LOG(warning) << "assign_parent_attributes: HeapAlloc failed";
SetLastError(ERROR_OUTOFMEMORY);
return false;
}
if (!InitializeProcThreadAttributeList(attrList, 1, 0, &size)) {
DWORD err = GetLastError();
BOOST_LOG(warning) << "assign_parent_attributes: InitializeProcThreadAttributeList failed: " << err;
HeapFree(GetProcessHeap(), 0, attrList);
attrList = nullptr;
SetLastError(err);
return false;
}
if (!UpdateProcThreadAttribute(attrList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parent, sizeof(parent), nullptr, nullptr)) {
DWORD err = GetLastError();
BOOST_LOG(warning) << "assign_parent_attributes: UpdateProcThreadAttribute failed: " << err;
DeleteProcThreadAttributeList(attrList);
HeapFree(GetProcessHeap(), 0, attrList);
attrList = nullptr;
SetLastError(err);
return false;
}
si.lpAttributeList = attrList;
return true;
}
void free_parent_attributes(LPPROC_THREAD_ATTRIBUTE_LIST attrList) {
if (attrList) {
DeleteProcThreadAttributeList(attrList);
HeapFree(GetProcessHeap(), 0, attrList);
}
}
bool launch_detached_command(const wchar_t *application, const std::wstring &cmd, STARTUPINFOEXW &si, PROCESS_INFORMATION &pi, DWORD flags) {
std::vector<wchar_t> cmdline(cmd.begin(), cmd.end());
cmdline.push_back(0);
return CreateProcessW(application, cmdline.data(), nullptr, nullptr, FALSE, flags, nullptr, nullptr, &si.StartupInfo, &pi);
}
void close_process_info(PROCESS_INFORMATION &pi) {
if (pi.hThread) {
CloseHandle(pi.hThread);
pi.hThread = nullptr;
}
if (pi.hProcess) {
CloseHandle(pi.hProcess);
pi.hProcess = nullptr;
}
}
bool launch_uri_detached_parented(const std::wstring &uri) {
// Try to parent the explorer-launched process to the shell; fall back to a vanilla launch if that fails.
HANDLE parent = open_explorer_parent_handle();
if (!parent) {
BOOST_LOG(warning) << "Unable to open explorer.exe as parent; proceeding without parent override";
}
auto attempt_launch = [&](bool use_parent) -> std::pair<bool, DWORD> {
STARTUPINFOEXW si {};
si.StartupInfo.cb = sizeof(si);
LPPROC_THREAD_ATTRIBUTE_LIST attrList = nullptr;
if (use_parent) {
if (!assign_parent_attributes(parent, si, attrList)) {
// Attribute wiring failed; drop back to a non-parented launch.
use_parent = false;
}
}
std::wstring exe = get_explorer_path();
std::wstring cmd = L"\"" + exe + L"\"";
if (!uri.empty()) {
cmd += L" ";
cmd += uri;
}
DWORD flags = CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW | DETACHED_PROCESS | CREATE_BREAKAWAY_FROM_JOB;
if (use_parent) {
flags |= EXTENDED_STARTUPINFO_PRESENT;
}
PROCESS_INFORMATION pi {};
BOOL ok = launch_detached_command(exe.c_str(), cmd, si, pi, flags);
DWORD err = ok ? ERROR_SUCCESS : GetLastError();
free_parent_attributes(attrList);
if (ok) {
close_process_info(pi);
}
return {ok != FALSE, err};
};
auto result = attempt_launch(parent != nullptr);
if (!result.first && result.second == ERROR_INVALID_HANDLE) {
BOOST_LOG(warning) << "CreateProcessW(explorer uri) failed with ERROR_INVALID_HANDLE; retrying without explicit parent";
result = attempt_launch(false);
}
if (parent) {
CloseHandle(parent);
}
if (!result.first) {
BOOST_LOG(warning) << "CreateProcessW(explorer uri) failed: " << result.second;
return false;
}
return true;
}
std::wstring query_assoc_string(ASSOCSTR str, LPCWSTR extra) {
std::array<wchar_t, 4096> buf {};
DWORD sz = static_cast<DWORD>(buf.size());
if (AssocQueryStringW(ASSOCF_NOTRUNCATE, str, L"playnite", extra, buf.data(), &sz) == S_OK && buf[0] != 0) {
return std::wstring(buf.data());
}
return std::wstring();
}
std::wstring parse_command_executable(const std::wstring &command) {
if (command.empty()) {
return std::wstring();
}
int argc = 0;
LPWSTR *argv = CommandLineToArgvW(command.c_str(), &argc);
std::wstring exe;
if (argv && argc >= 1) {
exe.assign(argv[0]);
} else if (!command.empty() && command.front() == L'"') {
auto pos = command.find(L'"', 1);
if (pos != std::wstring::npos) {
exe = command.substr(1, pos - 1);
}
}
if (argv) {
LocalFree(argv);
}
if (exe.empty()) {
auto space = command.find(L' ');
exe = (space == std::wstring::npos) ? command : command.substr(0, space);
}
return exe;
}
std::wstring query_playnite_executable_from_assoc() {
auto exe = query_assoc_string(ASSOCSTR_EXECUTABLE, nullptr);
if (!exe.empty()) {
return exe;
}
auto command = query_assoc_string(ASSOCSTR_COMMAND, L"open");
return parse_command_executable(command);
}
bool launch_executable_detached_parented_with_args(const std::wstring &exe_full_path, const std::wstring &args) {
// Build command line: "<exe>" <args>
std::wstring cmd = L"\"" + exe_full_path + L"\"";
if (!args.empty()) {
cmd += L" ";
cmd += args;
}
std::vector<wchar_t> cmdline(cmd.begin(), cmd.end());
cmdline.push_back(0);
std::wstring working_dir;
try {
std::filesystem::path p(exe_full_path);
working_dir = p.parent_path().wstring();
} catch (...) {
}
const wchar_t *cwd_ptr = working_dir.empty() ? nullptr : working_dir.c_str();
auto attempt_launch = [&](bool use_parent) -> std::pair<bool, DWORD> {
HANDLE parent = nullptr;
LPPROC_THREAD_ATTRIBUTE_LIST attrList = nullptr;
STARTUPINFOEXW si_ex {};
STARTUPINFOW si_basic {};
STARTUPINFOW *si_ptr = nullptr;
DWORD flags = EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT |
CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW | DETACHED_PROCESS | CREATE_BREAKAWAY_FROM_JOB;
if (use_parent) {
parent = open_explorer_parent_handle();
if (!parent) {
use_parent = false;
} else if (!assign_parent_attributes(parent, si_ex, attrList)) {
CloseHandle(parent);
parent = nullptr;
use_parent = false;
} else {
si_ex.StartupInfo.cb = sizeof(si_ex);
si_ptr = &si_ex.StartupInfo;
}
}
if (!use_parent) {
flags &= ~EXTENDED_STARTUPINFO_PRESENT;
}
if (!si_ptr) {
si_basic.cb = sizeof(si_basic);
si_ptr = &si_basic;
}
PROCESS_INFORMATION pi {};
BOOL ok = CreateProcessW(exe_full_path.c_str(), cmdline.data(), nullptr, nullptr, FALSE, flags, nullptr, cwd_ptr, si_ptr, &pi);
DWORD err = ok ? ERROR_SUCCESS : GetLastError();
if (pi.hThread) {
CloseHandle(pi.hThread);
}
if (pi.hProcess) {
CloseHandle(pi.hProcess);
}
if (attrList) {
free_parent_attributes(attrList);
}
if (parent) {
CloseHandle(parent);
}
return {ok != FALSE, err};
};
auto result = attempt_launch(true);
if (!result.first && result.second == ERROR_INVALID_HANDLE) {
BOOST_LOG(warning) << "CreateProcessW(executable with args) failed with ERROR_INVALID_HANDLE when using explorer parent; retrying without explicit parent";
result = attempt_launch(false);
}
if (result.first) {
return true;
}
BOOST_LOG(warning) << "CreateProcessW(executable with args) failed: " << result.second;
return false;
}
bool launch_executable_detached_parented(const std::wstring &exe_full_path) {
return launch_executable_detached_parented_with_args(exe_full_path, std::wstring());
}
bool spawn_cleanup_watchdog_process(const std::wstring &self_path, const std::string &install_dir_utf8, int exit_timeout_secs, bool fullscreen_flag, std::optional<DWORD> wait_for_pid) {
try {
const watchdog_cache_lock_t cache_lock;
const auto staged_watchdog = cache_lock ? stage_cleanup_watchdog(self_path) : std::nullopt;
const std::wstring watchdog_path = staged_watchdog ? staged_watchdog->path.wstring() : self_path;
if (staged_watchdog) {
BOOST_LOG(info) << "Cleanup watcher staged at '" << platf::dxgi::wide_to_utf8(watchdog_path)
<< "' sha256=" << platf::dxgi::wide_to_utf8(staged_watchdog->sha256);
} else if (!cache_lock) {
BOOST_LOG(warning) << "Cleanup watcher staging skipped: unable to acquire the cache lock; upgrade survival is unavailable";
} else {
BOOST_LOG(warning) << "Cleanup watcher will run from the installed executable; upgrade survival is unavailable";
}
std::wstring wcmd = quote_command_line_argument(watchdog_path) + L" --do-cleanup";
if (!install_dir_utf8.empty()) {
wcmd += L" --install-dir " + quote_command_line_argument(platf::dxgi::utf8_to_wide(install_dir_utf8));
}
if (exit_timeout_secs > 0) {
wcmd += L" --exit-timeout " + std::to_wstring(exit_timeout_secs);
}
if (fullscreen_flag) {
wcmd += L" --fullscreen";
}
if (wait_for_pid) {
wcmd += L" --wait-for-pid " + std::to_wstring(*wait_for_pid);
}
BOOST_LOG(info) << "Spawning cleanup watcher (fullscreen=" << fullscreen_flag << ", installDir='" << install_dir_utf8
<< "' waitPid=" << (wait_for_pid ? std::to_string(*wait_for_pid) : std::string("none")) << ")";
STARTUPINFOW si {};
si.cb = sizeof(si);
si.dwFlags |= STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
PROCESS_INFORMATION pi {};
std::wstring cmdline = wcmd;
DWORD flags_base = CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW | DETACHED_PROCESS;
DWORD flags_try = flags_base | CREATE_BREAKAWAY_FROM_JOB;
BOOL ok = CreateProcessW(watchdog_path.c_str(), cmdline.data(), nullptr, nullptr, FALSE, flags_try, nullptr, nullptr, &si, &pi);
bool created_with_breakaway = ok != FALSE;
if (!ok) {
const DWORD breakaway_error = GetLastError();
BOOST_LOG(warning) << "Cleanup watcher spawn with job breakaway failed, error=" << breakaway_error
<< "; retrying without breakaway";
ok = CreateProcessW(watchdog_path.c_str(), cmdline.data(), nullptr, nullptr, FALSE, flags_base, nullptr, nullptr, &si, &pi);
}
if (!ok) {
BOOST_LOG(warning) << "Cleanup watcher spawn failed (fullscreen=" << fullscreen_flag << ") error=" << GetLastError();
return false;
}
BOOL in_job = FALSE;
const BOOL job_query_succeeded = IsProcessInJob(pi.hProcess, nullptr, &in_job);
BOOST_LOG(info) << "Cleanup watcher spawned (fullscreen=" << fullscreen_flag << ", pid=" << pi.dwProcessId
<< ", breakaway_create=" << (created_with_breakaway ? "true" : "false")
<< ", in_job=" << (job_query_succeeded ? (in_job ? "true" : "false") : "unknown") << ")";
if (pi.hThread) {
CloseHandle(pi.hThread);
}
if (pi.hProcess) {
CloseHandle(pi.hProcess);
}
return true;
} catch (...) {
BOOST_LOG(warning) << "Exception launching cleanup watcher";
return false;
}
}
} // namespace playnite_launcher::playnite