Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid UIA event flooding in NvDA by moving UIA event handler COM objects into c++ #14888

Merged
merged 32 commits into from Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
33ce9fb
UIAHandler propertyChange event: avoid needlessly creating an NVDAObj…
michaelDCurran Oct 17, 2022
d209f17
UIAHandler automation event: don't needlessly create an NVDAObject wh…
michaelDCurran Oct 17, 2022
de5332f
UIA event handlers: pass windowHandle into NvDAObject constructor if …
michaelDCurran Oct 17, 2022
9b00020
Implement a new UI Automation event handler COM object in c++ which a…
michaelDCurran Apr 23, 2023
3605524
Add lisence and fix linting
michaelDCurran May 1, 2023
d191e72
UIAEventLimiter: improve EventRecordContaint c++20 concept so as to n…
michaelDCurran May 1, 2023
9cd6cb7
Merge branch 'master' into UIAEventLimiting
michaelDCurran May 2, 2023
7f1028c
UIA event limiter: no longer coalesce propertyChange events, as it is…
michaelDCurran May 2, 2023
2e26800
UIA event limiter: ensure not to flood NVDA with needless flush messa…
michaelDCurran May 2, 2023
c1a436e
UIAHandler: execute the UIA event limiter flush on NVDA's UIAHandler…
michaelDCurran May 2, 2023
b94f4a1
UIA event limiter:
michaelDCurran May 8, 2023
a532775
UIA event limiter: remove no longer existing flush function from nvda…
michaelDCurran May 8, 2023
25ed7e7
UIAHandler: Ensure that RateLimitedEventHandler reference count even…
michaelDCurran May 19, 2023
ca61680
Merge branch 'master' into UIAEventLimiting
michaelDCurran Jun 11, 2023
9ba2827
For XAML UIA elements: Don't use caret event detection to wait for t…
michaelDCurran Jun 12, 2023
3fac419
Linting
michaelDCurran Jun 12, 2023
63da590
add a return type
michaelDCurran Jun 12, 2023
b4d9f17
Core's message window no longer needs to be exposed from core.
michaelDCurran Jun 12, 2023
8a2f16f
Apply suggestions from code review
michaelDCurran Jun 21, 2023
8200c7d
Apply suggestions from code review
michaelDCurran Jun 21, 2023
18d9182
Merge branch 'master' into UIAEventLimiting
michaelDCurran Jun 21, 2023
1f29c6d
Add typing
michaelDCurran Jun 22, 2023
48913a9
Workaround for caret detection when backspacing in XAML text fields (…
LeonarddeR Jul 9, 2023
1f5f569
Merge branch 'master' into UIAEventLimiting
michaelDCurran Oct 22, 2023
009526a
Merge branch 'master' into UIAEventLimiting
michaelDCurran Nov 22, 2023
3e6ab81
Update what's new
michaelDCurran Nov 22, 2023
114c2c4
Add type info
michaelDCurran Nov 22, 2023
4ce9806
Update user_docs/en/changes.t2t
michaelDCurran Nov 22, 2023
0041b1d
Address review action
michaelDCurran Nov 22, 2023
6bd41fc
Linting.
michaelDCurran Nov 22, 2023
9283a99
Add feature flag (#15814)
LeonarddeR Nov 22, 2023
ae6abd0
Merge branch 'master' into UIAEventLimiting
michaelDCurran Nov 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 34 additions & 0 deletions nvdaHelper/local/UIAEventLimiter/api.cpp
@@ -0,0 +1,34 @@
/*
This file is a part of the NVDA project.
URL: http://github.com/nvaccess/nvda/
Copyright 2023 NV Access Limited.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2.0, as published by
the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
This license can be found at:
http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*/

#include <windows.h>
#include <common/log.h>
#include "rateLimitedEventHandler.h"

HRESULT rateLimitedUIAEventHandler_create(IUnknown* pExistingHandler, RateLimitedEventHandler** ppRateLimitedEventHandler) {
LOG_DEBUG(L"rateLimitedUIAEventHandler_create called");
if(!pExistingHandler || !ppRateLimitedEventHandler) {
LOG_ERROR(L"rateLimitedUIAEventHandler_create: one or more NULL arguments");
return E_INVALIDARG;
}

// Create the RateLimitedEventHandler instance
*ppRateLimitedEventHandler = new RateLimitedEventHandler(pExistingHandler);
if (!(*ppRateLimitedEventHandler)) {
LOG_ERROR(L"rateLimitedUIAEventHandler_create: Could not create RateLimitedUIAEventHandler. Returning");
return E_OUTOFMEMORY;
}
LOG_DEBUG(L"rateLimitedUIAEventHandler_create: done");
return S_OK;
}
107 changes: 107 additions & 0 deletions nvdaHelper/local/UIAEventLimiter/eventRecord.h
@@ -0,0 +1,107 @@
/*
This file is a part of the NVDA project.
URL: http://github.com/nvaccess/nvda/
Copyright 2023 NV Access Limited.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2.0, as published by
the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
This license can be found at:
http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*/

#pragma once

#include <vector>
#include <variant>
#include <atlcomcli.h>
#include <UIAutomation.h>
#include "utils.h"

// The following structs are used to represent UI Automation event params.
// A part from holding the params,
// each struct also contains a method for generating a comparison key
// which is used to detect and remove duplicate events.
// The key is made up of the element's runtime ID,
// plus any extra event params that make the vent unique,
michaelDCurran marked this conversation as resolved.
Show resolved Hide resolved
// E.g. event ID, property ID etc.

struct AutomationEventRecord_t {
CComPtr<IUIAutomationElement> sender;
EVENTID eventID;
std::vector<int> generateCoalescingKey() const {
auto key = getRuntimeIDFromElement(sender);
key.push_back(eventID);
return key;
}
};

struct PropertyChangedEventRecord_t {
CComPtr<IUIAutomationElement> sender;
PROPERTYID propertyID;
CComVariant newValue;
std::vector<int> generateCoalescingKey() const {
auto key = getRuntimeIDFromElement(sender);
key.push_back(UIA_AutomationPropertyChangedEventId);
key.push_back(propertyID);
return key;
}
};

struct FocusChangedEventRecord_t {
CComPtr<IUIAutomationElement> sender;
std::vector<int> generateCoalescingKey() const {
auto key = getRuntimeIDFromElement(sender);
key.push_back(UIA_AutomationFocusChangedEventId);
return key;
}
};

struct NotificationEventRecord_t {
CComPtr<IUIAutomationElement> sender;
NotificationKind notificationKind;
NotificationProcessing notificationProcessing;
CComBSTR displayString;
CComBSTR activityID;
std::vector<int> generateCoalescingKey() const {
auto key = getRuntimeIDFromElement(sender);
key.push_back(UIA_NotificationEventId);
key.push_back(notificationKind);
key.push_back(notificationProcessing);
// Include the activity ID in the key also,
// by converting it to a sequence of ints.
if(activityID.m_str) {
for(int c: std::wstring_view(activityID.m_str)) {
key.push_back(c);
}
} else {
key.push_back(0);
}
return key;
}
};

struct ActiveTextPositionChangedEventRecord_t {
CComPtr<IUIAutomationElement> sender;
CComPtr<IUIAutomationTextRange> range;
std::vector<int> generateCoalescingKey() const {
auto key = getRuntimeIDFromElement(sender);
key.push_back(UIA_ActiveTextPositionChangedEventId);
return key;
}
};

// @brief a variant type that holds all possible UI Automation event records we support.
using EventRecordVariant_t = std::variant<AutomationEventRecord_t, FocusChangedEventRecord_t, PropertyChangedEventRecord_t, NotificationEventRecord_t, ActiveTextPositionChangedEventRecord_t>;

// @brief A concept to be used with the above event record types
// that ensures the type has a generateCoalescingKey method,
// and that the type appears in the EventRecordVariant_t type.
template<typename T>
concept EventRecordConstraints = requires(T t) {
{ t.generateCoalescingKey() } -> std::same_as<std::vector<int>>;
// The type must be supported by the EventRecordVariant_t variant type
requires supports_alternative<T, EventRecordVariant_t>;
};
243 changes: 243 additions & 0 deletions nvdaHelper/local/UIAEventLimiter/rateLimitedEventHandler.cpp
@@ -0,0 +1,243 @@
/*
This file is a part of the NVDA project.
URL: http://github.com/nvaccess/nvda/
Copyright 2023 NV Access Limited.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2.0, as published by
the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
This license can be found at:
http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*/

#include <windows.h>
#include <uiautomation.h>
#include <numeric>
#include <variant>
#include <map>
#include <vector>
#include <functional>
#include <mutex>
#include <atomic>
#include <atlcomcli.h>
#include <comutil.h>
#include <common/log.h>
#include "eventRecord.h"
#include "rateLimitedEventHandler.h"

template<EventRecordConstraints EventRecordClass, typename... EventRecordArgTypes>
HRESULT RateLimitedEventHandler::queueEvent(EventRecordArgTypes&&... args) {
LOG_DEBUG(L"RateLimitedUIAEventHandler::queueEvent called");
bool needsFlush = false;
{ std::lock_guard lock(m_mtx);
michaelDCurran marked this conversation as resolved.
Show resolved Hide resolved
// work out whether we need to request a flush after inserting this event.
needsFlush = m_eventRecords.empty();
LOG_DEBUG(L"RateLimitedUIAEventHandler::queueEvent: Inserting new event");
auto& recordVar = m_eventRecords.emplace_back(std::in_place_type_t<EventRecordClass>{}, args...);
auto recordVarIter = m_eventRecords.end();
recordVarIter--;
auto& record = std::get<EventRecordClass>(recordVar);
auto coalescingKey = record.generateCoalescingKey();
auto existingKeyIter = m_eventRecordsByKey.find(coalescingKey);
if(existingKeyIter != m_eventRecordsByKey.end()) {
LOG_DEBUG(L"RateLimitedUIAEventHandler::queueEvent: found existing event with same key");
auto& [existingRecordVarIter,existingCoalesceCount] = existingKeyIter->second;
LOG_DEBUG(L"RateLimitedUIAEventHandler::queueEvent: updating key and count to "<<(existingCoalesceCount+1));
existingKeyIter->second = {recordVarIter, existingCoalesceCount + 1};
LOG_DEBUG(L"RateLimitedUIAEventHandler::queueEvent: erasing old item");
m_eventRecords.erase(existingRecordVarIter);
} else {
LOG_DEBUG(L"RateLimitedUIAEventHandler::queueEvent: Adding key");
m_eventRecordsByKey.insert_or_assign(coalescingKey, std::pair(recordVarIter, 1));
}
if(needsFlush) {
LOG_DEBUG(L"RateLimitedUIAEventHandler::queueEvent: requesting flush");
m_needsFlush = true;
m_flushConditionVar.notify_one();
}
} // m_mtx released.
return S_OK;
}

void RateLimitedEventHandler::flusherThreadFunc(std::stop_token stopToken) {
LOG_DEBUG(L"flusherThread started");
// If this thread is requested to stop, we need to ensure we wake up.
std::stop_callback stopCallback{stopToken, [this](){
this->m_flushConditionVar.notify_all();
}};
do { // thread main loop
LOG_DEBUG(L"flusherThreadFunc sleeping...");
{ std::unique_lock lock(m_mtx);
// sleep until a flush is needed or this thread should stop.
m_flushConditionVar.wait(lock, [this, stopToken](){
return this->m_needsFlush || stopToken.stop_requested();
});
LOG_DEBUG(L"flusherThread woke up");
if(stopToken.stop_requested()) {
LOG_DEBUG(L"flusherThread returning as stop requested");
return;
}
m_needsFlush = false;
} // m_mtx released here.
flushEvents();
} while(!stopToken.stop_requested());
LOG_DEBUG(L"flusherThread returning");
}

HRESULT RateLimitedEventHandler::emitEvent(const AutomationEventRecord_t& record) const {
LOG_DEBUG(L"RateLimitedUIAEventHandler::emitAutomationEvent called");
if(!m_pExistingAutomationEventHandler) {
LOG_ERROR(L"RateLimitedUIAEventHandler::emitAutomationEvent: interface not supported.");
return E_NOINTERFACE;
}
LOG_DEBUG(L"Emitting automationEvent for eventID "<<record.eventID);
return m_pExistingAutomationEventHandler->HandleAutomationEvent(record.sender, record.eventID);
}

HRESULT RateLimitedEventHandler::emitEvent(const FocusChangedEventRecord_t& record) const {
LOG_DEBUG(L"RateLimitedUIAEventHandler::emitFocusChangedEvent called");
if(!m_pExistingFocusChangedEventHandler) {
LOG_ERROR(L"RateLimitedUIAEventHandler::emitFocusChangedEvent: interface not supported.");
return E_NOINTERFACE;
}
LOG_DEBUG(L"Emitting focus changed event");
return m_pExistingFocusChangedEventHandler->HandleFocusChangedEvent(record.sender);
}

HRESULT RateLimitedEventHandler::emitEvent(const PropertyChangedEventRecord_t& record) const {
LOG_DEBUG(L"RateLimitedUIAEventHandler::emitPropertyChangedEvent called");
if(!m_pExistingPropertyChangedEventHandler) {
LOG_ERROR(L"RateLimitedUIAEventHandler::emitPropertyChangedEvent: interface not supported.");
return E_NOINTERFACE;
}
LOG_DEBUG(L"Emitting property changed event for property "<<(record.propertyID));
return m_pExistingPropertyChangedEventHandler->HandlePropertyChangedEvent(record.sender, record.propertyID, record.newValue);
}

HRESULT RateLimitedEventHandler::emitEvent(const NotificationEventRecord_t& record) const {
LOG_DEBUG(L"RateLimitedUIAEventHandler::emitNotificationEvent called");
if(!m_pExistingNotificationEventHandler) {
LOG_ERROR(L"RateLimitedUIAEventHandler::emitNotificationChangedEvent: interface not supported.");
return E_NOINTERFACE;
}
LOG_DEBUG(L"Emitting notification event");
return m_pExistingNotificationEventHandler->HandleNotificationEvent(record.sender, record.notificationKind, record.notificationProcessing, record.displayString, record.activityID);
}

HRESULT RateLimitedEventHandler::emitEvent(const ActiveTextPositionChangedEventRecord_t& record) const {
LOG_DEBUG(L"RateLimitedUIAEventHandler::emitActiveTextPositionChangedEvent called");
if(!m_pExistingActiveTextPositionChangedEventHandler) {
LOG_ERROR(L"RateLimitedUIAEventHandler::emitActiveTextPositionChangedEvent: interface not supported.");
return E_NOINTERFACE;
}
LOG_DEBUG(L"Emitting active text position changed event");
return m_pExistingActiveTextPositionChangedEventHandler->HandleActiveTextPositionChangedEvent(record.sender, record.range);
}

RateLimitedEventHandler::~RateLimitedEventHandler() {
LOG_DEBUG(L"RateLimitedUIAEventHandler::~RateLimitedUIAEventHandler called");
}

RateLimitedEventHandler::RateLimitedEventHandler(IUnknown* pExistingHandler):
m_pExistingAutomationEventHandler(pExistingHandler), m_pExistingFocusChangedEventHandler(pExistingHandler), m_pExistingPropertyChangedEventHandler(pExistingHandler), m_pExistingNotificationEventHandler(pExistingHandler), m_pExistingActiveTextPositionChangedEventHandler(pExistingHandler),
m_flusherThread([this](std::stop_token st){ this->flusherThreadFunc(st); })
michaelDCurran marked this conversation as resolved.
Show resolved Hide resolved
{
LOG_DEBUG(L"RateLimitedUIAEventHandler::RateLimitedUIAEventHandler called");
}

// IUnknown methods
ULONG STDMETHODCALLTYPE RateLimitedEventHandler::AddRef() {
auto refCount = InterlockedIncrement(&m_refCount);
LOG_DEBUG(L"AddRef: "<<refCount)
return refCount;
}

ULONG STDMETHODCALLTYPE RateLimitedEventHandler::Release() {
auto refCount = InterlockedDecrement(&m_refCount);
if (refCount == 0) {
delete this;
}
LOG_DEBUG(L"Release: "<<refCount)
return refCount;
}

HRESULT STDMETHODCALLTYPE RateLimitedEventHandler::QueryInterface(REFIID riid, void** ppInterface) {
if (riid == __uuidof(IUnknown)) {
*ppInterface = static_cast<IUIAutomationEventHandler*>(this);
AddRef();
return S_OK;
} else if (riid == __uuidof(IUIAutomationEventHandler)) {
*ppInterface = static_cast<IUIAutomationEventHandler*>(this);
AddRef();
return S_OK;
} else if (riid == __uuidof(IUIAutomationFocusChangedEventHandler)) {
*ppInterface = static_cast<IUIAutomationFocusChangedEventHandler*>(this);
AddRef();
return S_OK;
} else if (riid == __uuidof(IUIAutomationPropertyChangedEventHandler)) {
*ppInterface = static_cast<IUIAutomationPropertyChangedEventHandler*>(this);
AddRef();
return S_OK;
} else if (riid == __uuidof(IUIAutomationNotificationEventHandler)) {
*ppInterface = static_cast<IUIAutomationNotificationEventHandler*>(this);
AddRef();
return S_OK;
} else if (riid == __uuidof(IUIAutomationActiveTextPositionChangedEventHandler)) {
*ppInterface = static_cast<IUIAutomationActiveTextPositionChangedEventHandler*>(this);
AddRef();
return S_OK;
}
*ppInterface = nullptr;
return E_NOINTERFACE;
}

// IUIAutomationEventHandler method
HRESULT STDMETHODCALLTYPE RateLimitedEventHandler::HandleAutomationEvent(IUIAutomationElement* sender, EVENTID eventID) {
LOG_DEBUG(L"RateLimitedUIAEventHandler::HandleAutomationEvent called");
LOG_DEBUG(L"Queuing automationEvent for eventID "<<eventID);
return queueEvent<AutomationEventRecord_t>(sender, eventID);
}

// IUIAutomationFocusEventHandler method
HRESULT STDMETHODCALLTYPE RateLimitedEventHandler::HandleFocusChangedEvent(IUIAutomationElement* sender) {
LOG_DEBUG(L"RateLimitedUIAEventHandler::HandleFocusChangedEvent called");
return queueEvent<FocusChangedEventRecord_t>(sender);
}

// IUIAutomationPropertyChangedEventHandler method
HRESULT STDMETHODCALLTYPE RateLimitedEventHandler::HandlePropertyChangedEvent(IUIAutomationElement* sender, PROPERTYID propertyID, VARIANT newValue) {
LOG_DEBUG(L"RateLimitedUIAEventHandler::HandlePropertyChangedEvent called");
return queueEvent<PropertyChangedEventRecord_t>(sender, propertyID, newValue);
}

// IUIAutomationNotificationEventHandler method
HRESULT STDMETHODCALLTYPE RateLimitedEventHandler::HandleNotificationEvent(IUIAutomationElement* sender, NotificationKind notificationKind, NotificationProcessing notificationProcessing, BSTR displayString, BSTR activityID) {
seanbudd marked this conversation as resolved.
Show resolved Hide resolved
LOG_DEBUG(L"RateLimitedUIAEventHandler::HandleNotificationEvent called");
return queueEvent<NotificationEventRecord_t>(sender, notificationKind, notificationProcessing, displayString, activityID);
}

// IUIAutomationActiveTextPositionchangedEventHandler method
HRESULT STDMETHODCALLTYPE RateLimitedEventHandler::HandleActiveTextPositionChangedEvent(IUIAutomationElement* sender, IUIAutomationTextRange* range) {
LOG_DEBUG(L"RateLimitedUIAEventHandler::HandleActiveTextPositionChangedEvent called");
return queueEvent<ActiveTextPositionChangedEventRecord_t>(sender, range);
}

void RateLimitedEventHandler::flushEvents() {
LOG_DEBUG(L"RateLimitedEventHandler::flushEvents called");
decltype(m_eventRecords) eventRecordsCopy;
decltype(m_eventRecordsByKey) eventRecordsByKeyCopy;
{ std::lock_guard lock{m_mtx};
eventRecordsCopy.swap(m_eventRecords);
eventRecordsByKeyCopy.swap(m_eventRecordsByKey);
} // m_mtx released here.
// Emit events
LOG_DEBUG(L"RateLimitedUIAEventHandler::flusherThreadFunc: Emitting events...");
for(const auto& recordVar: eventRecordsCopy) {
std::visit([this](const auto& record) {
this->emitEvent(record);
}, recordVar);
}
LOG_DEBUG(L"RateLimitedUIAEventHandler::flusherThreadFunc: Done emitting events");
}