server/tools/playnite_launcher/focus_utils.cpp
Rama main · réplica de lectura del repositorio original.
#include "tools/playnite_launcher/focus_utils.h"
#include "src/logging.h"
#include "src/platform/windows/ipc/misc_utils.h"
#include <algorithm>
#include <chrono>
#include <Psapi.h>
#include <thread>
#include <TlHelp32.h>
#include <vector>
#include <windows.h>
using namespace std::chrono_literals;
namespace playnite_launcher::focus {
namespace {
std::wstring normalize_path(std::wstring value) {
for (auto &ch : value) {
if (ch == L'/') {
ch = L'\\';
}
ch = static_cast<wchar_t>(std::towlower(ch));
}
return value;
}
bool has_prefix_with_boundary(const std::wstring &path, const std::wstring &dir) {
if (path.size() < dir.size()) {
return false;
}
if (path.compare(0, dir.size(), dir) != 0) {
return false;
}
if (path.size() == dir.size()) {
return true;
}
return path[dir.size()] == L'\\';
}
bool path_starts_with_dir(const std::wstring &path, const std::wstring &dir) {
if (dir.empty()) {
return false;
}
auto normalized_dir = normalize_path(dir);
// Never strip a bare drive root down to its designator: "d:\" would become
// "d:", and the boundary check below would then accept every path on that
// volume. Matches the guard in playnite_process.cpp's normalizer.
while (
normalized_dir.size() > 1 &&
normalized_dir.back() == L'\\' &&
!(normalized_dir.size() == 3 && normalized_dir[1] == L':')
) {
normalized_dir.pop_back();
}
return has_prefix_with_boundary(normalize_path(path), normalized_dir);
}
bool is_candidate_window(HWND hwnd, DWORD pid) {
DWORD owner = 0;
GetWindowThreadProcessId(hwnd, &owner);
if (owner != pid) {
return false;
}
if (!IsWindowVisible(hwnd)) {
return false;
}
return GetWindow(hwnd, GW_OWNER) == nullptr;
}
BOOL CALLBACK find_window_proc(HWND hwnd, LPARAM param) {
auto *result = reinterpret_cast<HWND *>(param);
DWORD pid = static_cast<DWORD>(reinterpret_cast<uintptr_t>(*result));
if (is_candidate_window(hwnd, pid)) {
*result = hwnd;
return FALSE;
}
return TRUE;
}
SIZE_T query_working_set(DWORD pid) {
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ, FALSE, pid);
if (!process) {
return 0;
}
PROCESS_MEMORY_COUNTERS_EX pmc {};
SIZE_T size = 0;
if (GetProcessMemoryInfo(process, reinterpret_cast<PROCESS_MEMORY_COUNTERS *>(&pmc), sizeof(pmc))) {
size = pmc.WorkingSetSize;
}
CloseHandle(process);
return size;
}
std::vector<DWORD> enumerate_process_ids() {
DWORD needed = 0;
std::vector<DWORD> ids(1024);
if (!EnumProcesses(ids.data(), static_cast<DWORD>(ids.size() * sizeof(DWORD)), &needed)) {
return {};
}
if (needed > ids.size() * sizeof(DWORD)) {
ids.resize((needed / sizeof(DWORD)) + 256);
if (!EnumProcesses(ids.data(), static_cast<DWORD>(ids.size() * sizeof(DWORD)), &needed)) {
return {};
}
}
ids.resize(needed / sizeof(DWORD));
return ids;
}
bool process_matches_dir(DWORD pid, const std::wstring &dir, std::wstring &path) {
if (!get_process_image_path(pid, path)) {
return false;
}
return path_starts_with_dir(path, dir);
}
bool append_candidate_if_match(DWORD pid, const std::wstring &install_dir, std::vector<std::pair<DWORD, SIZE_T>> &matches, bool require_window) {
if (pid == 0) {
return false;
}
std::wstring path;
if (!process_matches_dir(pid, install_dir, path)) {
return false;
}
if (require_window && !find_main_window_for_pid(pid)) {
return false;
}
matches.push_back({pid, query_working_set(pid)});
return true;
}
std::vector<DWORD> extract_sorted_pids(std::vector<std::pair<DWORD, SIZE_T>> matches) {
std::sort(matches.begin(), matches.end(), [](const auto &a, const auto &b) {
return a.second > b.second;
});
std::vector<DWORD> result;
result.reserve(matches.size());
for (auto &item : matches) {
result.push_back(item.first);
}
return result;
}
} // namespace
HWND find_main_window_for_pid(DWORD pid) {
HWND result = reinterpret_cast<HWND>(static_cast<uintptr_t>(pid));
EnumWindows(find_window_proc, reinterpret_cast<LPARAM>(&result));
return is_candidate_window(result, pid) ? result : nullptr;
}
bool try_focus_hwnd(HWND hwnd) {
if (!hwnd) {
return false;
}
HWND fg = GetForegroundWindow();
DWORD fg_tid = fg ? GetWindowThreadProcessId(fg, nullptr) : 0;
DWORD cur_tid = GetCurrentThreadId();
if (fg && fg_tid != 0 && fg_tid != cur_tid) {
AttachThreadInput(cur_tid, fg_tid, TRUE);
}
ShowWindow(hwnd, SW_RESTORE);
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
BOOL ok = SetForegroundWindow(hwnd);
SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
if (fg && fg_tid != 0 && fg_tid != cur_tid) {
AttachThreadInput(cur_tid, fg_tid, FALSE);
}
return ok != FALSE;
}
bool confirm_foreground_pid(DWORD pid) {
if (!pid) {
return false;
}
HWND fg = GetForegroundWindow();
if (!fg) {
return false;
}
if (!IsWindowVisible(fg) || IsIconic(fg) || GetWindow(fg, GW_OWNER) != nullptr) {
return false;
}
DWORD fpid = 0;
GetWindowThreadProcessId(fg, &fpid);
if (fpid != pid) {
return false;
}
auto is_cloaked = [&](HWND hwnd) -> bool {
using DwmGetWindowAttribute_t = HRESULT(WINAPI *)(HWND, DWORD, PVOID, DWORD);
static HMODULE dwm = LoadLibraryW(L"dwmapi.dll");
static auto fn = dwm ? reinterpret_cast<DwmGetWindowAttribute_t>(GetProcAddress(dwm, "DwmGetWindowAttribute")) : nullptr;
if (!fn) {
return false;
}
DWORD cloaked = 0;
constexpr DWORD kDwmwaCloaked = 14;
if (SUCCEEDED(fn(hwnd, kDwmwaCloaked, &cloaked, sizeof(cloaked)))) {
return cloaked != 0;
}
return false;
};
if (is_cloaked(fg)) {
return false;
}
RECT rect {};
if (!GetWindowRect(fg, &rect)) {
return false;
}
if (rect.right <= rect.left || rect.bottom <= rect.top) {
return false;
}
if (!MonitorFromRect(&rect, MONITOR_DEFAULTTONULL)) {
return false;
}
return true;
}
bool focus_process_by_name_extended(const wchar_t *exe_name_w, int max_successes, int timeout_secs, bool exit_on_first, std::function<bool()> cancel, DWORD *confirmed_pid_out) {
if (!exe_name_w || timeout_secs <= 0 || max_successes < 0) {
return false;
}
if (confirmed_pid_out) {
*confirmed_pid_out = 0;
}
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(std::max(1, timeout_secs));
int successes = 0;
bool any = false;
auto last_apply = std::chrono::steady_clock::now() - 1s;
while (std::chrono::steady_clock::now() < deadline) {
if (cancel && cancel()) {
break;
}
std::vector<DWORD> pids;
try {
pids = platf::dxgi::find_process_ids_by_name(exe_name_w);
} catch (...) {
pids.clear();
}
if (pids.empty()) {
std::this_thread::sleep_for(1s);
continue;
}
for (auto pid : pids) {
if (cancel && cancel()) {
break;
}
if (confirm_foreground_pid(pid)) {
successes++;
any = true;
if (confirmed_pid_out) {
*confirmed_pid_out = pid;
}
BOOST_LOG(info) << "Confirmed focus for PID=" << pid << " (already foreground), successes=" << successes;
if (exit_on_first || (max_successes > 0 && successes >= max_successes)) {
return true;
}
std::this_thread::sleep_for(200ms);
continue;
}
auto now = std::chrono::steady_clock::now();
if (now - last_apply < 1s) {
continue;
}
HWND hwnd = find_main_window_for_pid(pid);
if (hwnd && try_focus_hwnd(hwnd)) {
std::this_thread::sleep_for(100ms);
if (confirm_foreground_pid(pid)) {
successes++;
any = true;
if (confirmed_pid_out) {
*confirmed_pid_out = pid;
}
BOOST_LOG(info) << "Confirmed focus for PID=" << pid << ", successes=" << successes;
if (exit_on_first || (max_successes > 0 && successes >= max_successes)) {
return true;
}
}
}
last_apply = now;
}
std::this_thread::sleep_for(1s);
}
return any;
}
std::vector<DWORD> find_pids_under_install_dir_sorted(const std::wstring &install_dir, bool require_window) {
auto pids = enumerate_process_ids();
std::vector<std::pair<DWORD, SIZE_T>> matches;
matches.reserve(pids.size());
for (auto pid : pids) {
append_candidate_if_match(pid, install_dir, matches, require_window);
}
return extract_sorted_pids(std::move(matches));
}
std::vector<DWORD> find_pids_under_install_dir_sorted(const std::wstring &install_dir) {
return find_pids_under_install_dir_sorted(install_dir, true);
}
bool focus_by_install_dir_extended(const std::wstring &install_dir, int max_successes, int total_wait_sec, bool exit_on_first, std::function<bool()> cancel, DWORD *confirmed_pid_out) {
if (install_dir.empty() || total_wait_sec <= 0 || max_successes < 0) {
return false;
}
if (confirmed_pid_out) {
*confirmed_pid_out = 0;
}
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(std::max(1, total_wait_sec));
int successes = 0;
bool any = false;
auto last_apply = std::chrono::steady_clock::now() - 1s;
while (std::chrono::steady_clock::now() < deadline) {
if (cancel && cancel()) {
break;
}
auto candidates = find_pids_under_install_dir_sorted(install_dir);
if (candidates.empty()) {
std::this_thread::sleep_for(1s);
continue;
}
for (auto pid : candidates) {
if (cancel && cancel()) {
break;
}
if (confirm_foreground_pid(pid)) {
successes++;
any = true;
if (confirmed_pid_out) {
*confirmed_pid_out = pid;
}
BOOST_LOG(info) << "Confirmed focus (installDir) for PID=" << pid << " (already foreground), successes=" << successes;
if (exit_on_first || (max_successes > 0 && successes >= max_successes)) {
return true;
}
continue;
}
auto now = std::chrono::steady_clock::now();
if (now - last_apply < 1s) {
continue;
}
HWND hwnd = find_main_window_for_pid(pid);
if (hwnd && try_focus_hwnd(hwnd)) {
std::this_thread::sleep_for(100ms);
if (confirm_foreground_pid(pid)) {
successes++;
any = true;
if (confirmed_pid_out) {
*confirmed_pid_out = pid;
}
BOOST_LOG(info) << "Confirmed focus (installDir) for PID=" << pid << ", successes=" << successes;
if (exit_on_first || (max_successes > 0 && successes >= max_successes)) {
return true;
}
}
}
last_apply = now;
}
std::this_thread::sleep_for(1s);
}
return any;
}
bool get_process_image_path(DWORD pid, std::wstring &out) {
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (!h) {
return false;
}
wchar_t buf[MAX_PATH];
DWORD sz = ARRAYSIZE(buf);
BOOL ok = QueryFullProcessImageNameW(h, 0, buf, &sz);
CloseHandle(h);
if (!ok) {
return false;
}
out.assign(buf, sz);
return true;
}
void terminate_pid(DWORD pid) {
HANDLE h = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
if (!h) {
return;
}
TerminateProcess(h, 1);
CloseHandle(h);
}
} // namespace playnite_launcher::focus