Repository: nvaccess/nvda

Issue:
Problem with gui.nvdaControls.CustomCheckListBox
### Steps to reproduce:
1. Press NVDA+control+k to open the Keyboard category of the NVDA settings dialog.
2. Press OK button to close the dialog.
3. Press NVDA+q to NVDA exit.
4. Press control+alt+n to start NVDA.

### Actual behavior:
NVDA starts for a very long time, after which everything freezes. It becomes impossible to use the system.

### Expected behavior:
NVDA should start and work normally.

### System configuration:
Windows 7 32bit SP1 with all updates.

#### NVDA Installed/portable/running from source:
Installed and portable

#### NVDA version:
alpha-16220,87eb36f8

#### Does the issue still occur after restarting your PC?
Yes, the issue is always reproduced.



Discussion:
I can reproduce it on Windows 7 Professional x64 all of the time. It is almost impossible to get a log from this crash, because the rest of the system also crashes with NVDA. I believe that it is a good candidate for a P1. @michaelDCurran @feerrenrut 
Should this one be fixed when #8006 was merged?
Nope, that pr is unrelated.

This list is now used in advanced settings and the same problem can be observed there. The only way to restore computer to its working condition after restart of NVDA is a complete reboot of the system. Can this be given attention it deserves? Solving it before 2019.1 is released would be nice.
I can not reproduce this myself, but will try to provide a fix.

Resolving patch:
diff --git a/source/gui/accPropServer.py b/source/gui/accPropServer.py
--- a/source/gui/accPropServer.py
+++ b/source/gui/accPropServer.py
@@ -9,29 +9,57 @@
 from logHandler import log
 from  comtypes.automation import VT_EMPTY
 from  comtypes import COMObject
-from comInterfaces.Accessibility import IAccPropServer
-from abc import ABCMeta, abstractmethod
+from comInterfaces.Accessibility import IAccPropServer, ANNO_CONTAINER, ANNO_THIS
+from abc import ABCMeta, abstractmethod, abstractproperty
 from six import with_metaclass
 import weakref
+import winUser
+import wx
 
 class IAccPropServer_Impl(with_metaclass(ABCMeta, COMObject)):
