Repository: nvaccess/nvda

Issue:
Python 3 versions of NVDA: NVDA crashes with recursion error when accessing childCount in initOverlayClass in some cases
This has been originally noticed because [Columns Review](https://github.com/ABuffEr/columnsReview) by @ABuffer  was causing crashes for users.

### Steps to reproduce:
1. Place [attached global plugin](https://github.com/nvaccess/nvda/files/5006814/testing.py.txt) into globalPlugins in the scratchpad removing the .txt extension.

There are two scenarios in which crashes occurs:
#### Scenario 1 in Windows 10 task manager:
- Ensure that view  in the task manager is set to "more details"
- Open task manager with ctrl + shift + escape and try to navigate in the list of processes.
#### Scenario 2 In Windows 7 list of networks:
- Ensure that you have more than one network available e.g VPN tunnel in addition to your main connection etc.
- Move to the system tray with Win + b
- Navigate to the list of networks, open it with space and try to navigate between available connections with arrow keys.


### Actual behavior:
In both scenarios NVDA crashes.
Log from the first scenario:

ERROR - NVDAObjects.__call__ (13:29:26.288) - Dummy-2 (8520):
Exception in chooseNVDAObjectOverlayClasses for <'taskmgr' (appName 'taskmgr', process ID 2972) at address 56772d0>
Traceback (most recent call last):
  File "NVDAObjects\__init__.pyc", line 95, in __call__
  File "appModules\taskmgr.pyc", line 31, in chooseNVDAObjectOverlayClasses
  File "baseObject.pyc", line 41, in __get__
  File "baseObject.pyc", line 144, in _getPropertyViaCache
  File "NVDAObjects\UIA\__init__.pyc", line 1304, in _get_presentationType
  File "baseObject.pyc", line 41, in __get__
  File "baseObject.pyc", line 144, in _getPropertyViaCache
  File "NVDAObjects\__init__.pyc", line 744, in _get_presentationType
  File "baseObject.pyc", line 41, in __get__
  File "baseObject.pyc", line 144, in _getPropertyViaCache
  File "NVDAObjects\UIA\__init__.pyc", line 1238, in _get_states
  File "NVDAObjects\UIA\__init__.pyc", line 835, in _prefetchUIACacheForPropertyIDs
  File "comtypesMonkeyPatches.pyc", line 26, in __call__
ctypes.ArgumentError: argument 1: <class 'RecursionError'>: maximum recursion depth exceeded in __instancecheck__


Log from the second scenario:

ERROR - NVDAObjects.__call__ (13:46:31.275) - Dummy-3 (5556):
Exception in event_NVDAObject_init for <'explorer' (appName 'explorer', process ID 3752) at address 45ecc30>
Traceback (most recent call last):
  File "NVDAObjects\__init__.pyc", line 153, in __call__
  File "appModules\explorer.pyc", line 368, in event_NVDAObject_init
  File "baseObject.pyc", line 41, in __get__
  File "baseObject.pyc", line 144, in _getPropertyViaCache
  File "NVDAObjects\UIA\__init__.pyc", line 1331, in _get_parent
  File "NVDAObjects\__init__.pyc", line 86, in __call__
  File "NVDAObjects\UIA\__init__.pyc", line 852, in findOverlayClasses
  File "baseObject.pyc", line 41, in __get__
  File "baseObject.pyc", line 144, in _getPropertyViaCache
  File "NVDAObjects\UIA\__init__.pyc", line 1115, in _get_TextInfo
  File "baseObject.pyc", line 41, in __get__
  File "baseObject.pyc", line 144, in _getPropertyViaCache
  File "NVDAObjects\UIA\__init__.pyc", line 1100, in _get_UIATextPattern
  File "NVDAObjects\UIA\__init__.pyc", line 1079, in _getUIAPattern
  File "comtypesMonkeyPatches.pyc", line 26, in __call__
RecursionError: maximum recursion depth exceeded while calling a Python object






### Expected behavior:
No crash.
### System configuration
#### NVDA installed/portable/running from source:
tested with both installed and portable - no difference
#### NVDA version:
Crashes occurs with every version since Python 3 migration - first noticed with 2019.3 and still reproducible with 2022 and latest Alpha.
#### Windows version:
Windows 10 2004 for the first scenario Windows 7 X64 for the second.
#### Name and version of other software in use when reproducing the issue:
None
#### Other information about your system:
If the line `c = self.childCount` in the attached globalPlugin is commented out crash does not occur.
### 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.
With 2019.2.1.  and earlier there is no crash with the same plugin so this is a regression.
#### If addons are disabled, is your problem still occuring?
No but this is expected -  the report is about NVDA interaction with the globalPlugin.
#### Did you try to run the COM registry fixing tool in NVDA menu / tools?
Yes - no difference


Discussion:
cc @michaelDCurran @feerrenrut 
Not too sure why this changed in Python3, or exact steps happening here, but:
UIA NVDAObject has no actual implementation of the childCount property, thus it uses the base NvDAObject's one which just returns ```len(self.children)```.
This means that all of the NVDAObject's children are instantiated in order to calculate the child count. One or more of these children NVDAObjects may perhaps call parent at some point during their instantiation and therefore the GlobalPlugin's initOverlayClass of sampleCls may run recursively.
I would suggest fixing this by:
* Moving the majority of UIA NVDAObject's children property into a new UIAChildren property. this property would return the array of cached UIAElement children. 
* both UIA NVDAObject's children property, and a new implementation for childCount, would make use of the UIAChildren property. This avoids childCount instantiating any NVDAObjects. This should hopefully address the issue, and most likely provide some performance improvement. 
I don't have time to work on this right now, but it is a fairly simple change if someone wants to submit a pr for it.

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
@@ -1404,23 +1404,39 @@ def _get_lastChild(self):
 			return None
 		return self.correctAPIForRelation(UIA(UIAElement=lastChildElement))
 
-	def _get_children(self):
-		childrenCacheRequest=UIAHandler.handler.baseCacheRequest.clone()
-		childrenCacheRequest.TreeScope=UIAHandler.TreeScope_Children
+	def _get_UIAChildren(self):
+		childrenCacheRequest = UIAHandler.handler.baseCacheRequest.clone()
+		childrenCacheRequest.TreeScope = UIAHandler.TreeScope_Children
 		try:
-			cachedChildren=self.UIAElement.buildUpdatedCache(childrenCacheRequest).getCachedChildren()
+			return self.UIAElement.buildUpdatedCache(childrenCacheRequest).getCachedChildren()
 		except COMError as e:
 			log.debugWarning("Could not fetch cached children from UIA element: %s"%e)
-			return super(UIA,self).children
-		children=[]
-		if not cachedChildren:
-			# GetCachedChildren returns null if there are no children.
+			raise e
+
+	def _get_children(self):
+		try:
+			cachedChildren = self.UIAChildren
+			children = []
+			if not cachedChildren:
+				# GetCachedChildren returns null if there are no children.
+				return children
+			for index in range(cachedChildren.length):
+				e = cachedChildren.getElement(index)
+				windowHandle = self.windowHandle
+				children.append(self.correctAPIForRelation(UIA(windowHandle=windowHandle, UIAElement=e)))
 			return children
-		for index in range(cachedChildren.length):
-			e=cachedChildren.getElement(index)
-			windowHandle=self.windowHandle
-			children.append(self.correctAPIForRelation(UIA(windowHandle=windowHandle,UIAElement=e)))
-		return children
+		except COMError:
+			return super().children
+
+	def _get_childCount(self):
+		try:
+			cachedChildren = self.UIAChildren
+			if not cachedChildren:
+				# GetCachedChildren returns null if there are no children.
+				return 0
+			return cachedChildren.length
+		except COMError:
+			return len(super().children)
 
 	def _get_rowNumber(self):
 		val=self._getUIACacheablePropertyValue(UIAHandler.UIA_GridItemRowPropertyId,True)


Test patch: