// SPDX-License-Identifier: LGPL-2.1-or-later #include "msaa_events.h" #include "msaa_states.h" #include #include #include #include #include #include namespace { // MSAA notifications describe transient state, unlike ordered editor actions. // Bound pending work and favor current focus if an application floods events. constexpr size_t capacity = 32; std::string text(BSTR value) { if (!value) return {}; int size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value, -1, nullptr, 0, nullptr, nullptr); if (size <= 1) return {}; std::string result(size, '\0'); WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value, -1, result.data(), size, nullptr, nullptr); result.resize(size - 1); return result; } std::string property(IAccessible *object, HRESULT (STDMETHODCALLTYPE IAccessible::*getter)(VARIANT, BSTR *), VARIANT child) { BSTR value = nullptr; HRESULT hr = (object->*getter)(child, &value); auto result = SUCCEEDED(hr) ? text(value) : std::string{}; SysFreeString(value); return result; } struct Accessible { IAccessible *object = nullptr; VARIANT child{}; ~Accessible() { if (object) object->Release(); VariantClear(&child); } }; } bool msaa_event_in_foreground(HWND window) { HWND foreground = GetForegroundWindow(); HWND root = GetAncestor(window, GA_ROOT); if (!foreground || !root) return false; if (root == foreground) return true; if (!IsWindowVisible(root) || !(GetWindowLongPtrW(root, GWL_STYLE) & WS_POPUP)) return false; // Bound traversal even if a malformed owner chain cycles. Owned popups can // expose MSAA focus while the foreground owner retains native keyboard focus. for (unsigned depth = 0; root && depth < 32; ++depth) { root = GetWindow(root, GW_OWNER); if (root == foreground) return true; } return false; } struct MsaaEventReader::State { std::mutex mutex; std::condition_variable changed; std::deque pending; std::deque ready; std::atomic stopping{false}; uint64_t generation = 0; bool current(const MsaaEvent &event) { if (stopping || GetTickCount64() >= event.deadline || !msaa_event_in_foreground(event.window)) return false; std::lock_guard lock(mutex); return generation == event.generation; } MsaaResult read(const MsaaEvent &event) { MsaaResult result; result.source = event; Accessible accessible; if (!current(event) || FAILED(AccessibleObjectFromEvent(event.window, event.object, event.child, &accessible.object, &accessible.child)) || !accessible.object) return result; if (!current(event)) return result; VARIANT role{}; LONG roleId = 0; if (SUCCEEDED(accessible.object->get_accRole(accessible.child, &role))) { if (role.vt == VT_I4) { roleId = role.lVal; WCHAR label[128]{}; if (GetRoleTextW(roleId, label, 128)) result.role = text(label); else result.role = std::to_string(roleId); } else if (role.vt == VT_BSTR) result.role = text(role.bstrVal); } VariantClear(&role); if (!current(event)) return result; VARIANT state{}; if (SUCCEEDED(accessible.object->get_accState(accessible.child, &state)) && state.vt == VT_I4) { result.states = msaa_states(static_cast(state.lVal), roleId); result.password = state.lVal & STATE_SYSTEM_PROTECTED; } VariantClear(&state); if (!current(event)) return result; result.name = property(accessible.object, &IAccessible::get_accName, accessible.child); if (!current(event)) return result; // Protected values must be excluded before querying the provider. if (!result.password) result.value = property(accessible.object, &IAccessible::get_accValue, accessible.child); if (!current(event)) return result; result.description = property(accessible.object, &IAccessible::get_accDescription, accessible.child); if (!current(event)) return result; accessible.object->get_accChildCount(&result.count); result.native_client = accessible.child.vt == VT_I4 && accessible.child.lVal == CHILDID_SELF && (result.role.empty() || roleId == ROLE_SYSTEM_CLIENT); result.available = true; return result; } static DWORD WINAPI work(LPVOID pointer) { std::unique_ptr> retained(static_cast*>(pointer)); auto state = *retained; const HRESULT com = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); for (;;) { MsaaEvent event; { std::unique_lock lock(state->mutex); state->changed.wait(lock, [&] { return state->stopping || !state->pending.empty(); }); if (state->stopping) break; event = state->pending.front(); state->pending.pop_front(); } auto result = SUCCEEDED(com) ? state->read(event) : MsaaResult{}; result.source = event; if (!state->current(event)) continue; std::lock_guard lock(state->mutex); if (state->stopping || event.generation != state->generation) continue; if (state->ready.size() == capacity) state->ready.pop_front(); state->ready.push_back(std::move(result)); } if (SUCCEEDED(com)) CoUninitialize(); return 0; } }; MsaaEventReader::~MsaaEventReader() { stop(); } bool MsaaEventReader::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 MsaaEventReader::invalidate(uint64_t generation) { if (!state_) return; std::lock_guard lock(state_->mutex); state_->generation = generation; state_->pending.clear(); state_->ready.clear(); } void MsaaEventReader::submit(const MsaaEvent &event) { if (!state_) return; std::lock_guard lock(state_->mutex); if (state_->stopping || state_->generation != event.generation) return; if (state_->pending.size() == capacity) state_->pending.pop_front(); state_->pending.push_back(event); state_->changed.notify_one(); } bool MsaaEventReader::take(MsaaResult &result) { if (!state_) return false; std::lock_guard lock(state_->mutex); if (state_->ready.empty()) return false; result = std::move(state_->ready.front()); state_->ready.pop_front(); return true; } void MsaaEventReader::stop() { if (!state_) return; { std::lock_guard lock(state_->mutex); state_->stopping = true; state_->pending.clear(); state_->ready.clear(); state_->changed.notify_one(); } // A hung COM call cannot be safely terminated. It retains only its own // shared state and native references until completion or process exit. WaitForSingleObject(worker_, 100); CloseHandle(worker_); worker_ = nullptr; state_.reset(); }