Repository: nvaccess/nvda

Issue:
NVDA 2021.1 crashes when attempting to exit from the menu with exit dialog disabled.

### Steps to reproduce:
1. Ensure that you have "Show exit options when exiting NVDA" unchecked in the general settings.
2. Activate Exit in the NVDA menu.
### Actual behavior:
NVDA plays an error sound, and standard Windows dialog for application has  stopped working appears.
The following is in the log:
```
Input: kb(desktop):enter
DEBUG - core._terminate (21:07:28.924) - MainThread (8492):
Terminating updateCheck
DEBUG - core._terminate (21:07:28.924) - MainThread (8492):
Terminating watchdog
DEBUG - core._terminate (21:07:28.924) - MainThread (8492):
Terminating globalPluginHandler
DEBUG - core._closeAllWindows (21:07:28.924) - MainThread (8492):
destroying system tray icon and menu
ERROR - unhandled exception (21:07:28.954) - MainThread (8492):
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 184, in _crashHandler
SystemError: <_FuncPtr object at 0x02A16AF8> returned a result with an error set
IO - inputCore.InputManager.executeGesture (21:07:30.184) - winInputHook (1072):
Input: kb(desktop):alt+tab

```

### Expected behavior:
NVDA exits normally.

### System configuration
#### NVDA installed/portable/running from source:
From the launcher

#### NVDA version:
NVDA 2021.1 Beta 1

#### Windows version:
Windows 7 X64

#### Name and version of other software in use when reproducing the issue:
None
#### Other information about your system:
None relevant
### 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.
This is new in 2021.1

#### If add-ons are disabled, is your problem still occurring?
Yes

#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu?
Not tried


cc @seanbudd 


Discussion:

Resolving patch:
diff --git a/source/core.py b/source/core.py
--- a/source/core.py
+++ b/source/core.py
@@ -232,7 +232,9 @@ def getWxLangOrNone() -> Optional['wx.LanguageInfo']:
 
 
 def triggerNVDAExit():
-	preNVDAExit.notifyOnce()
+	import queueHandler
+	# queue this so that the calling process can exit safely (eg a Popup menu)
+	queueHandler.queueFunction(queueHandler.eventQueue, preNVDAExit.notifyOnce)
 
 
 def _closeAllWindows():


Test patch:
diff --git a/tests/system/nvdaSettingsFiles/standard-dontShowExitDialog.ini b/tests/system/nvdaSettingsFiles/standard-dontShowExitDialog.ini
new file mode 100644
--- /dev/null
+++ b/tests/system/nvdaSettingsFiles/standard-dontShowExitDialog.ini
@@ -0,0 +1,18 @@
+schemaVersion = 2
+[general]
+	showWelcomeDialogAtStartup = True
+	askToExit = False
+[update]
+	askedAllowUsageStats = True
+	autoCheck = False
+	startupNotification = False
+	allowUsageStats = False
+[speech]
+	synth = speechSpySynthDriver
+[development]
+	enableScratchpadDir = True
+[virtualBuffers]
+	autoSayAllOnPageLoad = False
+	passThroughAudioIndication = False
+[annotations]
+	reportDetails = True
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
@@ -32,6 +32,33 @@ def NVDA_Starts():
 	_process.process_should_be_running(_nvdaProcessAlias)
 
 
+def quits_from_menu(showExitDialog=True):
+	"""Ensure NVDA can be quit from menu."""
+	spy = _nvdaLib.getSpyLib()
+	_builtIn.sleep(1)
+	spy.emulateKeyPress("NVDA+n")
+	spy.emulateKeyPress("x", blockUntilProcessed=False)  # don't block so NVDA can exit
+	if showExitDialog:
+		exitTitleIndex = spy.wait_for_specific_speech("Exit NVDA")
+
+		spy.wait_for_speech_to_finish()
+		actualSpeech = spy.get_speech_at_index_until_now(exitTitleIndex)
+
+		_asserts.strings_match(
+			actualSpeech,
+			"\n".join([
+				"Exit NVDA  dialog",
+				"What would you like to do?  combo box  Exit  collapsed  Alt plus d"
+			])
+		)
+		_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.process_should_be_stopped(_nvdaProcessAlias)
+
+
+
 def quits_from_keyboard():
 	"""Ensure NVDA can be quit from keyboard."""
 	spy = _nvdaLib.getSpyLib()
@@ -40,7 +67,7 @@ def quits_from_keyboard():
 	_builtIn.sleep(1)  # the dialog is not always receiving the enter keypress, wait a little longer for it
 	spy.emulateKeyPress("enter")
 
-	spy.emulateKeyPress("insert+q")
+	spy.emulateKeyPress("NVDA+q")
 	exitTitleIndex = spy.wait_for_specific_speech("Exit NVDA")
 
 	spy.wait_for_speech_to_finish()
@@ -54,7 +81,7 @@ def quits_from_keyboard():
 		])
 	)
 	_builtIn.sleep(1)  # the dialog is not always receiving the enter keypress, wait a little longer for it
-	spy.emulateKeyPress("enter", blockUntilProcessed=False)
+	spy.emulateKeyPress("enter", blockUntilProcessed=False)  # don't block so NVDA can exit
 	_process.wait_for_process(_nvdaProcessAlias, timeout="10 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
@@ -30,6 +30,11 @@ Quits from keyboard
 	[Setup]	start NVDA	standard-doShowWelcomeDialog.ini
 	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
+	quits from menu	False	# run test
+
 Read welcome dialog
 	[Documentation]	Ensure that the welcome dialog can be read in full
 	[Setup]	start NVDA	standard-doShowWelcomeDialog.ini