Repository: nvaccess/nvda

Issue:
NVDA crashes if exiting while welcome dialog open
<!-- Please read the text in this edit field before filling it in.
Please thoroughly read NVDA's wiki article on how to fill in this template, including how to provide the required files.
Issues may be closed if the required information is not present.
https://github.com/nvaccess/nvda/blob/master/devDocs/githubIssueTemplateExplanationAndExamples.md
Please also note that the NVDA project has a Citizen and Contributor Code of Conduct which can be found at https://github.com/nvaccess/nvda/blob/master/CODE_OF_CONDUCT.md. NV Access expects that all contributors and other community members read and abide by the rules set out in this document while participating or contributing to this project. This includes creating or commenting on issues and pull requests. 

Each of the questions and sections below start with multiple hash symbols (#). Place your answers and information on the blank line below each question.
-->

### Steps to reproduce:
1. Have NVDA running
2. Press NVDA+n then h to open the help menu, down arrow to "Welcome dialog" and enter to open that.
3. Press NVDA+Q then ENTER to quit NVDA

### Actual behavior:

NVDA crashes and restarts.  Log fragment from crash:

```
IO - inputCore.InputManager.executeGesture (08:32:33.488) - winInputHook (29244):
Input: kb(desktop):NVDA+q
DEBUGWARNING - NVDAObjects.IAccessible.IAccessible._get_IAccessibleRole (08:32:33.600) - MainThread (31608):
accRole failed: (-2147024809, 'The parameter is incorrect.', (None, None, None, 0, None))
IO - speech.speech.speak (08:32:33.638) - MainThread (31608):
Speaking [LangChangeCommand ('en_US'), 'Exit NVDA', 'dialog', CancellableSpeech (still valid)]
IO - speech.speech.speak (08:32:33.640) - MainThread (31608):
Speaking [LangChangeCommand ('en_US'), 'What would you like to do?', 'combo box', 'Exit', 'collapsed', 'Alt+d', CancellableSpeech (still valid)]
IO - inputCore.InputManager.executeGesture (08:32:34.000) - winInputHook (29244):
Input: kb(desktop):numpadEnter
DEBUG - core.triggerNVDAExit (08:32:34.001) - MainThread (31608):
_doShutdown has been queued
DEBUG - core._terminate (08:32:34.018) - MainThread (31608):
Terminating updateCheck
DEBUG - core._terminate (08:32:34.019) - MainThread (31608):
Terminating watchdog
DEBUG - core._terminate (08:32:34.019) - MainThread (31608):
Terminating globalPluginHandler
DEBUG - core._closeAllWindows (08:32:34.019) - MainThread (31608):
destroying system tray icon and menu
ERROR - unhandled exception (08:32:34.030) - MainThread (31608):
wx._core.wxAssertionError: C++ assertion "GetEventHandler() == this" failed at ..\..\src\common\wincmn.cpp(470) in wxWindowBase::~wxWindowBase(): any pushed event handlers must have been removed

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "_ctypes/callbacks.c", line 232, in 'calling callback function'
  File "watchdog.pyc", line 183, in _crashHandler
SystemError: <_FuncPtr object at 0x02EF15D0> returned a result with an error set
CRITICAL - watchdog._crashHandler (08:32:34.166) - MainThread (31608):
NVDA crashed! Minidump written to C:\Users\quent\AppData\Local\Temp\nvda.log\..\nvda_crash.dmp
INFO - watchdog._crashHandler (08:32:36.446) - MainThread (31608):
Listing stacks for Python threads:
Python stack for thread 33660 (touchHandler.TouchHandler):
  File "threading.pyc", line 890, in _bootstrap
  File "threading.pyc", line 926, in _bootstrap_inner
  File "touchHandler.pyc", line 238, in run

Python stack for thread 29244 (winInputHook):
  File "threading.pyc", line 890, in _bootstrap
  File "threading.pyc", line 926, in _bootstrap_inner
  File "threading.pyc", line 870, in run
  File "winInputHook.pyc", line 79, in hookThreadFunc

Python stack for thread 25368 (_UIAHandler.UIAHandler.MTAThread):
  File "threading.pyc", line 890, in _bootstrap
  File "threading.pyc", line 926, in _bootstrap_inner
  File "threading.pyc", line 870, in run
  File "_UIAHandler.pyc", line 340, in MTAThreadFunc
  File "queue.pyc", line 170, in get
  File "threading.pyc", line 296, in wait

Python stack for thread 15036 (braille._BgThread):
  File "threading.pyc", line 890, in _bootstrap
  File "threading.pyc", line 926, in _bootstrap_inner
  File "threading.pyc", line 870, in run
  File "braille.pyc", line 2201, in func

Python stack for thread 31608 (MainThread):
  File "nvda.pyw", line 362, in <module>
  File "core.pyc", line 746, in main
  File "wx\core.pyc", line 2237, in MainLoop
  File "wx\core.pyc", line 3407, in <lambda>
  File "gui\__init__.pyc", line 117, in showGui
  File "gui\__init__.pyc", line 546, in onActivate
  File "gui\__init__.pyc", line 495, in <lambda>
  File "gui\startupDialogs.pyc", line 119, in run
  File "gui\__init__.pyc", line 830, in Notify
  File "core.pyc", line 708, in run
  File "queueHandler.pyc", line 88, in pumpAll
  File "queueHandler.pyc", line 55, in flushQueue
  File "core.pyc", line 293, in _doShutdown
  File "core.pyc", line 353, in _closeAllWindows
  File "watchdog.pyc", line 212, in _crashHandler
  File "watchdog.pyc", line 62, in getFormattedStacksForAllThreads

INFO - watchdog._crashHandler (08:32:36.446) - MainThread (31608):
Restarting due to crash
INFO - core.restartUnsafely (08:32:36.446) - MainThread (31608):
Restarting unsafely
DEBUG - core._startNewInstance (08:32:36.446) - MainThread (31608):
Starting new NVDA instance: NewNVDAInstance(filePath='C:\\Program Files (x86)\\NVDA\\nvda.exe', parameters='-r', directory='C:\\Program Files (x86)\\NVDA')

```
### Expected behavior:

NVDA should exit.  There was a previous issue for this for which a fix was merged earlier in the year - https://github.com/nvaccess/nvda/issues/1740 - not sure if this is related or a completely separate issue.  Closing NVDA while the settings dialog is open does work.


### System configuration
#### NVDA installed/portable/running from source:

#### NVDA version:

NVDA 2021.2 installed

#### Windows version:

Windows 10 (64-bit) Version: 2009, Build: 19043 

#### Name and version of other software in use when reproducing the issue:

#### Other information about your system:

### 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.

No

#### If NVDA add-ons are disabled, is your problem still occurring?

No add-ons installed

#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu?



Discussion:
I think this is an issue with how the Welcome Dialog is opened (the code is different to other dialogs if I recall correctly), and was possibly introduced in 2021.1.

When fixing this, I would want to fix a bug noted in https://github.com/nvaccess/nvda/pull/12800#issuecomment-911676751: 

NVDA+N not working while these dialogs are opened:
Welcome dialog
About dialog

 I imagine this issue is true for the about dialog as well
Confirmed that this is the case with the about dialog too

Resolving patch:
diff --git a/source/gui/__init__.py b/source/gui/__init__.py
--- a/source/gui/__init__.py
+++ b/source/gui/__init__.py
@@ -26,6 +26,7 @@
 from . import guiHelper
 from .settingsDialogs import SettingsDialog
 from .settingsDialogs import *
+from .startupDialogs import WelcomeDialog
 from .inputGestures import InputGesturesDialog
 import speechDictHandler
 from . import logViewer
@@ -367,7 +368,7 @@ def onConfigProfilesCommand(self, evt):
 
 class SysTrayIcon(wx.adv.TaskBarIcon):
 
-	def __init__(self, frame):
+	def __init__(self, frame: MainFrame):
 		super(SysTrayIcon, self).__init__()
 		icon=wx.Icon(ICON_PATH,wx.BITMAP_TYPE_ICO)
 		self.SetIcon(icon, versionInfo.name)
@@ -684,6 +685,7 @@ def onOk(self, evt):
 		if action >= 2 and config.isAppX:
 			action += 1
 		if action == 0:
+			WelcomeDialog.closeInstances()
 			if not core.triggerNVDAExit():
 				log.error("NVDA already in process of exiting, this indicates a logic error.")
 			return  # there's no need to destroy ExitDialog in this instance as triggerNVDAExit will do this
diff --git a/source/gui/startupDialogs.py b/source/gui/startupDialogs.py
--- a/source/gui/startupDialogs.py
+++ b/source/gui/startupDialogs.py
@@ -4,6 +4,8 @@
 # This file may be used under the terms of the GNU General Public License, version 2 or later.
 # For more details see: https://www.gnu.org/licenses/gpl-2.0.html
 
+from typing import Set
+import weakref
 import wx
 
 import config
@@ -36,10 +38,12 @@ class WelcomeDialog(
 		"Press NVDA+n at any time to activate the NVDA menu.\n"
 		"From this menu, you can configure NVDA, get help and access other NVDA functions."
 	)
+	_instances: Set["WelcomeDialog"] = weakref.WeakSet()
 
 	def __init__(self, parent):
 		# Translators: The title of the Welcome dialog when user starts NVDA for the first time.
 		super().__init__(parent, wx.ID_ANY, _("Welcome to NVDA"))
+		WelcomeDialog._instances.add(self)
 
 		mainSizer = wx.BoxSizer(wx.VERTICAL)
 		# Translators: The header for the Welcome dialog when user starts NVDA for the first time.
@@ -108,6 +112,7 @@ def onOk(self, evt):
 		except Exception:
 			log.debugWarning("Could not save", exc_info=True)
 		self.EndModal(wx.ID_OK)
+		self.Close()
 
 	@classmethod
 	def run(cls):
@@ -117,9 +122,17 @@ def run(cls):
 		gui.mainFrame.prePopup()
 		d = cls(gui.mainFrame)
 		d.ShowModal()
-		wx.CallAfter(d.Destroy)
 		gui.mainFrame.postPopup()
 
+	@classmethod
+	def closeInstances(cls):
+		instances = list(cls._instances)
+		for instance in instances:
+			if instance and not instance.IsBeingDeleted() and instance.IsModal():
+				instance.EndModal(wx.ID_CLOSE_ALL)
+			else:
+				cls._instances.remove(instance)
+
 
 class LauncherDialog(
 		gui.contextHelp.ContextHelpMixin,


Test patch:
diff --git a/tests/system/robot/startupShutdownNVDA.py b/tests/system/robot/startupShutdownNVDA.py
--- a/tests/system/robot/startupShutdownNVDA.py
+++ b/tests/system/robot/startupShutdownNVDA.py
@@ -39,6 +39,22 @@ def NVDA_Starts():
 	_process.process_should_be_running(_nvdaProcessAlias)
 
 
+def open_welcome_dialog_from_menu():
+	spy = _nvdaLib.getSpyLib()
+	spy.emulateKeyPress("NVDA+n")
+	spy.emulateKeyPress("h")
+	spy.emulateKeyPress("l")
+	spy.wait_for_specific_speech("Welcome to NVDA")  # ensure the dialog is present.
+
+
+def open_about_dialog_from_menu():
+	spy = _nvdaLib.getSpyLib()
+	spy.emulateKeyPress("NVDA+n")
+	spy.emulateKeyPress("h")
+	spy.emulateKeyPress("a")
+	spy.wait_for_specific_speech("About NVDA")  # ensure the dialog is present.
+
+
 def quits_from_menu(showExitDialog=True):
 	"""Ensure NVDA can be quit from menu."""
 	spy = _nvdaLib.getSpyLib()
@@ -61,17 +77,14 @@ def quits_from_menu(showExitDialog=True):
 		_builtIn.sleep(1)  # the dialog is not always receiving the enter keypress, wait a little for it
 		spy.emulateKeyPress("enter", blockUntilProcessed=False)  # don't block so NVDA can exit
 
-	_process.wait_for_process(_nvdaProcessAlias, timeout="10 sec")
+	_process.wait_for_process(_nvdaProcessAlias, timeout="3 sec")
 	_process.process_should_be_stopped(_nvdaProcessAlias)
 
 
 def quits_from_keyboard():
 	"""Ensure NVDA can be quit from keyboard."""
 	spy = _nvdaLib.getSpyLib()
-	spy.wait_for_specific_speech("Welcome to NVDA")  # ensure the dialog is present.
-	spy.wait_for_speech_to_finish()
-	_builtIn.sleep(1)  # the dialog is not always receiving the enter keypress, wait a little longer for it
-	spy.emulateKeyPress("enter")
+	_builtIn.sleep(1)  # the dialog is not always receiving the enter keypress, wait a little for it
 
 	spy.emulateKeyPress("NVDA+q")
 	exitTitleIndex = spy.wait_for_specific_speech("Exit NVDA")
@@ -87,8 +100,9 @@ def quits_from_keyboard():
 		])
 	)
 	_builtIn.sleep(1)  # the dialog is not always receiving the enter keypress, wait a little longer for it
