// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/accessibility/platform/inspect/ax_event_recorder_win_uia.h"
#include <numeric>
#include <utility>
#include <psapi.h>
#include "base/logging.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/scoped_bstr.h"
#include "base/win/scoped_com_initializer.h"
#include "base/win/scoped_safearray.h"
#include "base/win/scoped_variant.h"
#include "base/win/windows_version.h"
#include "build/build_config.h"
#include "ui/accessibility/platform/inspect/ax_inspect_utils_win.h"
#include "ui/accessibility/platform/uia_registrar_win.h"
#include "ui/base/win/atl_module.h"
namespace ui {
namespace {
#if defined(COMPILER_MSVC)
#define RETURN_ADDRESS() _ReturnAddress()
#elif defined(COMPILER_GCC) && !BUILDFLAG(IS_NACL)
#define RETURN_ADDRESS() \
__builtin_extract_return_addr(__builtin_return_address(0))
#else
#define RETURN_ADDRESS() nullptr
#endif
static std::pair<uintptr_t, uintptr_t> GetModuleAddressRange(
const wchar_t* module_name) {
MODULEINFO info;
CHECK(GetModuleInformation(GetCurrentProcess(), GetModuleHandle(module_name),
&info, sizeof(info)));
const uintptr_t start = reinterpret_cast<uintptr_t>(info.lpBaseOfDll);
return std::make_pair(start, start + info.SizeOfImage);
}
std::string UiaIdentifierToStringPretty(int32_t id) {
auto str = base::WideToUTF8(UiaIdentifierToString(id));
// Remove UIA_ prefix, and EventId/PropertyId suffixes
if (base::StartsWith(str, "UIA_", base::CompareCase::SENSITIVE))
str = str.substr(std::size("UIA_") - 1);
if (base::EndsWith(str, "EventId", base::CompareCase::SENSITIVE))
str = str.substr(0, str.size() - std::size("EventId") + 1);
if (base::EndsWith(str, "PropertyId", base::CompareCase::SENSITIVE))
str = str.substr(0, str.size() - std::size("PropertyId") + 1);
return str;
}
} // namespace
// static
volatile base::subtle::Atomic32 AXEventRecorderWinUia::instantiated_ = 0;
AXEventRecorderWinUia::AXEventRecorderWinUia(const AXTreeSelector& selector) {
CHECK(!base::subtle::NoBarrier_AtomicExchange(&instantiated_, 1))
<< "There can be only one instance at a time.";
// Find the root content window
HWND hwnd = GetHWNDBySelector(selector);
CHECK(hwnd);
// Create the event thread, and pump messages via |initialization_loop| until
// initialization is complete.
base::RunLoop initialization_loop;
base::PlatformThread::Create(0, &thread_, &thread_handle_);
thread_.Init(this, hwnd, initialization_loop, shutdown_loop_);
initialization_loop.Run();
}
AXEventRecorderWinUia::~AXEventRecorderWinUia() {
base::subtle::NoBarrier_AtomicExchange(&instantiated_, 0);
}
void AXEventRecorderWinUia::WaitForDoneRecording() {
// Pump messages via |shutdown_loop_| until the thread is complete.
shutdown_loop_.Run();
base::PlatformThread::Join(thread_handle_);
}
AXEventRecorderWinUia::Thread::Thread() {}
AXEventRecorderWinUia::Thread::~Thread() {}
void AXEventRecorderWinUia::Thread::Init(AXEventRecorderWinUia* owner,
HWND hwnd,
base::RunLoop& initialization,
base::RunLoop& shutdown) {
owner_ = owner;
hwnd_ = hwnd;
initialization_complete_ = initialization.QuitClosure();
shutdown_complete_ = shutdown.QuitClosure();
}
void AXEventRecorderWinUia::Thread::ThreadMain() {
// UIA calls must be made on an MTA thread to prevent random timeouts.
base::win::ScopedCOMInitializer com_init{
base::win::ScopedCOMInitializer::kMTA};
// Create an instance of the CUIAutomation class.
CoCreateInstance(CLSID_CUIAutomation, NULL, CLSCTX_INPROC_SERVER,
IID_IUIAutomation, &uia_);
CHECK(uia_.Get());
// Register the custom event to mark the end of the test.
shutdown_sentinel_ = UiaRegistrarWin::GetInstance().GetTestCompleteEventId();
// Find the IUIAutomationElement for the root content window
uia_->ElementFromHandle(hwnd_, &root_);
CHECK(root_.Get());
// Create the event handler
win::CreateATLModuleIfNeeded();
CHECK(
SUCCEEDED(CComObject<EventHandler>::CreateInstance(&uia_event_handler_)));
uia_event_handler_->AddRef();
uia_event_handler_->Init(this, root_);
// Create a cache request to avoid cross-thread issues when logging.
CHECK(SUCCEEDED(uia_->CreateCacheRequest(&cache_request_)));
CHECK(cache_request_.Get());
CHECK(SUCCEEDED(cache_request_->AddProperty(UIA_NamePropertyId)));
CHECK(SUCCEEDED(cache_request_->AddProperty(UIA_AriaRolePropertyId)));
// Subscribe to the shutdown sentinel event
uia_->AddAutomationEventHandler(
shutdown_sentinel_, root_.Get(), TreeScope::TreeScope_Subtree,
cache_request_.Get(), uia_event_handler_.Get());
// Subscribe to focus events
uia_->AddFocusChangedEventHandler(cache_request_.Get(),
uia_event_handler_.Get());
// Subscribe to all property-change events
static const PROPERTYID kMinProp = UIA_RuntimeIdPropertyId;
static const PROPERTYID kMaxProp = UIA_HeadingLevelPropertyId;
std::array<PROPERTYID, (kMaxProp - kMinProp) + 1> property_list;
std::iota(property_list.begin(), property_list.end(), kMinProp);
uia_->AddPropertyChangedEventHandlerNativeArray(
root_.Get(), TreeScope::TreeScope_Subtree, cache_request_.Get(),
uia_event_handler_.Get(), &property_list[0], property_list.size());
// Subscribe to all structure-change events
uia_->AddStructureChangedEventHandler(root_.Get(), TreeScope_Subtree,
cache_request_.Get(),
uia_event_handler_.Get());
// Subscribe to all automation events (except structure-change events and
// live-region events, which are handled elsewhere).
static const EVENTID kMinEvent = UIA_ToolTipOpenedEventId;
static const EVENTID kMaxEvent = UIA_ActiveTextPositionChangedEventId;
for (EVENTID event_id = kMinEvent; event_id <= kMaxEvent; ++event_id) {
if (event_id != UIA_StructureChangedEventId &&
event_id != UIA_LiveRegionChangedEventId) {
uia_->AddAutomationEventHandler(
event_id, root_.Get(), TreeScope::TreeScope_Subtree,
cache_request_.Get(), uia_event_handler_.Get());
}
}
// Subscribe to live-region change events. This must be the last event we
// subscribe to, because |AXFragmentRootWin| will fire events when advised of
// the subscription, and this can hang the test-process (on Windows 19H1+) if
// we're simultaneously trying to subscribe to other events.
uia_->AddAutomationEventHandler(
UIA_LiveRegionChangedEventId, root_.Get(), TreeScope::TreeScope_Subtree,
cache_request_.Get(), uia_event_handler_.Get());
// Signal that initialization is complete; this will wake the main thread to
// start executing the test.
std::move(initialization_complete_).Run();
// Wait for shutdown signal
shutdown_signal_.Wait();
// Cleanup
uia_->RemoveAllEventHandlers();
uia_event_handler_->CleanUp();
uia_event_handler_.Reset();
cache_request_.Reset();
root_.Reset();
uia_.Reset();
// Signal thread shutdown complete; this will wake the main thread to compile
// test results and compare against the expected results.
std::move(shutdown_complete_).Run();
}
void AXEventRecorderWinUia::Thread::SendShutdownSignal() {
shutdown_signal_.Signal();
}
void AXEventRecorderWinUia::Thread::OnEvent(const std::string& event) {
// Pass the event to the thread-safe owner_.
owner_->OnEvent(event);
}
AXEventRecorderWinUia::Thread::EventHandler::EventHandler() {
// Some events are duplicated between UIAutomationCore.dll and RPCRT4.dll.
// Before WIN10_19H1, events are mainly sent from RPCRT4.dll, with a few
// duplicates sent from UIAutomationCore.dll.
// After WIN10_19H1, events are mainly sent from UIAutomationCore.dll, with a
// few duplicates sent from RPCRT4.dll.
allowed_module_address_range_ = GetModuleAddressRange(
(base::win::GetVersion() >= base::win::Version::WIN10_19H1)
? L"UIAutomationCore.dll"
: L"RPCRT4.dll");
}
AXEventRecorderWinUia::Thread::EventHandler::~EventHandler() {}
void AXEventRecorderWinUia::Thread::EventHandler::Init(
AXEventRecorderWinUia::Thread* owner,
Microsoft::WRL::ComPtr<IUIAutomationElement> root) {
owner_ = owner;
root_ = root;
}
void AXEventRecorderWinUia::Thread::EventHandler::CleanUp() {
owner_ = nullptr;
root_.Reset();
}
IFACEMETHODIMP
AXEventRecorderWinUia::Thread::EventHandler::HandleFocusChangedEvent(
IUIAutomationElement* sender) {
if (!owner_ || !IsCallerFromAllowedModule(RETURN_ADDRESS()))
return S_OK;
base::win::ScopedSafearray id;
sender->GetRuntimeId(id.Receive());
base::win::ScopedVariant id_variant(id.Release());
Microsoft::WRL::ComPtr<IUIAutomationElement> element_found;
Microsoft::WRL::ComPtr<IUIAutomationCondition> condition;
owner_->uia_->CreatePropertyCondition(UIA_RuntimeIdPropertyId, id_variant,
&condition);
CHECK(condition);
root_->FindFirst(TreeScope::TreeScope_Subtree, condition.Get(),
&element_found);
if (!element_found) {
VLOG(1) << "Ignoring UIA focus event outside our frame";
return S_OK;
}
// Transfer ownership of the RuntimeId SAFEARRAY back into |id| then debounce
// focus events that are consecutively received for the same |sender|. This
// needs to happen after determining the |sender| is within the |root_| frame,
// otherwise a RuntimeId outside the frame may be cached. For example, when
// receiving a global focus event not related to the |root_| frame.
{
VARIANT tmp = id_variant.Release();
id.Reset(V_ARRAY(&tmp));
}
if (auto lock_scope = id.CreateLockScope<VT_I4>()) {
// Debounce focus events received from the same |sender|.
if (base::ranges::equal(*lock_scope, last_focused_runtime_id_)) {
return S_OK;
}
last_focused_runtime_id_ = {lock_scope->begin(), lock_scope->end()};
}
std::string log = base::StringPrintf("AutomationFocusChanged %s",
GetSenderInfo(sender).c_str());
owner_->OnEvent(log);
return S_OK;
}
IFACEMETHODIMP
AXEventRecorderWinUia::Thread::EventHandler::HandlePropertyChangedEvent(
IUIAutomationElement* sender,
PROPERTYID property_id,
VARIANT new_value) {
if (!owner_ || !IsCallerFromAllowedModule(RETURN_ADDRESS()))
return S_OK;
std::string prop_str = UiaIdentifierToStringPretty(property_id);
if (prop_str.empty()) {
VLOG(1) << "Ignoring UIA property-changed event " << property_id;
return S_OK;
}
std::string log = base::StringPrintf("%s changed %s", prop_str.c_str(),
GetSenderInfo(sender).c_str());
owner_->OnEvent(log);
return S_OK;
}
IFACEMETHODIMP
AXEventRecorderWinUia::Thread::EventHandler::HandleStructureChangedEvent(
IUIAutomationElement* sender,
StructureChangeType change_type,
SAFEARRAY* runtime_id) {
if (!owner_ || !IsCallerFromAllowedModule(RETURN_ADDRESS()))
return S_OK;
std::string type_str;
switch (change_type) {
case StructureChangeType_ChildAdded:
type_str = "ChildAdded";
break;
case StructureChangeType_ChildRemoved:
type_str = "ChildRemoved";
break;
case StructureChangeType_ChildrenInvalidated:
type_str = "ChildrenInvalidated";
break;
case StructureChangeType_ChildrenBulkAdded:
type_str = "ChildrenBulkAdded";
break;
case StructureChangeType_ChildrenBulkRemoved:
type_str = "ChildrenBulkRemoved";
break;
case StructureChangeType_ChildrenReordered:
type_str = "ChildrenReordered";
break;
}
std::string log =
base::StringPrintf("StructureChanged/%s %s", type_str.c_str(),
GetSenderInfo(sender).c_str());
owner_->OnEvent(log);
return S_OK;
}
IFACEMETHODIMP
AXEventRecorderWinUia::Thread::EventHandler::HandleAutomationEvent(
IUIAutomationElement* sender,
EVENTID event_id) {
if (!owner_ || !IsCallerFromAllowedModule(RETURN_ADDRESS()))
return S_OK;
if (event_id == owner_->shutdown_sentinel_) {
// This is a sentinel value that tells us the tests are finished.
owner_->SendShutdownSignal();
} else {
std::string event_str = UiaIdentifierToStringPretty(event_id);
if (event_str.empty()) {
VLOG(1) << "Ignoring UIA automation event " << event_id;
return S_OK;
}
// Remove duplicate menuclosed events with no event data.
// The "duplicates" are benign. UIA currently duplicates *all* events for
// in-process listeners, and the event-recorder tries to eliminate the
// duplicates... but since the recorder sometimes isn't able to retrieve
// the role, the duplicate-elimination logic doesn't see them as
// duplicates in this case.
std::string sender_info =
event_id == UIA_MenuClosedEventId ? "" : GetSenderInfo(sender);
std::string log =
base::StringPrintf("%s %s", event_str.c_str(), sender_info.c_str());
owner_->OnEvent(log);
}
return S_OK;
}
// Due to a bug in Windows (fixed in Windows 10 19H1, but found broken in 20H2),
// events are raised exactly twice for any in-proc off-thread event listeners.
// To avoid this, in UIA API methods we can pass the RETURN_ADDRESS() to this
// method to determine whether the caller belongs to a specific platform module.
bool AXEventRecorderWinUia::Thread::EventHandler::IsCallerFromAllowedModule(
void* return_address) {
const auto address = reinterpret_cast<uintptr_t>(return_address);
return address >= allowed_module_address_range_.first &&
address < allowed_module_address_range_.second;
}
std::string AXEventRecorderWinUia::Thread::EventHandler::GetSenderInfo(
IUIAutomationElement* sender) {
std::string sender_info;
auto append_property = [&](const char* name, auto getter) {
base::win::ScopedBstr bstr;
(sender->*getter)(bstr.Receive());
if (bstr.Length() > 0) {
sender_info +=
base::StringPrintf("%s%s=%s", sender_info.empty() ? "" : ", ", name,
BstrToUTF8(bstr.Get()).c_str());
}
};
append_property("role", &IUIAutomationElement::get_CachedAriaRole);
append_property("name", &IUIAutomationElement::get_CachedName);
if (!sender_info.empty())
sender_info = "on " + sender_info;
return sender_info;
}
} // namespace ui