Skip to content

Commit 57dc902

Browse files
authored
Merge b78a060 into 60c0b76
2 parents 60c0b76 + b78a060 commit 57dc902

6 files changed

Lines changed: 46 additions & 124 deletions

File tree

source/core.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -377,22 +377,13 @@ def __init__(self, windowName=None):
377377
self.orientationStateCache = self.ORIENTATION_NOT_INITIALIZED
378378
self.orientationCoordsCache = (0,0)
379379
self.handlePowerStatusChange()
380-
# Accept WM_EXIT_NVDA from other NVDA instances
381-
import winUser
382-
if not winUser.user32.ChangeWindowMessageFilterEx(self.handle, winUser.WM_EXIT_NVDA, 1, None):
383-
log.error(
384-
f"Unable to set the thread {self.handle} to receive WM_EXIT_NVDA from other processes")
385-
raise winUser.WinError()
386380

387381
def windowProc(self, hwnd, msg, wParam, lParam):
388382
post_windowMessageReceipt.notify(msg=msg, wParam=wParam, lParam=lParam)
389383
if msg == self.WM_POWERBROADCAST and wParam == self.PBT_APMPOWERSTATUSCHANGE:
390384
self.handlePowerStatusChange()
391385
elif msg == winUser.WM_DISPLAYCHANGE:
392386
self.handleScreenOrientationChange(lParam)
393-
elif msg == winUser.WM_EXIT_NVDA:
394-
log.debug("NVDA instance being closed from another instance")
395-
gui.safeAppExit()
396387

397388
def handleScreenOrientationChange(self, lParam):
398389
import ui

source/gui/__init__.py

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

5252
class MainFrame(wx.Frame):
5353

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

361361
def safeAppExit():
362362
"""
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.
363+
Ensures the app is exited by all the top windows being destroyed
365364
"""
366365

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

407-
global hasAppExited
408-
hasAppExited = True
409-
410-
411379
class SysTrayIcon(wx.adv.TaskBarIcon):
412380

413381
def __init__(self, frame):
@@ -616,13 +584,33 @@ def wx_CallAfter_wrapper(func, *args, **kwargs):
616584
wx.CallAfter = wx_CallAfter_wrapper
617585

618586
def terminate():
619-
global mainFrame
587+
import brailleViewer
588+
brailleViewer.destroyBrailleViewer()
620589

621-
# If MainLoop is terminated through WM_QUIT, such as starting an NVDA instance older than 2021.1,
622-
# safeAppExit has not been called yet
623-
if not hasAppExited:
624-
safeAppExit()
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)
625594

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()
626614
mainFrame = None
627615

628616
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-
wx.CallAfter(d.Destroy)
120+
d.Destroy()
121121
gui.mainFrame.postPopup()
122122

123123

source/nvda.pyw

Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -160,28 +160,18 @@ for name in pathAppArgs:
160160
newVal = os.path.abspath(origVal)
161161
setattr(globalVars.appArgs, name, newVal)
162162

163-
164-
def safelyTerminateRunningNVDA(window: winUser.HWND):
163+
def terminateRunningNVDA(window):
165164
processID,threadID=winUser.getWindowThreadProcessID(window)
166-
winUser.PostSafeQuitMessage(window)
165+
winUser.PostMessage(window,winUser.WM_QUIT,0,0)
167166
h=winKernel.openProcess(winKernel.SYNCHRONIZE,False,processID)
168167
if not h:
169168
# The process is already dead.
170169
return
171170
try:
172-
res = winKernel.waitForSingleObject(h, 6000) # give time to exit NVDA safely
171+
res=winKernel.waitForSingleObject(h,4000)
173172
if res==0:
174173
# The process terminated within the timeout period.
175174
return
176-
else:
177-
raise OSError("Failed to terminate with WM_EXIT_NVDA")
178-
except OSError:
179-
# allow for updating between NVDA versions, as NVDA <= 2020.4 does not accept WM_EXIT_NVDA messages
180-
print("Failed to post a safe quit message across NVDA instances, sending WM_QUIT", file=sys.stderr)
181-
res = _terminateRunningLegacyNVDA(window)
182-
if res == 0:
183-
# The process terminated within the timeout period.
184-
return
185175
finally:
186176
winKernel.closeHandle(h)
187177

@@ -195,20 +185,6 @@ def safelyTerminateRunningNVDA(window: winUser.HWND):
195185
finally:
196186
winKernel.closeHandle(h)
197187