+	_process.process_should_be_running(_nvdaProcessAlias)
 	spy.emulateKeyPress("enter", blockUntilProcessed=False)  # don't block so NVDA can exit
-	_process.wait_for_process(_nvdaProcessAlias, timeout="10 sec")
+	_process.wait_for_process(_nvdaProcessAlias, timeout="3 sec")
 	_process.process_should_be_stopped(_nvdaProcessAlias)
 
 
diff --git a/tests/system/robot/startupShutdownNVDA.robot b/tests/system/robot/startupShutdownNVDA.robot
--- a/tests/system/robot/startupShutdownNVDA.robot
+++ b/tests/system/robot/startupShutdownNVDA.robot
@@ -36,6 +36,20 @@ Quits from keyboard
 	[Documentation]	Starts NVDA and ensures that it can be quit using the keyboard
 	quits_from_keyboard	# run test
 
+Quits from keyboard with welcome dialog open
+	[Documentation]	Starts NVDA and ensures that it can be quit with the welcome dialog open
+	[Setup]	start NVDA	standard-dontShowWelcomeDialog.ini
+	open welcome dialog from menu
+	quits from keyboard	# run test
+
+Quits from keyboard with about dialog open
+	[Documentation]	Starts NVDA and ensures that it can be quit with the about dialog open
+	[Setup]	start NVDA	standard-dontShowWelcomeDialog.ini
+	# Excluded to be fixed still (#12907, #12957)
+	[Tags]	excluded_from_build
+	open about dialog from menu
+	quits from keyboard	# run test
+
 Quits from menu
 	[Documentation]	Starts NVDA and ensures that it can be quit using the keyboard
 	[Setup]	start NVDA	standard-dontShowExitDialog.ini