Repository: nvaccess/nvda

Issue:
UIA objects/find overlay classes: COM error thrown when automation ID cannot be obtained as UI Automation Core cannot invoke event subscribers
Hi,

Discovered while debugging modern keyboard support in 20H2:

### Steps to reproduce:
With NVDA set to debug logging, do:

1. Open emoji panel (Windows+period).
2. Press Tab to move to emoji group.
3. Press right arrow a couple times to move to a different group and press Enter to select it.

### Actual behavior:
The following traceback pair is recorded:

DEBUGWARNING - _UIAHandler.UIAHandler.IUIAutomationEventHandler_HandleAutomationEvent (12:48:58.162) - Dummy-3 (11912):
HandleAutomationEvent: Exception while creating object for event stateChange
Traceback (most recent call last):
  File "_UIAHandler.pyc", line 464, in IUIAutomationEventHandler_HandleAutomationEvent
  File "NVDAObjects\__init__.pyc", line 86, in __call__
  File "NVDAObjects\UIA\__init__.pyc", line 890, in findOverlayClasses
  File "comtypes\__init__.pyc", line 279, in __getattr__
  File "comtypesMonkeyPatches.pyc", line 26, in __call__
_ctypes.COMError: (-2147220991, 'An event was unable to invoke any of the subscribers', (None, None, None, 0, None))
DEBUGWARNING - _UIAHandler.UIAHandler.IUIAutomationEventHandler_HandleAutomationEvent (12:48:58.166) - Dummy-3 (11912):
HandleAutomationEvent: Exception while creating object for event UIA_elementSelected
Traceback (most recent call last):
  File "_UIAHandler.pyc", line 464, in IUIAutomationEventHandler_HandleAutomationEvent
  File "NVDAObjects\__init__.pyc", line 86, in __call__
  File "NVDAObjects\UIA\__init__.pyc", line 890, in findOverlayClasses
  File "comtypes\__init__.pyc", line 279, in __getattr__
  File "comtypesMonkeyPatches.pyc", line 26, in __call__
_ctypes.COMError: (-2147220991, 'An event was unable to invoke any of the subscribers', (None, None, None, 0, None))

### Expected behavior:
UIA objects are constructed so that events can be handled.

### System configuration
#### NVDA installed/portable/running from source:
Installed

#### NVDA version:
alpha-20656,87eee7f4

#### Windows version:
Windows 10 Version 20H2 beta (build 19042.423)

#### Name and version of other software in use when reproducing the issue:
None

#### Other information about your system:
Used to develop add-ons such as Windows 10 App Essentials

### Other questions
#### Does the issue still occur after restarting your computer?
Yes

#### Have you tried any other versions of NVDA? If so, please report their behaviors.
Yes - occurs in older versions such as 2020.1.

#### If addons are disabled, is your problem still occurring?
Yes

#### Did you try to run the COM registry fixing tool in NVDA menu / tools?
Not applicable

### Cause:
The tracebacks above are seen when an exception occurs when trying to obtain automation ID. The part where UIA object's findOverlayClasses is invoked comes from DevExpress fix (issue #10918). A related issue exists when NVDA fails to locate UIA search fieldds.

### Possible solutions:
1. Let find overlay classes method raise an exception and return early if an exception occurs while trying to obtain automation ID. This also allows a local variable to record automation ID, saving NVDA from having to hunt this through attribute access throughout the method.
2. Assign a placeholder when an exception occurs when obtaining automation ID. Because automation ID is a string, it should be assigned to a string value (say, an empty string). At the same time, log the fact that automation ID could not be located.
3. Wrap individual overlay class conditions involving automation ID inside a try statement. This is perhaps the least disruptive solution, although this will mean future pull requests involving UIA object's find overlay classes method must be careful to do this whenever automation ID is consulted.
4. Unless the specs say otherwise, from UIA handler thread's event handlers, do not instantiate UIA objects if an exception occurs when fetching automation ID. This is perhaps the most optimized solution, as it saves NVDA from going through routines to instantiate overlay classes just to handle an event.
5. Find a way to talk to UIAutomationCore.dll or the UIA element in question to determine conditions where fetching automation ID's might produce exceptions. This will involve looking at apps where automation ID fetch exception is pronounced (such as modern keyboard) so a workaround can be developed (including the strategies listed above).
6. Add a dedicated automation ID property to UIA objects (idea came from @LeonardDeR). This allows errors to be logged if any ans simplifies many code paths where automation ID check is involved (such as find overlay classes, add-ons, and UIA objects scattered throughout NVDA).

Thanks.


Discussion:
We could also put automationId into a property on the object, making that call catch the COMError. This can be done in a similar way as how `_get_UIAFullDescription` is implemented.

Resolving patch:
diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py
--- a/source/NVDAObjects/UIA/__init__.py
+++ b/source/NVDAObjects/UIA/__init__.py
@@ -842,6 +842,8 @@ def _prefetchUIACacheForPropertyIDs(self,IDs):
 	def findOverlayClasses(self,clsList):
 		UIAControlType=self.UIAElement.cachedControlType
 		UIAClassName=self.UIAElement.cachedClassName
+		# #11445: to avoid COM errors, do not fetch cached UIA Automation Id from the underlying element.
+		UIAAutomationId = self.UIAAutomationId
 		if UIAClassName=="NetUITWMenuItem" and UIAControlType==UIAHandler.UIA_MenuItemControlTypeId and not self.name and not self.previous:
 			# Bounces focus from a netUI dead placeholder menu item when no item is selected up to the menu itself.
 			clsList.append(PlaceholderNetUITWMenuItem)
@@ -849,7 +851,11 @@ def findOverlayClasses(self,clsList):
 			clsList.append(WpfTextView)
 		elif UIAClassName=="NetUIDropdownAnchor":
 			clsList.append(NetUIDropdownAnchor)
-		elif self.TextInfo==UIATextInfo and (UIAClassName=='_WwG' or self.windowClassName=='_WwG' or self.UIAElement.cachedAutomationID.startswith('UIA_AutomationId_Word_Content')):
+		elif self.TextInfo == UIATextInfo and (
+			UIAClassName == '_WwG'
+			or self.windowClassName == '_WwG'
+			or UIAAutomationId.startswith('UIA_AutomationId_Word_Content')
+		):
 			from .wordDocument import WordDocument, WordDocumentNode
 			if self.role==controlTypes.ROLE_DOCUMENT:
 				clsList.append(WordDocument)
@@ -858,7 +864,11 @@ def findOverlayClasses(self,clsList):
 		# #5136: Windows 8.x and Windows 10 uses different window class and other attributes for toast notifications.
 		elif UIAClassName=="ToastContentHost" and UIAControlType==UIAHandler.UIA_ToolTipControlTypeId: #Windows 8.x
 			clsList.append(Toast_win8)
-		elif self.windowClassName=="Windows.UI.Core.CoreWindow" and UIAControlType==UIAHandler.UIA_WindowControlTypeId and "ToastView" in self.UIAElement.cachedAutomationId: # Windows 10
+		elif (
+			self.windowClassName == "Windows.UI.Core.CoreWindow"
+			and UIAControlType == UIAHandler.UIA_WindowControlTypeId
+			and "ToastView" in UIAAutomationId
+		):  # Windows 10
 			clsList.append(Toast_win10)
 		# #8118: treat UIA tool tips (including those found in UWP apps) as proper tool tips, especially those found in Microsoft Edge and other apps.
 		# Windows 8.x toast, although a form of tool tip, is covered separately.
@@ -882,12 +892,12 @@ def findOverlayClasses(self,clsList):
 				clsList.append(edge.EdgeList)
 			else:
 				clsList.append(edge.EdgeNode)
-		elif self.role==controlTypes.ROLE_DOCUMENT and self.UIAElement.cachedAutomationId=="Microsoft.Windows.PDF.DocumentView":
+		elif self.role == controlTypes.ROLE_DOCUMENT and UIAAutomationId == "Microsoft.Windows.PDF.DocumentView":
 			# PDFs
 			from . import edge
 			clsList.append(edge.EdgeHTMLRoot)
 		elif (
-			self.UIAElement.cachedAutomationId == "RichEditControl"
+			UIAAutomationId == "RichEditControl"
 			and "DevExpress.XtraRichEdit" in self.UIAElement.cachedProviderDescription
 		):
 			clsList.insert(0, DevExpressXtraRichEdit)
@@ -928,7 +938,7 @@ def findOverlayClasses(self,clsList):
 			clsList.append(Dialog)
 		# #6241: Try detecting all possible suggestions containers and search fields scattered throughout Windows 10.
 		try:
-			if self.UIAElement.cachedAutomationID in ("SearchTextBox", "TextBox"):
+			if UIAAutomationId in ("SearchTextBox", "TextBox"):
 				clsList.append(SearchField)
 		except COMError:
 			log.debug("Failed to locate UIA search field", exc_info=True)
@@ -1125,6 +1135,7 @@ def setFocus(self):
 	def _get_devInfo(self):
 		info=super(UIA,self).devInfo
 		info.append("UIAElement: %r"%self.UIAElement)
+		# #11445: allow exceptions to be recorded when presenting Automation Id.
 		try:
 			ret=self.UIAElement.currentAutomationID
 		except Exception as e:
@@ -1167,6 +1178,13 @@ def _get_devInfo(self):
 		info.append("UIA patterns available: %s"%", ".join(patternsAvailable))
 		return info
 
+	def _get_UIAAutomationId(self):
+		try:
+			return self._getUIACacheablePropertyValue(UIAHandler.UIA_AutomationIdPropertyId)
+		except COMError:
+			# #11445: due to timing errors, elements will be instantiated with no automation Id present.
+			return ""
+
 	def _get_name(self):
 		try:
 			return self._getUIACacheablePropertyValue(UIAHandler.UIA_NamePropertyId)


Test patch: