Skip to content

Commit 89fe666

Browse files
authored
Merge 9f561ca into bc8eb11
2 parents bc8eb11 + 9f561ca commit 89fe666

5 files changed

Lines changed: 100 additions & 36 deletions

File tree

source/core.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,13 +388,22 @@ def __init__(self, windowName=None):
388388
self.orientationStateCache = self.ORIENTATION_NOT_INITIALIZED
389389
self.orientationCoordsCache = (0,0)
390390
self.handlePowerStatusChange()
391+
# Accept WM_EXIT_NVDA from other NVDA instances
392+
import winUser
393+
if not winUser.user32.ChangeWindowMessageFilterEx(self.handle, winUser.WM_EXIT_NVDA, 1, None):
394+
log.error(
395+
f"Unable to set the thread {self.handle} to receive WM_EXIT_NVDA from other processes")
396+
raise winUser.WinError()
391397

392398
def windowProc(self, hwnd, msg, wParam, lParam):
393399
post_windowMessageReceipt.notify(msg=msg, wParam=wParam, lParam=lParam)
394400
if msg == self.WM_POWERBROADCAST and wParam == self.PBT_APMPOWERSTATUSCHANGE:
395401
self.handlePowerStatusChange()
396402
elif msg == winUser.WM_DISPLAYCHANGE:
397403
self.handleScreenOrientationChange(lParam)
404+
elif msg == winUser.WM_EXIT_NVDA:
405+
log.debug("NVDA instance being closed from another instance")
406+
gui.safeAppExit()
398407

399408
def handleScreenOrientationChange(self, lParam):
400409
import ui

source/gui/__init__.py

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
### Globals
4848
mainFrame = None
4949
isInMessageBox = False
50-
50+
hasAppExited = False
5151

5252
class MainFrame(wx.Frame):
5353

@@ -360,22 +360,53 @@ def onConfigProfilesCommand(self, evt):
360360

361361
def safeAppExit():
362362
"""
363-
Ensures the app is exited by all the top windows being destroyed
363+
Ensures the app is exited by all the top windows being destroyed.
364+
wx objects that don't inherit from wx.Window (eg sysTrayIcon, Menu) need to be manually destroyed.
364365
"""
365366

367+
import brailleViewer
368+
brailleViewer.destroyBrailleViewer()
369+
370+
# prevent race condition with object deletion
371+
# prevent deletion of the object while we work on it.
372+
_SettingsDialog = settingsDialogs.SettingsDialog
373+
nonWeak: typing.Dict[_SettingsDialog, _SettingsDialog] = dict(_SettingsDialog._instances)
374+
375+
for instance, state in nonWeak.items():
376+
if state is _SettingsDialog.DialogState.DESTROYED:
377+
log.error(
378+
"Destroyed but not deleted instance of gui.SettingsDialog exists"
379+
f": {instance.title} - {instance.__class__.__qualname__} - {instance}"
380+
)
381+
else:
382+
log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance))
383+
384+
# wx.Windows destroy child Windows automatically but wx.Menu and TaskBarIcon don't inherit from wx.Window.
385+
# They must be manually destroyed when exiting the app.
386+
# Note: this doesn't consistently clean them from the tray and appears to be a wx issue. (#12286, #12238)
387+
log.debug("destroying system tray icon and menu")
388+
389+
mainFrame.sysTrayIcon.menu.Destroy()
390+
mainFrame.sysTrayIcon.RemoveIcon()
391+
mainFrame.sysTrayIcon.Destroy()
392+
366393
for window in wx.GetTopLevelWindows():
367394
if isinstance(window, wx.Dialog) and window.IsModal():
368-
log.info(f"ending modal {window} during exit process")
395+
log.debug(f"ending modal {window} during exit process")
369396
wx.CallAfter(window.EndModal, wx.ID_CLOSE_ALL)
370397
if isinstance(window, MainFrame):
371-
log.info(f"destroying main frame during exit process")
398+
log.debug("destroying main frame during exit process")
372399
# the MainFrame has EVT_CLOSE bound to the ExitDialog
373400
# which calls this function on exit, so destroy this window
374401
wx.CallAfter(window.Destroy)
375402
else:
376-
log.info(f"closing window {window} during exit process")
403+
log.debug(f"closing window {window} during exit process")
377404
wx.CallAfter(window.Close)
378405

406+
global hasAppExited
407+
hasAppExited = True
408+
409+
379410
class SysTrayIcon(wx.adv.TaskBarIcon):
380411

381412
def __init__(self, frame):
@@ -584,33 +615,13 @@ def wx_CallAfter_wrapper(func, *args, **kwargs):
584615
wx.CallAfter = wx_CallAfter_wrapper
585616

586617
def terminate():
587-
import brailleViewer
588-
brailleViewer.destroyBrailleViewer()
618+
global mainFrame
589619

590-
# prevent race condition with object deletion
591-
# prevent deletion of the object while we work on it.
592-
_SettingsDialog = settingsDialogs.SettingsDialog
593-
nonWeak: typing.Dict[_SettingsDialog, _SettingsDialog] = dict(_SettingsDialog._instances)
620+
# If MainLoop is terminated through WM_QUIT, such as starting an NVDA instance older than 2021.1,
621+
# safeAppExit has not been called yet
622+
if not hasAppExited:
623+
safeAppExit()
594624

595-
for instance, state in nonWeak.items():
596-
if state is _SettingsDialog.DialogState.DESTROYED:
597-
log.error(
598-
"Destroyed but not deleted instance of gui.SettingsDialog exists"
599-
f": {instance.title} - {instance.__class__.__qualname__} - {instance}"
600-
)
601-
else:
602-
log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance))
603-
global mainFrame
604-
# This is called after the main loop exits because WM_QUIT exits the main loop
605-
# without destroying all objects correctly and we need to support WM_QUIT.
606-
# Therefore, any request to exit should exit the main loop.
607-
safeAppExit()
608-
# #4460: We need another iteration of the main loop
609-
# so that everything (especially the TaskBarIcon) is cleaned up properly.
610-
# ProcessPendingEvents doesn't seem to work, but MainLoop does.
611-
# Because the top window gets destroyed,
612-
# MainLoop thankfully returns pretty quickly.
613-
wx.GetApp().MainLoop()
614625
mainFrame = None
615626

616627
def showGui():

source/gui/startupDialogs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def run(cls):
117117
gui.mainFrame.prePopup()
118118
d = cls(gui.mainFrame)
119119
d.ShowModal()
120-
d.Destroy()
120+
wx.CallAfter(d.Destroy)
121121
gui.mainFrame.postPopup()
122122

123123

source/nvda.pyw

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,21 @@ for name in pathAppArgs:
162162

163163
def terminateRunningNVDA(window):
164164
processID,threadID=winUser.getWindowThreadProcessID(window)
165-
winUser.PostMessage(window,winUser.WM_QUIT,0,0)
165+
try:
166+
winUser.PostSafeQuitMessage(window)
167+
except PermissionError:
168+
# allow for updating between NVDA versions, as NVDA <= 2020.4 does not accept WM_EXIT_NVDA messages
169+
log.debugWarning("Failed to post a safe quit message across NVDA instances, sending WM_QUIT")
170+
winUser.PostMessage(window, winUser.WM_QUIT, 0, 0)
171+
except OSError as winErr:
172+
log.error("Failed to post a quit message across NVDA instances")
173+
raise winErr
166174
h=winKernel.openProcess(winKernel.SYNCHRONIZE,False,processID)
167175
if not h:
168176
# The process is already dead.
169177
return
170178
try:
171-
res=winKernel.waitForSingleObject(h,4000)
179+
res = winKernel.waitForSingleObject(h, 6000) # give time to exit NVDA safely
172180
if res==0:
173181
# The process terminated within the timeout period.
174182
return
@@ -251,9 +259,14 @@ if customVenvDetected:
251259
log.warning("NVDA launched using a custom Python virtual environment.")
252260
if globalVars.appArgs.changeScreenReaderFlag:
253261
winUser.setSystemScreenReaderFlag(True)
254-
#Accept wm_quit from other processes, even if running with higher privilages
255-
if not ctypes.windll.user32.ChangeWindowMessageFilter(winUser.WM_QUIT,1):
256-
raise WinError()
262+
263+
# Accept WM_QUIT from other processes, even if running with higher privilages
264+
# 2020.4 and earlier versions sent a WM_QUIT message when asking NVDA to exit.
265+
# Some users may run several different versions of NVDA, so we continue to support this.
266+
# WM_QUIT does not allow NVDA to shutdown cleanly, now WM_EXIT_NVDA is used instead
267+
if not ctypes.windll.user32.ChangeWindowMessageFilter(winUser.WM_QUIT, winUser.MSGFLT.ALLOW):
268+
log.error("Unable to set the NVDA process to receive WM_QUIT messages from other processes")
269+
raise winUser.WinError()
257270
# Make this the last application to be shut down and don't display a retry dialog box.
258271
winKernel.SetProcessShutdownParameters(0x100, winKernel.SHUTDOWN_NORETRY)
259272
if not isSecureDesktop and not config.isAppX:

source/winUser.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from ctypes.wintypes import HWND, RECT, DWORD
1414
import winKernel
1515
from textUtils import WCHAR_ENCODING
16+
import enum
1617

1718
#dll handles
1819
user32=windll.user32
@@ -377,6 +378,27 @@ class GUITHREADINFO(Structure):
377378
# The height of the virtual screen, in pixels.
378379
SM_CYVIRTUALSCREEN = 79
379380

381+
382+
class MSGFLT(enum.IntEnum):
383+
# Actions associated with ChangeWindowMessageFilterEx
384+
# https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-changewindowmessagefilterex
385+
# Adds the message to the filter. This has the effect of allowing the message to be received.
386+
ALLOW = 1
387+
# Removes the message from the filter. This has the effect of blocking the message.
388+
DISALLOW = 2
389+
# Resets the window message filter to the default.
390+
# Any message allowed globally or process-wide will get through.
391+
RESET = 0
392+
393+
394+
# Registers an application wide Window Message so that NVDA can be exited across instances
395+
WM_EXIT_NVDA = user32.RegisterWindowMessageW("WM_EXIT_NVDA")
396+
if not WM_EXIT_NVDA:
397+
winErr = WinError()
398+
# provides additional information to the OSError based WinError
399+
winErr.filename = "Failed to register Windows application message WM_EXIT_NVDA"
400+
raise winErr
401+
380402
def setSystemScreenReaderFlag(val):
381403
user32.SystemParametersInfoW(SPI_SETSCREENREADER,val,0,SPIF_UPDATEINIFILE|SPIF_SENDCHANGE)
382404

@@ -601,6 +623,15 @@ def PostMessage(hwnd, msg, wParam, lParam):
601623
if not user32.PostMessageW(hwnd, msg, wParam, lParam):
602624
raise WinError()
603625

626+
627+
def PostSafeQuitMessage(hwnd: HWND):
628+
"""
629+
Posts a WM_EXIT_NVDA quit message across windows to exit NVDA safely from another instance
630+
@param hwnd: Target NVDA window id
631+
"""
632+
if not user32.PostMessageW(hwnd, WM_EXIT_NVDA, None, None):
633+
raise WinError()
634+
604635
user32.VkKeyScanExW.restype = SHORT
605636
def VkKeyScanEx(ch, hkl):
606637
res = user32.VkKeyScanExW(WCHAR(ch), hkl)

0 commit comments

Comments
 (0)