Skip to content

Commit 6bf2942

Browse files
authored
Merge 59509c2 into e2d2696
2 parents e2d2696 + 59509c2 commit 6bf2942

6 files changed

Lines changed: 104 additions & 67 deletions

File tree

source/JABHandler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -795,10 +795,10 @@ def initialize():
795795
):
796796
enableBridge()
797797
# Accept wm_copydata and any wm_user messages from other processes even if running with higher privileges
798-
if not windll.user32.ChangeWindowMessageFilter(winUser.WM_COPYDATA, 1):
798+
if not windll.user32.ChangeWindowMessageFilter(winUser.WM_COPYDATA, winUser.MSGFLT.ALLOW):
799799
raise WinError()
800800
for msg in range(winUser.WM_USER + 1, 0xffff):
801-
if not windll.user32.ChangeWindowMessageFilter(msg, 1):
801+
if not windll.user32.ChangeWindowMessageFilter(msg, winUser.MSGFLT.ALLOW):
802802
raise WinError()
803803
bridgeDll.Windows_run()
804804
# Register java events

source/core.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -580,16 +580,10 @@ def _doPostNvdaStartupAction():
580580

581581
queueHandler.queueFunction(queueHandler.eventQueue, _doPostNvdaStartupAction)
582582

583-
584583
log.debug("entering wx application main loop")
585584
app.MainLoop()
586585

587586
log.info("Exiting")
588-
if updateCheck:
589-
_terminate(updateCheck)
590-
591-
_terminate(watchdog)
592-
_terminate(globalPluginHandler, name="global plugin handler")
593587
_terminate(gui)
594588
config.saveOnExit()
595589

source/gui/__init__.py

Lines changed: 78 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# This file is covered by the GNU General Public License.
66
# See the file COPYING for more details.
77

8-
import typing
8+
from typing import Dict, Optional
99
import time
1010
import os
1111
import sys
@@ -25,14 +25,17 @@
2525
import queueHandler
2626
import core
2727
from . import guiHelper
28-
from . import settingsDialogs
28+
from .settingsDialogs import SettingsDialog
2929
from .settingsDialogs import *
3030
from .inputGestures import InputGesturesDialog
3131
import speechDictHandler
3232
from . import logViewer
3333
import speechViewer
3434
import winUser
3535
import api
36+
import globalPluginHandler
37+
import brailleViewer
38+
import watchdog
3639

3740
try:
3841
import updateCheck
@@ -47,6 +50,7 @@
4750
### Globals
4851
mainFrame = None
4952
isInMessageBox = False
53+
_hasAppExited = False
5054

5155

5256
class MainFrame(wx.Frame):
@@ -357,25 +361,6 @@ def onConfigProfilesCommand(self, evt):
357361
ProfilesDialog(gui.mainFrame).Show()
358362
self.postPopup()
359363

360-
361-
def safeAppExit():
362-
"""
363-
Ensures the app is exited by all the top windows being destroyed
364-
"""
365-
366-
for window in wx.GetTopLevelWindows():
367-
if isinstance(window, wx.Dialog) and window.IsModal():
368-
log.info(f"ending modal {window} during exit process")
369-
wx.CallAfter(window.EndModal, wx.ID_CLOSE_ALL)
370-
if isinstance(window, MainFrame):
371-
log.info(f"destroying main frame during exit process")
372-
# the MainFrame has EVT_CLOSE bound to the ExitDialog
373-
# which calls this function on exit, so destroy this window
374-
wx.CallAfter(window.Destroy)
375-
else:
376-
log.info(f"closing window {window} during exit process")
377-
wx.CallAfter(window.Close)
378-
379364
class SysTrayIcon(wx.adv.TaskBarIcon):
380365

381366
def __init__(self, frame):
@@ -558,6 +543,74 @@ def onActivate(self, evt):
558543
appModules.nvda.nvdaMenuIaIdentity = None
559544
mainFrame.postPopup()
560545

546+
547+
def _terminateModule(module, name: Optional[str] = None):
548+
if name is None:
549+
name = module.__name__
550+
log.debug("Terminating %s" % name)
551+
try:
552+
module.terminate()
553+
except Exception as e:
554+
log.exception(f"Error terminating {name}\nException: {e}")
555+
556+
557+
def safeAppExit():
558+
"""
559+
Ensures the app is exited by all the top windows being destroyed.
560+
wx objects that don't inherit from wx.Window (eg sysTrayIcon, Menu) need to be manually destroyed.
561+
"""
562+
global _hasAppExited, mainFrame
563+
564+
if _hasAppExited:
565+
return
566+
567+
if updateCheck:
568+
_terminateModule(updateCheck)
569+
570+
_terminateModule(watchdog)
571+
_terminateModule(globalPluginHandler)
572+
brailleViewer.destroyBrailleViewer()
573+
574+
app = wx.GetApp()
575+
576+
# prevent race condition with object deletion
577+
# prevent deletion of the object while we work on it.
578+
_SettingsDialog = SettingsDialog
579+
nonWeak: Dict[_SettingsDialog, _SettingsDialog] = dict(_SettingsDialog._instances)
580+
581+
for instance, state in nonWeak.items():
582+
if state is _SettingsDialog.DialogState.DESTROYED:
583+
log.error(
584+
"Destroyed but not deleted instance of gui.SettingsDialog exists"
585+
f": {instance.title} - {instance.__class__.__qualname__} - {instance}"
586+
)
587+
else:
588+
log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance))
589+
590+
# wx.Windows destroy child Windows automatically but wx.Menu and TaskBarIcon don't inherit from wx.Window.
591+
# They must be manually destroyed when exiting the app.
592+
# Note: this doesn't consistently clean them from the tray and appears to be a wx issue. (#12286, #12238)
593+
if mainFrame is not None:
594+
log.debug("destroying system tray icon and menu")
595+
app.ScheduleForDestruction(mainFrame.sysTrayIcon.menu)
596+
mainFrame.sysTrayIcon.RemoveIcon()
597+
app.ScheduleForDestruction(mainFrame.sysTrayIcon)
598+
599+
for window in wx.GetTopLevelWindows():
600+
if isinstance(window, wx.Dialog) and window.IsModal():
601+
log.debug(f"ending modal {window} during exit process")
602+
wx.CallAfter(window.EndModal, wx.ID_CLOSE_ALL)
603+
if isinstance(window, MainFrame):
604+
log.debug("destroying main frame during exit process")
605+
# the MainFrame has EVT_CLOSE bound to the ExitDialog
606+
# which calls this function on exit, so destroy this window
607+
app.ScheduleForDestruction(window)
608+
else:
609+
log.debug(f"closing window {window} during exit process")
610+
wx.CallAfter(window.Close)
611+
612+
_hasAppExited = True
613+
561614
def initialize():
562615
global mainFrame
563616
if mainFrame:
@@ -585,33 +638,12 @@ def wx_CallAfter_wrapper(func, *args, **kwargs):
585638
wx.CallAfter = wx_CallAfter_wrapper
586639

587640
def terminate():
588-
import brailleViewer
589-
brailleViewer.destroyBrailleViewer()
590-
591-
# prevent race condition with object deletion
592-
# prevent deletion of the object while we work on it.
593-
_SettingsDialog = settingsDialogs.SettingsDialog
594-
nonWeak: typing.Dict[_SettingsDialog, _SettingsDialog] = dict(_SettingsDialog._instances)
595-
596-
for instance, state in nonWeak.items():
597-
if state is _SettingsDialog.DialogState.DESTROYED:
598-
log.error(
599-
"Destroyed but not deleted instance of gui.SettingsDialog exists"
600-
f": {instance.title} - {instance.__class__.__qualname__} - {instance}"
601-
)
602-
else:
603-
log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance))
604641
global mainFrame
605-
# This is called after the main loop exits because WM_QUIT exits the main loop
606-
# without destroying all objects correctly and we need to support WM_QUIT.
607-
# Therefore, any request to exit should exit the main loop.
642+
643+
# If MainLoop is terminated through WM_QUIT, such as starting an NVDA instance older than 2021.1,
644+
# safeAppExit has not been called yet
608645
safeAppExit()
609-
# #4460: We need another iteration of the main loop
610-
# so that everything (especially the TaskBarIcon) is cleaned up properly.
611-
# ProcessPendingEvents doesn't seem to work, but MainLoop does.
612-
# Because the top window gets destroyed,
613-
# MainLoop thankfully returns pretty quickly.
614-
wx.GetApp().MainLoop()
646+
615647
mainFrame = None
616648

617649
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: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def terminateRunningNVDA(window):
168168
# The process is already dead.
169169
return
170170
try:
171-
res=winKernel.waitForSingleObject(h,4000)
171+
res = winKernel.waitForSingleObject(h, 6000) # give time to exit NVDA safely
172172
if res==0:
173173
# The process terminated within the timeout period.
174174
return
@@ -198,8 +198,8 @@ if oldAppWindowHandle and not globalVars.appArgs.easeOfAccess:
198198
sys.exit(0)
199199
try:
200200
terminateRunningNVDA(oldAppWindowHandle)
201-
except:
202-
sys.exit(1)
201+
except Exception as e:
202+
parser.error(f"Couldn't terminate existing NVDA process, abandoning start:\nException: {e}")
203203
if globalVars.appArgs.quit or (oldAppWindowHandle and globalVars.appArgs.easeOfAccess):
204204
sys.exit(0)
205205
elif globalVars.appArgs.check_running:
@@ -251,9 +251,11 @@ if customVenvDetected:
251251
log.warning("NVDA launched using a custom Python virtual environment.")
252252
if globalVars.appArgs.changeScreenReaderFlag:
253253
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()
254+
255+
# Accept WM_QUIT from other processes, even if running with higher privileges
256+
if not ctypes.windll.user32.ChangeWindowMessageFilter(winUser.WM_QUIT, winUser.MSGFLT.ALLOW):
257+
log.error("Unable to set the NVDA process to receive WM_QUIT messages from other processes")
258+
raise winUser.WinError()
257259
# Make this the last application to be shut down and don't display a retry dialog box.
258260
winKernel.SetProcessShutdownParameters(0x100, winKernel.SHUTDOWN_NORETRY)
259261
if not isSecureDesktop and not config.isAppX:

source/winUser.py

Lines changed: 15 additions & 6 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
@@ -114,12 +115,6 @@ class GUITHREADINFO(Structure):
114115
CBS_OWNERDRAWFIXED=0x0010
115116
CBS_OWNERDRAWVARIABLE=0x0020
116117
CBS_HASSTRINGS=0x00200
117-
WM_NULL=0
118-
WM_QUIT=18
119-
WM_COPYDATA=74
120-
WM_NOTIFY=78
121-
WM_DEVICECHANGE=537
122-
WM_USER=1024
123118
#PeekMessage
124119
PM_REMOVE=1
125120
PM_NOYIELD=2
@@ -146,6 +141,7 @@ class GUITHREADINFO(Structure):
146141
WM_NOTIFY = 78
147142
WM_USER = 1024
148143
WM_QUIT = 18
144+
WM_DEVICECHANGE = 537
149145
WM_DISPLAYCHANGE = 0x7e
150146
WM_GETTEXT=13
151147
WM_GETTEXTLENGTH=14
@@ -377,6 +373,19 @@ class GUITHREADINFO(Structure):
377373
# The height of the virtual screen, in pixels.
378374
SM_CYVIRTUALSCREEN = 79
379375

376+
377+
class MSGFLT(enum.IntEnum):
378+
# Actions associated with ChangeWindowMessageFilterEx
379+
# https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-changewindowmessagefilterex
380+
# Adds the message to the filter. This has the effect of allowing the message to be received.
381+
ALLOW = 1
382+
# Removes the message from the filter. This has the effect of blocking the message.
383+
DISALLOW = 2
384+
# Resets the window message filter to the default.
385+
# Any message allowed globally or process-wide will get through.
386+
RESET = 0
387+
388+
380389
def setSystemScreenReaderFlag(val):
381390
user32.SystemParametersInfoW(SPI_SETSCREENREADER,val,0,SPIF_UPDATEINIFILE|SPIF_SENDCHANGE)
382391

0 commit comments

Comments
 (0)