Repository: nvaccess/nvda

Issue:
NVDA sometimes loses speech when toggling highlight
### Steps to reproduce:

* Start the computer. Note: I do not start Zoomtext nor Windows Magnifier.
* Start NVDA (from source)
* Open Vision settings panel
* Tab to the first item 'Enable Highlight'
* Press 'E' key during about 5 seconds; should activate and unactivate 'Enable Highlight' item at very high frequency.
* Press Tab many times to go to next items.

### Actual behavior:

Nothing is heard.
However, pressing windows key a few times let me know visually that the system is responsive since I can see the start menu open and close without delay.

Log attached: 
[nvda.log](https://github.com/nvaccess/nvda/files/3863918/nvda.log)


### Expected behavior:

The focus should move to the next item and thus, these items should be announced.

### Context

The steps to reproduce is not a real use case, but it is the best stress test I found to be able to reproduce the issue.
The original real use case is just playing with various options in the Vision panel, as an exploration, i.e. tabbing around and toggling the various focus highlight options. I got however the bug only randomly and very few times (about 1/30); that why I have tried to find a use case easier to reproduce.


### System configuration
#### NVDA installed/portable/running from source:

From source

#### NVDA version:

Version : source-master-e7b61dc
(last master version)

#### Windows version:

Windows 10 1709

#### Name and version of other software in use when reproducing the issue:

N/A

#### Other information about your system:

Zoomtext installed but not active since the time I have started my PC. I did not start Zoomtext nor Windows Magnifier, in order not to interfer with the Highlight feature.

### Other questions
#### Does the issue still occur after restarting your PC?

Yes

#### Have you tried any other versions of NVDA? If so, please report their behaviors.

No, not applicable for older versions since this feature is new.
I did not try with last snapshot.




Discussion:
Cc @leonardder , @feerrenrut 
Ugh, this probably has to do with the highlighter thread not being 
killed correctly. I will try to come up with a fix tomorrow.


Resolving patch:
diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py
--- a/source/visionEnhancementProviders/NVDAHighlighter.py
+++ b/source/visionEnhancementProviders/NVDAHighlighter.py
@@ -104,11 +104,10 @@ def updateLocationForDisplays(self):
 	def __init__(self, highlighter):
 		if vision._isDebug():
 			log.debug("initializing NVDAHighlighter window")
-		super(HighlightWindow, self).__init__(
+		super().__init__(
 			windowName=self.windowName,
 			windowStyle=self.windowStyle,
-			extendedWindowStyle=self.extendedWindowStyle,
-			parent=gui.mainFrame.Handle
+			extendedWindowStyle=self.extendedWindowStyle
 		)
 		self.location = None
 		self.highlighterRef = weakref.ref(highlighter)
@@ -343,6 +342,14 @@ def _onCheckEvent(self, evt: wx.CommandEvent):
 		providerInst: Optional[NVDAHighlighter] = self._providerControl.getProviderInstance()
 		if providerInst:
 			providerInst.refresh()
+		elif evt.IsChecked():
+			# One or more check boxes are enabled, so the provider instance must be there.
+			# Yet, there is no instance. This must be a case where initialization failed.
+			settingsStorage.highlightBrowseMode = False
+			settingsStorage.highlightFocus = False
+			settingsStorage.highlightNavigator = False
+			self.updateDriverSettings()
+			self._updateEnabledState()
 
 
 class NVDAHighlighter(providerBase.VisionEnhancementProvider):
@@ -355,7 +362,7 @@ class NVDAHighlighter(providerBase.VisionEnhancementProvider):
 	_refreshInterval = 100
 	customWindowClass = HighlightWindow
 	_settings = NVDAHighlighterSettings()
-
+	_window: Optional[customWindowClass] = None
 	enabledContexts: Tuple[Context]  # type info for autoprop: L{_get_enableContexts}
 
 	@classmethod  # override
@@ -387,42 +394,52 @@ def __init__(self):
 		log.debug("Starting NVDAHighlighter")
 		self.contextToRectMap = {}
 		winGDI.gdiPlusInitialize()
-		self.window = None
 		self._highlighterThread = threading.Thread(
 			name=f"{self.__class__.__module__}.{self.__class__.__qualname__}",
 			target=self._run
 		)
+		self._highlighterRunningEvent = threading.Event()
 		self._highlighterThread.daemon = True
 		self._highlighterThread.start()
+		# Make sure the highlighter thread doesn't exit early.
+		waitResult = self._highlighterRunningEvent.wait(0.2)
+		if waitResult is False or not self._highlighterThread.is_alive():
+			raise RuntimeError("Highlighter thread wasn't able to initialize correctly")
 
 	def terminate(self):
 		log.debug("Terminating NVDAHighlighter")
-		if self._highlighterThread:
+		if self._highlighterThread and self._window and self._window.handle:
 			if not winUser.user32.PostThreadMessageW(self._highlighterThread.ident, winUser.WM_QUIT, 0, 0):
 				raise WinError()
-			self._highlighterThread.join()
+			else:
+				self._highlighterThread.join()
 			self._highlighterThread = None
 		winGDI.gdiPlusTerminate()
 		self.contextToRectMap.clear()
 		super().terminate()
 
 	def _run(self):
-		if vision._isDebug():
-			log.debug("Starting NVDAHighlighter thread")
-		window = self.window = self.customWindowClass(self)
-		self.timer = winUser.WinTimer(window.handle, 0, self._refreshInterval, None)
-		msg = MSG()
-		while winUser.getMessage(byref(msg), None, 0, 0):
-			winUser.user32.TranslateMessage(byref(msg))
-			winUser.user32.DispatchMessageW(byref(msg))
-		if vision._isDebug():
-			log.debug("Quit message received on NVDAHighlighter thread")
-		if self.timer:
-			self.timer.terminate()
-			self.timer = None
-		if self.window:
-			self.window.destroy()
-			self.window = None
+		try:
+			if vision._isDebug():
+				log.debug("Starting NVDAHighlighter thread")
+			try:
+				window = self._window = self.customWindowClass(self)
+				timer = winUser.WinTimer(window.handle, 0, self._refreshInterval, None)
+			finally:
+				self._highlighterRunningEvent.set()
+			msg = MSG()
+			# Python 3.8 note, Change this to use an Assignment expression to catch a return value of -1.
+			# See the remarks section of
+			# https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage
+			while winUser.getMessage(byref(msg), None, 0, 0) > 0:
+				winUser.user32.TranslateMessage(byref(msg))
+				winUser.user32.DispatchMessageW(byref(msg))
+			if vision._isDebug():
+				log.debug("Quit message received on NVDAHighlighter thread")
+			timer.terminate()
+			window.destroy()
+		except Exception:
+			log.exception("Exception in NVDA Highlighter thread")
 
 	def updateContextRect(self, context, rect=None, obj=None):
 		"""Updates the position rectangle of the highlight for the specified context.
@@ -454,8 +471,8 @@ def handleBrowseModeMove(self, obj=None):
 	def refresh(self):
 		"""Refreshes the screen positions of the enabled highlights.
 		"""
-		if self.window:
-			self.window.refresh()
+		if self._window and self._window.handle:
+			self._window.refresh()
 
 	def _get_enabledContexts(self):
 		"""Gets the contexts for which the highlighter is enabled.
diff --git a/source/windowUtils.py b/source/windowUtils.py
--- a/source/windowUtils.py
+++ b/source/windowUtils.py
@@ -141,6 +141,14 @@ class CustomWindow(AutoPropertyObject):
 	The window will be destroyed when the instance is deleted,
 	but it can be explicitly destroyed using L{destroy}.
 	"""
+	handle: Optional[int] = None
+
+	@classmethod
+	def __new__(cls, *args, **kwargs):
+		for instance in cls._hwndsToInstances.values():
+			if type(instance) is cls:
+				raise RuntimeError(f"Only one instance of {cls.__qualname__} may exist at a time")
+		return super().__new__(cls, *args, **kwargs)
 
 	@classmethod
 	def _get__wClass(cls):


Test patch: