210 lines
9.2 KiB
C++
210 lines
9.2 KiB
C++
// SPDX-License-Identifier: LGPL-2.1-or-later
|
|
#include "bootstrapper_controls.h"
|
|
#include <initguid.h>
|
|
#include <uiautomation.h>
|
|
#include <atomic>
|
|
#include <condition_variable>
|
|
#include <mutex>
|
|
#include <utility>
|
|
|
|
namespace {
|
|
template<class T> struct ComPtr {
|
|
T *value = nullptr;
|
|
~ComPtr() { if (value) value->Release(); }
|
|
};
|
|
struct Property {
|
|
VARIANT value{};
|
|
Property(IUIAutomationElement *element, PROPERTYID id) {
|
|
// Wine implements Ex; GetCurrentPropertyValue and many convenience
|
|
// getters return E_NOTIMPL in Wine 11.19.
|
|
if (FAILED(element->GetCurrentPropertyValueEx(id, TRUE, &value))) VariantClear(&value);
|
|
}
|
|
~Property() { VariantClear(&value); }
|
|
bool is_true() const { return value.vt == VT_BOOL && value.boolVal == VARIANT_TRUE; }
|
|
bool is_false() const { return value.vt == VT_BOOL && value.boolVal == VARIANT_FALSE; }
|
|
std::string string() const {
|
|
if (value.vt != VT_BSTR || !value.bstrVal) return {};
|
|
UINT length = SysStringLen(value.bstrVal);
|
|
if (length > 1023) length = 1023;
|
|
if (length && value.bstrVal[length - 1] >= 0xd800 &&
|
|
value.bstrVal[length - 1] <= 0xdbff) --length;
|
|
int size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS,
|
|
value.bstrVal, length, nullptr, 0, nullptr, nullptr);
|
|
std::string result(size > 0 ? size : 0, '\0');
|
|
if (size > 0) WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS,
|
|
value.bstrVal, length, result.data(), size, nullptr, nullptr);
|
|
return result;
|
|
}
|
|
};
|
|
std::string control_role(LONG type) {
|
|
switch (type) {
|
|
case UIA_ButtonControlTypeId: return "push button";
|
|
case UIA_ComboBoxControlTypeId: return "combo box";
|
|
case UIA_CheckBoxControlTypeId: return "check box";
|
|
case UIA_EditControlTypeId: return "edit";
|
|
case UIA_HyperlinkControlTypeId: return "link";
|
|
case UIA_GroupControlTypeId: return "grouping";
|
|
default: return {};
|
|
}
|
|
}
|
|
}
|
|
|
|
bool is_bootstrapper_window(HWND window) {
|
|
WCHAR className[128]{};
|
|
return window && GetClassNameW(window, className, 128) &&
|
|
lstrcmpW(className, L"Blizzard Bootstrapper") == 0;
|
|
}
|
|
|
|
BootstrapperControl read_bootstrapper_control(HWND window, uint64_t deadline) {
|
|
BootstrapperControl result;
|
|
if (!is_bootstrapper_window(window) || GetForegroundWindow() != window) return result;
|
|
result.window = window;
|
|
result.thread = GetWindowThreadProcessId(window, &result.process);
|
|
auto current = [&] { return GetTickCount64() < deadline &&
|
|
GetForegroundWindow() == window && is_bootstrapper_window(window); };
|
|
ComPtr<IUIAutomation> automation;
|
|
if (FAILED(CoCreateInstance(CLSID_CUIAutomation, nullptr, CLSCTX_INPROC_SERVER,
|
|
IID_IUIAutomation, reinterpret_cast<void **>(&automation.value))) || !current()) return result;
|
|
ComPtr<IUIAutomationCondition> condition;
|
|
ComPtr<IUIAutomationTreeWalker> walker;
|
|
ComPtr<IUIAutomationElement> root;
|
|
if (FAILED(automation.value->CreateTrueCondition(&condition.value)) || !current() ||
|
|
FAILED(automation.value->CreateTreeWalker(condition.value, &walker.value)) || !current() ||
|
|
FAILED(automation.value->ElementFromHandle(window, &root.value)) || !current()) return result;
|
|
IUIAutomationElement *child = nullptr;
|
|
if (FAILED(walker.value->GetFirstChildElement(root.value, &child))) return result;
|
|
std::string selectedLanguage;
|
|
// The bootstrapper exposes its language choices as siblings. A radio choice
|
|
// keeps HasKeyboardFocus even when Continue owns Tab focus: use the focused
|
|
// combo/button as the owner, and a radio only as the combo's current value.
|
|
for (unsigned count = 0; child && count < 64 && current(); ++count) {
|
|
ComPtr<IUIAutomationElement> element{child};
|
|
child = nullptr;
|
|
Property focused(element.value, UIA_HasKeyboardFocusPropertyId);
|
|
if (current() && focused.is_true()) {
|
|
Property type(element.value, UIA_ControlTypePropertyId);
|
|
Property offscreen(element.value, UIA_IsOffscreenPropertyId);
|
|
if (current() && type.value.vt == VT_I4 && !offscreen.is_true()) {
|
|
Property name(element.value, UIA_NamePropertyId);
|
|
if (type.value.lVal == UIA_RadioButtonControlTypeId) {
|
|
selectedLanguage = name.string();
|
|
} else if (result.role.empty()) {
|
|
result.role = control_role(type.value.lVal);
|
|
if (!result.role.empty()) {
|
|
result.name = name.string();
|
|
// IDs are local to this window's observed sibling layout. Wine's
|
|
// GetRuntimeId is also a stub; no UIA object reference crosses the
|
|
// worker boundary or becomes an actionable review target.
|
|
result.identity = ":" + std::to_string(count) + ":" + std::to_string(type.value.lVal);
|
|
Property password(element.value, UIA_IsPasswordPropertyId);
|
|
// Unknown protection is deliberately insufficient to read a value.
|
|
if (password.is_false() && current()) {
|
|
Property value(element.value, UIA_ValueValuePropertyId);
|
|
result.value = value.string();
|
|
} else if (password.is_true()) {
|
|
result.states = "password";
|
|
}
|
|
Property enabled(element.value, UIA_IsEnabledPropertyId);
|
|
if (enabled.is_false()) {
|
|
if (!result.states.empty()) result.states += ", ";
|
|
result.states += "unavailable";
|
|
}
|
|
if (type.value.lVal == UIA_CheckBoxControlTypeId && current()) {
|
|
Property checked(element.value, UIA_ToggleToggleStatePropertyId);
|
|
if (checked.value.vt == VT_I4 && checked.value.lVal >= ToggleState_Off &&
|
|
checked.value.lVal <= ToggleState_Indeterminate) {
|
|
if (!result.states.empty()) result.states += ", ";
|
|
result.states += checked.value.lVal == ToggleState_On ? "checked" :
|
|
checked.value.lVal == ToggleState_Indeterminate ? "half checked" : "not checked";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (current()) walker.value->GetNextSiblingElement(element.value, &child);
|
|
}
|
|
// A partial traversal must not present a stale/ambiguous selection.
|
|
bool complete = !child;
|
|
if (child) child->Release();
|
|
if (!complete || !current() || result.role.empty() || result.identity.empty()) return {};
|
|
// This UIA provider describes its custom language selector as a Group.
|
|
if (result.role == "grouping" && !selectedLanguage.empty()) result.role = "combo box";
|
|
if (result.role == "combo box" && !selectedLanguage.empty()) result.value = selectedLanguage;
|
|
result.available = true;
|
|
return result;
|
|
}
|
|
|
|
struct BootstrapperControlReader::State {
|
|
std::mutex mutex;
|
|
std::condition_variable changed;
|
|
std::atomic<bool> stopping{false};
|
|
HWND window = nullptr;
|
|
uint64_t generation = 0;
|
|
bool pending = false, busy = false, ready = false;
|
|
BootstrapperControl result;
|
|
static DWORD WINAPI work(LPVOID pointer) {
|
|
std::unique_ptr<std::shared_ptr<State>> retained(static_cast<std::shared_ptr<State> *>(pointer));
|
|
auto state = *retained;
|
|
const HRESULT com = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
|
for (;;) {
|
|
HWND window;
|
|
uint64_t generation;
|
|
{
|
|
std::unique_lock<std::mutex> lock(state->mutex);
|
|
state->changed.wait(lock, [&] { return state->stopping || state->pending; });
|
|
if (state->stopping) break;
|
|
window = state->window; generation = state->generation;
|
|
state->pending = false; state->busy = true;
|
|
}
|
|
const uint64_t deadline = GetTickCount64() + 500;
|
|
auto result = SUCCEEDED(com) ? read_bootstrapper_control(window, deadline) : BootstrapperControl{};
|
|
result.generation = generation; result.deadline = deadline;
|
|
std::lock_guard<std::mutex> lock(state->mutex);
|
|
state->busy = false;
|
|
if (!state->stopping && state->window == window && state->generation == generation) {
|
|
state->result = std::move(result); state->ready = true;
|
|
}
|
|
}
|
|
if (SUCCEEDED(com)) CoUninitialize();
|
|
return 0;
|
|
}
|
|
};
|
|
BootstrapperControlReader::~BootstrapperControlReader() { stop(); }
|
|
bool BootstrapperControlReader::start() {
|
|
if (state_) return false;
|
|
state_ = std::make_shared<State>();
|
|
auto retained = new std::shared_ptr<State>(state_);
|
|
worker_ = CreateThread(nullptr, 0, State::work, retained, 0, nullptr);
|
|
if (!worker_) { delete retained; state_.reset(); return false; }
|
|
return true;
|
|
}
|
|
void BootstrapperControlReader::observe(HWND window) {
|
|
if (!state_) return;
|
|
std::lock_guard<std::mutex> lock(state_->mutex);
|
|
if (state_->window != window) {
|
|
state_->window = window; ++state_->generation;
|
|
state_->pending = state_->ready = false;
|
|
}
|
|
if (window && !state_->stopping && !state_->busy && !state_->pending && !state_->ready) {
|
|
state_->pending = true; state_->changed.notify_one();
|
|
}
|
|
}
|
|
bool BootstrapperControlReader::take(BootstrapperControl &result) {
|
|
if (!state_) return false;
|
|
std::lock_guard<std::mutex> lock(state_->mutex);
|
|
if (!state_->ready) return false;
|
|
result = std::move(state_->result); state_->ready = false;
|
|
return true;
|
|
}
|
|
void BootstrapperControlReader::stop() {
|
|
if (!state_) return;
|
|
{
|
|
std::lock_guard<std::mutex> lock(state_->mutex);
|
|
state_->stopping = true; state_->pending = state_->ready = false;
|
|
state_->changed.notify_one();
|
|
}
|
|
WaitForSingleObject(worker_, 100);
|
|
CloseHandle(worker_); worker_ = nullptr; state_.reset();
|
|
}
|