198-
199-
def _terminateRunningLegacyNVDA(window: winUser.HWND) -> int:
200-
'''
201-
Returns 0 on success, raises an OSError based WinErr if the process isn't killed
202-
'''
203-
processID, _threadID = winUser.getWindowThreadProcessID(window)
204-
winUser.PostMessage(window, winUser.WM_QUIT, 0, 0)
205-
h = winKernel.openProcess(winKernel.SYNCHRONIZE, False, processID)
206-
if not h:
207-
# The process is already dead.
208-
return 0
209-
return winKernel.waitForSingleObject(h, 4000)
210-
211-
212188
#Handle running multiple instances of NVDA
213189
try:
214190
oldAppWindowHandle=winUser.FindWindow(u'wxWindowClassNR',u'NVDA')
@@ -221,9 +197,8 @@ if oldAppWindowHandle and not globalVars.appArgs.easeOfAccess:
221197
# NVDA is running.
222198
sys.exit(0)
223199
try:
224-
safelyTerminateRunningNVDA(oldAppWindowHandle)
225-
except Exception as e:
226-
print(f"Couldn't terminate existing NVDA process, abandoning start:\nException: {e}", file=sys.stderr)
200+
terminateRunningNVDA(oldAppWindowHandle)
201+
except:
227202
sys.exit(1)
228203
if globalVars.appArgs.quit or (oldAppWindowHandle and globalVars.appArgs.easeOfAccess):
229204
sys.exit(0)
@@ -276,14 +251,9 @@ if customVenvDetected:
276251
log.warning("NVDA launched using a custom Python virtual environment.")
277252
if globalVars.appArgs.changeScreenReaderFlag:
278253
winUser.setSystemScreenReaderFlag(True)
279-
280-
# Accept WM_QUIT from other processes, even if running with higher privilages
281-
# 2020.4 and earlier versions sent a WM_QUIT message when asking NVDA to exit.
282-
# Some users may run several different versions of NVDA, so we continue to support this.
283-
# WM_QUIT does not allow NVDA to shutdown cleanly, now WM_EXIT_NVDA is used instead
284-
if not ctypes.windll.user32.ChangeWindowMessageFilter(winUser.WM_QUIT, winUser.MSGFLT.ALLOW):
285-
log.error("Unable to set the NVDA process to receive WM_QUIT messages from other processes")
286-
raise winUser.WinError()
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()
287257
# Make this the last application to be shut down and don't display a retry dialog box.
288258
winKernel.SetProcessShutdownParameters(0x100, winKernel.SHUTDOWN_NORETRY)
289259
if not isSecureDesktop and not config.isAppX:

source/winUser.py

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

1817
#dll handles
1918
user32=windll.user32
@@ -115,6 +114,12 @@ class GUITHREADINFO(Structure):
115114
CBS_OWNERDRAWFIXED=0x0010
116115
CBS_OWNERDRAWVARIABLE=0x0020
117116
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
118123
#PeekMessage
119124
PM_REMOVE=1
120125
PM_NOYIELD=2
@@ -141,7 +146,6 @@ class GUITHREADINFO(Structure):
141146
WM_NOTIFY = 78
142147
WM_USER = 1024
143148
WM_QUIT = 18
144-
WM_DEVICECHANGE = 537
145149
WM_DISPLAYCHANGE = 0x7e
146150
WM_GETTEXT=13
147151
WM_GETTEXTLENGTH=14
@@ -373,27 +377,6 @@ class GUITHREADINFO(Structure):
373377
# The height of the virtual screen, in pixels.
374378
SM_CYVIRTUALSCREEN = 79
375379

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-
389-
# Registers an application wide Window Message so that NVDA can be exited across instances
390-
WM_EXIT_NVDA = user32.RegisterWindowMessageW("WM_EXIT_NVDA")
391-
if not WM_EXIT_NVDA:
392-
winErr = WinError()
393-
# provides additional information to the OSError based WinError
394-
winErr.filename = "Failed to register Windows application message WM_EXIT_NVDA"
395-
raise winErr
396-
397380
def setSystemScreenReaderFlag(val):
398381
user32.SystemParametersInfoW(SPI_SETSCREENREADER,val,0,SPIF_UPDATEINIFILE|SPIF_SENDCHANGE)
399382

@@ -618,15 +601,6 @@ def PostMessage(hwnd, msg, wParam, lParam):
618601
if not user32.PostMessageW(hwnd, msg, wParam, lParam):
619602
raise WinError()
620603

621-
622-
def PostSafeQuitMessage(hwnd: HWND):
623-
"""
624-
Posts a WM_EXIT_NVDA quit message across windows to exit NVDA safely from another instance
625-
@param hwnd: Target NVDA window id
626-
"""
627-
if not user32.PostMessageW(hwnd, WM_EXIT_NVDA, None, None):
628-
raise WinError()
629-
630604
user32.VkKeyScanExW.restype = SHORT
631605
def VkKeyScanEx(ch, hkl):
632606
res = user32.VkKeyScanExW(WCHAR(ch), hkl)

user_docs/en/changes.t2t

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@ What's New in NVDA
115115
- This usage is prefered instead of ti1.SetEndPoint(ti2,"startToEnd")
116116
- `wx.CENTRE_ON_SCREEN` and `wx.CENTER_ON_SCREEN` are removed, use `self.CentreOnScreen()` instead. (#12309)
117117
- `easeOfAccess.isSupported` has been removed, NVDA only supports versions of Windows where this evaluates to `True`. (#12222)
118-
- Do not exit NVDA by sending a `WM_QUIT` message to the process. Instead send `winUser.WM_EXIT_NVDA` to the window handle (found by `winUser.FindWindow('wxWindowClassNR', 'NVDA')`). (#12286)
119118

120119

121120
= 2020.4 =

0 commit comments

Comments
 (0)