#include "tools/playnite_launcher/playnite_process.h" #include "src/logging.h" #include "src/platform/windows/ipc/misc_utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 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(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(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 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 buffer {}; while (true) { DWORD bytes_read = 0; if (!ReadFile(file, buffer.data(), static_cast(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 digest {}; DWORD digest_size = static_cast(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 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 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::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(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 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 { 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 buf {}; DWORD sz = static_cast(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: "" std::wstring cmd = L"\"" + exe_full_path + L"\""; if (!args.empty()) { cmd += L" "; cmd += args; } std::vector 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 { 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 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