-	"""Base class for implementing a COM interface for AccPropServer.
-	Please override the _GetPropValue method, not GetPropValue.
-	GetPropValue wraps _getPropValue to catch and log exceptions (Which for some reason NVDA's logger misses when they occur in GetPropValue).
+	"""Base class for implementing a COM interface for a hwnd based AccPropServer\
+	to annotate a WX control.
+	The AccPropServer registers itself using the window handle of the WX control.
+	When the WX control is destroyed, the instance is automatically unregistered.
+	This should eventually be dropped in favor of WX' own annotation support,
+	blocked by wxWidgets/Phoenix#1129.
+	Please override the L{_GetPropValue} method, not L{GetPropValue}.
+	L{GetPropValue} wraps L{_getPropValue} to catch and log exceptions (Which for some reason NVDA's logger misses when they occur in GetPropValue).
+	You must also provide the L{properties} property.
 	"""
 
 	_com_interfaces_ = [
 		IAccPropServer
 	]
 
-	def __init__(self, control, *args, **kwargs):
+	def __init__(self, control, annotateChildren=True):
 		"""Initialize the instance of AccPropServer. 
 		@param control: the WX control instance, so you can look up things in the _getPropValue method.
 			It's available on self.control.
 		@Type control: Subclass of wx.Window
+		@param annotateChildren: whether the WX control is a container which children should be annotated.
+		@type annotateChildren: bool
 		"""
 		self.control = weakref.ref(control)
-		super(IAccPropServer_Impl, self).__init__(*args, **kwargs)
+		self.hwnd = control.GetHandle()
+		super(IAccPropServer_Impl, self).__init__()
+		# Import late to avoid circular import
+		from IAccessibleHandler import accPropServices
+		accPropServices.SetHwndPropServer(
+			hwnd=self.hwnd,
+			idObject=winUser.OBJID_CLIENT,
+			idChild=0,
+			paProps=self.properties,
+			cProps=len(self.properties),
+			pServer=self,
+			AnnoScope=ANNO_CONTAINER if annotateChildren else ANNO_THIS
+		)
+		# clean up of accPropServices needs to happen when the control is destroyed. We can't rely on 
+		# pythons `__del__` method to be called, and the wx framework does not call Destroy on child controls,
+		# automatically. Instead we can bind to the "window destroy" event for the control to do necessary 
+		# cleanup (wxWidgets/Phoenix/#630). Not performing this cleanup results in a reference to the parent
+		# window, keeping it from being deleted correctly. This can cause a freeze on exit of NVDA.
+		control.Bind(wx.EVT_WINDOW_DESTROY, self._onDestroyControl, source=control)
 
 	@abstractmethod
 	def _getPropValue(self, pIDString, dwIDStringLen, idProp):
@@ -44,6 +72,8 @@ def _getPropValue(self, pIDString, dwIDStringLen, idProp):
 			If the accessible element is HWND-based,
 			IAccessibleHandler.accPropServices.DecomposeHwndIdentityString can be used
 			to extract the HWND/idObject/idChild from the identity string.
+			Note that, while one IAccPropServer implementation can annotate
+			multiple accessible elements, it is still bound to one wx.Control.
 		@type pIDString: str
 		@param dwIDStringLen: Specifies the length of the identity string specified by the pIDString parameter.
 		@type dwIDStringLen: int
@@ -59,3 +89,24 @@ def GetPropValue(self, pIDString, dwIDStringLen, idProp):
 			log.exception()
 			return VT_EMPTY, 0
 
+	@abstractproperty
+	def properties(self):
+		""" Returns an array of properties that should be handled by this instance.
+		@rtype: L{comtypes.GUID}*n
+		"""
+		raise NotImplementedError
+
+	def _onDestroyControl(self, evt):
+		evt.Skip() # Allow other handlers to process this event.
+		self._cleanup()
+
+	def _cleanup(self):
+		# Import late to avoid circular import
+		from IAccessibleHandler import accPropServices
+		accPropServices.ClearHwndProps(
+			hwnd=self.hwnd,
+			idObject=winUser.OBJID_CLIENT,
+			idChild=0,
+			paProps=self.properties,
+			cProps=len(self.properties)
+		)
diff --git a/source/gui/nvdaControls.py b/source/gui/nvdaControls.py
--- a/source/gui/nvdaControls.py
+++ b/source/gui/nvdaControls.py
@@ -45,6 +45,15 @@ def OnSetFocus(self, evt):
 
 class ListCtrlAccPropServer(accPropServer.IAccPropServer_Impl):
 	"""AccPropServer for wx checkable lists which aren't fully accessible."""
+
+	#: An array with the GUIDs of the properties that an AccPropServer should override for list controls with checkboxes.
+	#: The role is supposed to be checkbox, rather than list item.
+	#: The state should be overridden to include the checkable state as well as the checked state if the item is checked.
+	properties = (comtypes.GUID * 2)(*[oleacc.PROPID_ACC_ROLE,oleacc.PROPID_ACC_STATE])
+
+	def __init__(self, control):
+		super(ListCtrlAccPropServer, self).__init__(control, annotateChildren=True)
+
 	def _getPropValue(self, pIDString, dwIDStringLen, idProp):
 		empty = (comtypes.automation.VT_EMPTY, 0)
 		control = self.control()  # self.control held as a weak ref, ensure it stays alive for the duration of this method
@@ -70,11 +79,6 @@ def _getPropValue(self, pIDString, dwIDStringLen, idProp):
 				states |= oleacc.STATE_SYSTEM_SELECTED | oleacc.STATE_SYSTEM_FOCUSED
 			return states, 1
 
-#: An array with the GUIDs of the properties that an AccPropServer should override for list controls with checkboxes.
-#: The role is supposed to be checkbox, rather than list item.
-#: The state should be overridden to include the checkable state as well as the checked state if the item is checked.
-CHECK_LIST_PROPS = (comtypes.GUID * 2)(*[oleacc.PROPID_ACC_ROLE,oleacc.PROPID_ACC_STATE])
-
 class CustomCheckListBox(wx.CheckListBox):
 	"""Custom checkable list to fix a11y bugs in the standard wx checkable list box."""
 
@@ -84,15 +88,6 @@ def __init__(self, *args, **kwargs):
 		from IAccessibleHandler import accPropServices
 		# Register object with COM to fix accessibility bugs in wx.
 		server = ListCtrlAccPropServer(self)
-		accPropServices.SetHwndPropServer(
-			hwnd=self.Handle,
-			idObject=winUser.OBJID_CLIENT,
-			idChild=0,
-			paProps=CHECK_LIST_PROPS,
-			cProps=len(CHECK_LIST_PROPS),
-			pServer=server,
-			AnnoScope=1
-		)
 		# Register ourself with ourself's selected event, so that we can notify winEvent of the state change.
 		self.Bind(wx.EVT_CHECKLISTBOX, self.notifyIAccessible)
 
@@ -117,27 +112,13 @@ def __init__(self, parent, id=wx.ID_ANY, autoSizeColumnIndex="LAST", pos=wx.Defa
 	):
 		AutoWidthColumnListCtrl.__init__(self, parent, id=id, pos=pos, size=size, style=style)
 		listmix.CheckListCtrlMixin.__init__(self, check_image, uncheck_image, imgsz)
-		# Import late to prevent circular import.
-		from IAccessibleHandler import accPropServices
 		# Register object with COM to fix accessibility bugs in wx.
 		server = ListCtrlAccPropServer(self)
-		accPropServices.SetHwndPropServer(
-			hwnd=self.GetHandle(),
-			idObject=winUser.OBJID_CLIENT,
-			idChild=0,
-			paProps=CHECK_LIST_PROPS,
-			cProps=len(CHECK_LIST_PROPS),
-			pServer=server,
-			AnnoScope=1
-		)
 		# Register our hook to check/uncheck items with space.
 		# Use wx.EVT_CHAR_HOOK, because EVT_LIST_KEY_DOWN isn't triggered for space.
 		self.Bind(wx.EVT_CHAR_HOOK, self.onCharHook)
 		# Register an additional event handler to call sendCheckListBoxEvent for mouse clicks if appropriate.
 		self.Bind(wx.EVT_LEFT_DOWN, self.onLeftDown)
-		# clean up of the accPropServices needs to happen when the control is destroyed. The Destroy method is not called
-		# by the wx framework, but we can register to receive the event. wxWidgets/Phoenix/#630
-		self.Bind(wx.EVT_WINDOW_DESTROY, self._onDestroy, source=self)
 
 	def GetCheckedItems(self):
 		return tuple(i for i in xrange(self.ItemCount) if self.IsChecked(i))
@@ -191,20 +172,6 @@ def sendCheckListBoxEvent(self, index):
 		evt.Int = index
 		self.ProcessEvent(evt)
 
-	def _onDestroy(self, evt):
-		evt.Skip() #  Allow other handlers to process this event.
-		self._cleanup()
-
-	def _cleanup(self):
-		from IAccessibleHandler import accPropServices
-		accPropServices.ClearHwndProps(
-			hwnd=self.GetHandle(),
-			idObject=winUser.OBJID_CLIENT,
-			idChild=0,
-			paProps=CHECK_LIST_PROPS,
-			cProps=len(CHECK_LIST_PROPS)
-		)
-
 class DPIScaledDialog(wx.Dialog, DpiScalingHelperMixin):
 	"""Automatically calls constructors in the right order, passing on arguments, and providing scaling features.
 	Until wxWidgets/wxWidgets#334 is resolved, and we have updated to that build of wx.


Test patch: