// SPDX-License-Identifier: LGPL-2.1-or-later #include "bootstrapper_controls.h" #include #include #include #include #include #include namespace { template 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 automation; if (FAILED(CoCreateInstance(CLSID_CUIAutomation, nullptr, CLSCTX_INPROC_SERVER, IID_IUIAutomation, reinterpret_cast(&automation.value))) || !current()) return result; ComPtr condition; ComPtr walker; ComPtr 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 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 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> retained(static_cast *>(pointer)); auto state = *retained; const HRESULT com = CoInitializeEx(nullptr, COINIT_MULTITHREADED); for (;;) { HWND window; uint64_t generation; { std::unique_lock 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 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(); auto retained = new std::shared_ptr(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 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 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 lock(state_->mutex); state_->stopping = true; state_->pending = state_->ready = false; state_->changed.notify_one(); } WaitForSingleObject(worker_, 100); CloseHandle(worker_); worker_ = nullptr; state_.reset(); }