server/tools/playnite_launcher/cleanup.cpp
Rama main · réplica de lectura del repositorio original.
#include "tools/playnite_launcher/cleanup.h"
#include "src/logging.h"
#include "src/platform/windows/ipc/misc_utils.h"
#include "tools/playnite_launcher/focus_utils.h"
#include "tools/playnite_launcher/playnite_process.h"
#include <algorithm>
#include <chrono>
#include <filesystem>
#include <functional>
#include <string>
#include <thread>
#include <TlHelp32.h>
#include <vector>
#include <windows.h>
using namespace std::chrono_literals;
namespace playnite_launcher::cleanup {
namespace {
using WindowFn = std::function<void(HWND)>;
void send_message_timeout(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
SendMessageTimeoutW(hwnd, msg, wParam, lParam, SMTO_ABORTIFHUNG, 5000, nullptr);
}
void enumerate_top_windows(DWORD pid, const WindowFn &fn) {
struct Ctx {
DWORD target;
const WindowFn *fn;
} ctx {pid, &fn};
auto proc = [](HWND hwnd, LPARAM param) -> BOOL {
auto *ctx = reinterpret_cast<Ctx *>(param);
DWORD owner = 0;
GetWindowThreadProcessId(hwnd, &owner);
if (owner == ctx->target) {
(*ctx->fn)(hwnd);
}
return TRUE;
};
EnumWindows(proc, reinterpret_cast<LPARAM>(&ctx));
}
HANDLE create_thread_snapshot() {
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
return snap == INVALID_HANDLE_VALUE ? nullptr : snap;
}
void enumerate_pid_threads(DWORD pid, const std::function<void(DWORD)> &fn) {
HANDLE snap = create_thread_snapshot();
if (!snap) {
return;
}
THREADENTRY32 te {sizeof(te)};
if (!Thread32First(snap, &te)) {
CloseHandle(snap);
return;
}
do {
if (te.th32OwnerProcessID == pid) {
fn(te.th32ThreadID);
}
} while (Thread32Next(snap, &te));
CloseHandle(snap);
}
void enumerate_thread_windows(DWORD thread_id, const WindowFn &fn) {
auto proc = [](HWND hwnd, LPARAM param) -> BOOL {
auto *f = reinterpret_cast<const WindowFn *>(param);
(*f)(hwnd);
return TRUE;
};
EnumThreadWindows(thread_id, proc, reinterpret_cast<LPARAM>(&fn));
}
void for_each_thread_window(DWORD pid, const WindowFn &fn) {
enumerate_pid_threads(pid, [&](DWORD thread_id) {
enumerate_thread_windows(thread_id, fn);
});
}
void post_close_messages(DWORD pid) {
enumerate_top_windows(pid, [&](HWND hwnd) {
send_message_timeout(hwnd, WM_CLOSE, 0, 0);
});
}
void post_logoff_messages(DWORD pid) {
enumerate_top_windows(pid, [&](HWND hwnd) {
send_message_timeout(hwnd, WM_QUERYENDSESSION, TRUE, ENDSESSION_CLOSEAPP);
});
enumerate_top_windows(pid, [&](HWND hwnd) {
send_message_timeout(hwnd, WM_ENDSESSION, TRUE, 0);
});
}
void post_quit_messages(DWORD pid) {
for_each_thread_window(pid, [&](HWND hwnd) {
PostMessageW(hwnd, WM_QUIT, 0, 0);
});
}
void signal_console_threads(DWORD pid) {
enumerate_pid_threads(pid, [&](DWORD thread_id) {
HANDLE thread = OpenThread(THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE, thread_id);
if (thread) {
GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, thread_id);
CloseHandle(thread);
}
});
}
bool ensure_window_minimized(HWND hwnd, std::chrono::milliseconds timeout) {
if (!hwnd) {
return false;
}
auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline) {
send_message_timeout(hwnd, WM_SYSCOMMAND, SC_RESTORE, 0);
ShowWindow(hwnd, SW_RESTORE);
send_message_timeout(hwnd, WM_SYSCOMMAND, SC_MINIMIZE, 0);
ShowWindow(hwnd, SW_SHOWMINNOACTIVE);
if (IsIconic(hwnd)) {
return true;
}
std::this_thread::sleep_for(100ms);
}
return IsIconic(hwnd) != FALSE;
}
struct CleanupPlan {
std::wstring install_dir;
int timeout_secs;
std::chrono::steady_clock::time_point start;
bool sent_close = false;
bool sent_logoff = false;
bool sent_quit = false;
bool logged_initial = false;
};
CleanupPlan make_plan(const std::wstring &install_dir, int timeout_secs) {
CleanupPlan plan;
plan.install_dir = install_dir;
plan.timeout_secs = std::max(1, timeout_secs);
plan.start = std::chrono::steady_clock::now();
return plan;
}
std::vector<DWORD> collect_candidates(const std::wstring &install_dir) {
auto pids = focus::find_pids_under_install_dir_sorted(install_dir);
if (pids.empty()) {
pids = focus::find_pids_under_install_dir_sorted(install_dir, false);
}
return pids;
}
double elapsed_fraction(const CleanupPlan &plan) {
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - plan.start).count();
double total = static_cast<double>(plan.timeout_secs) * 1000.0;
return std::clamp(elapsed / total, 0.0, 1.0);
}
void log_initial_once(CleanupPlan &plan, const std::vector<DWORD> &pids) {
if (plan.logged_initial) {
return;
}
plan.logged_initial = true;
BOOST_LOG(info) << "Cleanup: initial candidates count=" << pids.size();
for (auto pid : pids) {
std::wstring img;
focus::get_process_image_path(pid, img);
BOOST_LOG(info) << "Cleanup: candidate PID=" << pid << " path='" << platf::dxgi::wide_to_utf8(img) << "'";
}
}
void apply_stages(CleanupPlan &plan, const std::vector<DWORD> &pids) {
double fraction = elapsed_fraction(plan);
if (!plan.sent_close) {
for (auto pid : pids) {
post_close_messages(pid);
}
plan.sent_close = true;
return;
}
if (fraction >= 0.4 && !plan.sent_logoff) {
for (auto pid : pids) {
post_logoff_messages(pid);
}
plan.sent_logoff = true;
return;
}
if (fraction >= 0.7 && !plan.sent_quit) {
for (auto pid : pids) {
post_quit_messages(pid);
signal_console_threads(pid);
}
plan.sent_quit = true;
}
}
void force_terminate(const std::wstring &install_dir) {
auto remaining = collect_candidates(install_dir);
for (auto pid : remaining) {
std::wstring img;
focus::get_process_image_path(pid, img);
BOOST_LOG(warning) << "Cleanup: forcing termination of PID=" << pid
<< (img.empty() ? "" : (" path=" + platf::dxgi::wide_to_utf8(img)));
focus::terminate_pid(pid);
}
}
bool launch_desktop_command(const std::wstring &path, DWORD flags) {
std::wstring cmd = L"\"" + path + L"\" --startdesktop";
std::vector<wchar_t> cmdline(cmd.begin(), cmd.end());
cmdline.push_back(0);
STARTUPINFOW si {};
si.cb = sizeof(si);
si.dwFlags |= STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
PROCESS_INFORMATION pi {};
BOOL ok = CreateProcessW(path.c_str(), cmdline.data(), nullptr, nullptr, FALSE, flags, nullptr, nullptr, &si, &pi);
if (pi.hThread) {
CloseHandle(pi.hThread);
}
if (pi.hProcess) {
CloseHandle(pi.hProcess);
}
return ok;
}
std::wstring resolve_desktop_path() {
try {
std::wstring assoc = playnite::query_playnite_executable_from_assoc();
if (assoc.empty()) {
return {};
}
std::filesystem::path base(assoc);
if (_wcsicmp(base.filename().c_str(), L"Playnite.DesktopApp.exe") == 0) {
return assoc;
}
std::filesystem::path candidate = base.parent_path() / L"Playnite.DesktopApp.exe";
if (std::filesystem::exists(candidate)) {
return candidate.wstring();
}
return assoc;
} catch (...) {
return {};
}
}
bool launch_desktop_app(const std::wstring &path) {
if (path.empty() || !std::filesystem::exists(path)) {
BOOST_LOG(warning) << "Cleanup fullscreen: unable to resolve Playnite.DesktopApp path";
return false;
}
DWORD base_flags = CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW | DETACHED_PROCESS;
bool ok = launch_desktop_command(path, base_flags | CREATE_BREAKAWAY_FROM_JOB);
if (!ok) {
ok = launch_desktop_command(path, base_flags);
}
BOOST_LOG(info) << "Cleanup fullscreen: launch DesktopApp attempt result=" << (ok ? "ok" : "fail");
return ok;
}
std::vector<DWORD> wait_for_process(const wchar_t *exe, std::chrono::steady_clock::time_point deadline, std::chrono::milliseconds step) {
std::vector<DWORD> ids;
while (std::chrono::steady_clock::now() < deadline) {
try {
ids = platf::dxgi::find_process_ids_by_name(exe);
} catch (...) {
ids.clear();
}
if (!ids.empty()) {
break;
}
std::this_thread::sleep_for(step);
}
return ids;
}
bool minimize_desktop_once(const std::vector<DWORD> &pids) {
auto deadline = std::chrono::steady_clock::now() + 30s;
while (std::chrono::steady_clock::now() < deadline) {
for (auto pid : pids) {
HWND hwnd = focus::find_main_window_for_pid(pid);
if (!hwnd) {
continue;
}
if (!IsWindow(hwnd)) {
return true;
}
if (IsWindowVisible(hwnd) && !IsIconic(hwnd)) {
if (!ensure_window_minimized(hwnd, 5s)) {
BOOST_LOG(warning) << "Cleanup fullscreen: failed to confirm DesktopApp minimized";
}
return true;
}
}
std::this_thread::sleep_for(300ms);
}
BOOST_LOG(info) << "Cleanup fullscreen: DesktopApp window never reported visible before timeout";
return false;
}
bool wait_for_fullscreen_exit(int exit_timeout_secs) {
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(std::max(3, exit_timeout_secs));
while (std::chrono::steady_clock::now() < deadline) {
try {
auto ids = platf::dxgi::find_process_ids_by_name(L"Playnite.FullscreenApp.exe");
if (ids.empty()) {
return true;
}
} catch (...) {
}
std::this_thread::sleep_for(250ms);
}
return false;
}
} // namespace
void cleanup_graceful_then_forceful_in_dir(const std::wstring &install_dir, int exit_timeout_secs) {
if (install_dir.empty()) {
return;
}
BOOST_LOG(info) << "Cleanup: begin for install_dir='" << platf::dxgi::wide_to_utf8(install_dir)
<< "' timeout=" << exit_timeout_secs << "s";
CleanupPlan plan = make_plan(install_dir, exit_timeout_secs);
while (true) {
auto pids = collect_candidates(plan.install_dir);
log_initial_once(plan, pids);
if (pids.empty()) {
BOOST_LOG(info) << "Cleanup: all processes exited gracefully";
return;
}
apply_stages(plan, pids);
if (elapsed_fraction(plan) >= 1.0) {
break;
}
std::this_thread::sleep_for(250ms);
}
force_terminate(plan.install_dir);
}
void cleanup_fullscreen_via_desktop(int exit_timeout_secs) {
BOOST_LOG(info) << "Cleanup fullscreen: launching DesktopApp to close fullscreen";
std::wstring desktop_path = resolve_desktop_path();
launch_desktop_app(desktop_path);
auto wait_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(std::max(3, exit_timeout_secs));
auto desktop_pids = wait_for_process(L"Playnite.DesktopApp.exe", wait_deadline, 200ms);
if (!desktop_pids.empty()) {
minimize_desktop_once(desktop_pids);
}
if (wait_for_fullscreen_exit(exit_timeout_secs)) {
BOOST_LOG(info) << "Cleanup fullscreen: FullscreenApp exited after desktop launch";
}
}
} // namespace playnite_launcher::cleanup