Repository: nvaccess/nvda

Issue:
NVDA crashes after a succesfulll install

### Steps to reproduce:
1. Download launcher of the latest Alpha
2. Close currently running copy of NVDA if any.
3. Start NVDA from the previously downloaded launcher, accept license and choose "install" from the launcher dialog.
4. Accept UAC and wait for NVDA to install successfully.
### Actual behavior:
At the end of installation NVDA crashes and the newly installed copy  starts correctly.
[crash_2021.1.zip](https://github.com/nvaccess/nvda/files/6489839/crash_2021.1.zip)
The attached archive contains log at the debug level and crash dump.

### Expected behavior:
No crash.
### System configuration
#### NVDA installed/portable/running from source:
Running from the launcher.
#### NVDA version:
NVDA version alpha-22774,44fabd74

#### Windows version:
Windows 8
#### Name and version of other software in use when reproducing the issue:
None
#### 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.
2020.4 does not crash
#### If add-ons are disabled, is your problem still occurring?
Tested with a clean config so no add-ons involved.

#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu?
yes - while I haven't run the tool explicitly it is being run implicitly during install.

cc @michaelDCurran  @seanbudd 


Discussion:
Does this problem also occur with silent installation? Please try “--install-silent” command.
relevant log 

```python
DEBUG - core._terminate (20:06:53.874) - MainThread (4520):
Terminating watchdog
```

```python
NVDA exit
CRITICAL - watchdog._crashHandler (20:06:55.077) - MainThread (4520):
NVDA crashed! Minidump written to C:\Users\Admin\AppData\Local\Temp\nvda.log\..\nvda_crash.dmp
INFO - watchdog._crashHandler (20:06:57.202) - MainThread (4520):
Listing stacks for Python threads:
Python stack for thread 4520 (MainThread):
  File "watchdog.pyc", line 213, in _crashHandler
  File "watchdog.pyc", line 63, in getFormattedStacksForAllThreads

INFO - watchdog._crashHandler (20:06:57.202) - MainThread (4520):
Restarting due to crash
```

Resolving patch:
diff --git a/source/core.py b/source/core.py
--- a/source/core.py
+++ b/source/core.py
@@ -232,11 +232,7 @@ def getWxLangOrNone() -> Optional['wx.LanguageInfo']:
 
 
 def triggerNVDAExit():
-	preNVDAExit.notify()
-	# ensure NVDA only runs exit procedures once
-	handlers = list(preNVDAExit.handlers)  # don't mutate .handlers directly with unregister while iterating
-	for handler in handlers:
-		preNVDAExit.unregister(handler)
+	preNVDAExit.notifyOnce()
 
 
 def _closeAllWindows():
diff --git a/source/extensionPoints/__init__.py b/source/extensionPoints/__init__.py
--- a/source/extensionPoints/__init__.py
+++ b/source/extensionPoints/__init__.py
@@ -48,6 +48,20 @@ def notify(self, **kwargs):
 			except:
 				log.exception("Error running handler %r for %r" % (handler, self))
 
+	def notifyOnce(self, **kwargs):
+		"""Notify all registered handlers that the action has occurred.
+		Unregister handlers after calling.
+		@param kwargs: Arguments to pass to the handlers.
+		"""
+		oldHandlers = list(self.handlers)
+		for handler in oldHandlers:
+			try:
+				callWithSupportedKwargs(handler, **kwargs)
+				self.unregister(handler)
+			except Exception as e:
+				log.exception(f"Error running handler {handler} for {self}. Exception {e}")
+
+
 class Filter(HandlerRegistrar):
 	"""Allows interested parties to register to modify a specific kind of data.
 	For example, this might be used to allow modification of spoken messages before they are passed to the synthesizer.
diff --git a/source/gui/installerGui.py b/source/gui/installerGui.py
--- a/source/gui/installerGui.py
+++ b/source/gui/installerGui.py
@@ -110,17 +110,24 @@ def doInstall(
 			# Translators: The title of a dialog presented to indicate a successful operation.
 			_("Success"))
 	if startAfterInstall:
-		# #4475: ensure that the first window of the new process is not hidden by providing SW_SHOWNORMAL  
-		shellapi.ShellExecute(
-			None,
-			None,
-			os.path.join(installer.defaultInstallPath,'nvda.exe'),
-			None,
-			None,
-			winUser.SW_SHOWNORMAL
-		)
-	else:
-		core.triggerNVDAExit()
+		core.preNVDAExit.register(_startNewInstalledNVDAInstance)
+	core.triggerNVDAExit()
+
+
+def _startNewInstalledNVDAInstance():
+	_startNewNVDAInstance(installer.defaultInstallPath)
+
+
+def _startNewNVDAInstance(path: str):
+	# #4475: ensure that the first window of the new process is not hidden by providing SW_SHOWNORMAL
+	shellapi.ShellExecute(
+		None,
+		None,
+		os.path.join(path, 'nvda.exe'),
+		None,
+		None,
+		winUser.SW_SHOWNORMAL
+	)
 
 
 def doSilentInstall(
@@ -269,7 +276,7 @@ def onInstall(self, evt):
 			copyPortableConfig=self.copyPortableConfigCheckbox.Value,
 			silent=self.isUpdate
 		)
-		self.Destroy()
+		wx.GetApp().ScheduleForDestruction(self)
 
 	def onCancel(self, evt):
 		self.Destroy()
@@ -466,20 +473,13 @@ def doCreatePortable(portableDirectory,copyUserConfig=False,silent=False,startAf
 			wx.OK | wx.ICON_ERROR)
 		return
 	d.done()
-	if silent:
-		core.triggerNVDAExit()
-	else:
+	if not silent:
 		# Translators: The message displayed when a portable copy of NVDA has been successfully created.
 		# %s will be replaced with the destination directory.
 		gui.messageBox(_("Successfully created a portable copy of NVDA at %s")%portableDirectory,
 			_("Success"))
 		if startAfterCreate:
-			# #4475: ensure that the first window of the new process is not hidden by providing SW_SHOWNORMAL  
-			shellapi.ShellExecute(
-				None,
-				None,
-				os.path.join(portableDirectory, 'nvda.exe'),
-				None,
-				None,
-				winUser.SW_SHOWNORMAL
-			)
+			def startNewPortableNVDAInstance():
+				_startNewNVDAInstance(portableDirectory)
+			core.preNVDAExit.register(startNewPortableNVDAInstance)
+	core.triggerNVDAExit()


Test patch: