Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor the exit of nvda and gui.terminate #12286

Merged
merged 18 commits into from
Apr 22, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions source/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,13 +391,20 @@ def __init__(self, windowName=None):
self.orientationStateCache = self.ORIENTATION_NOT_INITIALIZED
self.orientationCoordsCache = (0,0)
self.handlePowerStatusChange()
# Accept WM_EXIT_NVDA from other NVDA instances
import winUser
if not winUser.user32.ChangeWindowMessageFilterEx(self.handle, winUser.WM_EXIT_NVDA, 1, None):
raise winUser.WinError()

def windowProc(self, hwnd, msg, wParam, lParam):
post_windowMessageReceipt.notify(msg=msg, wParam=wParam, lParam=lParam)
if msg == self.WM_POWERBROADCAST and wParam == self.PBT_APMPOWERSTATUSCHANGE:
self.handlePowerStatusChange()
elif msg == winUser.WM_DISPLAYCHANGE:
self.handleScreenOrientationChange(lParam)
elif msg == winUser.WM_EXIT_NVDA:
log.debug("NVDA instance being closed from another instance")
gui.safeAppExit()

def handleScreenOrientationChange(self, lParam):
import ui
Expand Down
42 changes: 18 additions & 24 deletions source/gui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,22 +358,36 @@ def onConfigProfilesCommand(self, evt):

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

import brailleViewer
brailleViewer.destroyBrailleViewer()

# wx.Windows destroy child Windows automatically but wx.Menu and TaskBarIcon don't inherit from wx.Window.
# They must be manually destroyed when exiting the app.
# Note: this doesn't consistently clean them from the tray and appears to be a wx issue. (#12286, #12238)
log.debug("destroying system tray icon and menu")

mainFrame.sysTrayIcon.menu.Destroy()
mainFrame.sysTrayIcon.RemoveIcon()
mainFrame.sysTrayIcon.Destroy()

for window in wx.GetTopLevelWindows():
if isinstance(window, wx.Dialog) and window.IsModal():
log.info(f"ending modal {window} during exit process")
log.debug(f"ending modal {window} during exit process")
wx.CallAfter(window.EndModal, wx.ID_CLOSE_ALL)
if isinstance(window, MainFrame):
log.info(f"destroying main frame during exit process")
log.debug("destroying main frame during exit process")
# the MainFrame has EVT_CLOSE bound to the ExitDialog
# which calls this function on exit, so destroy this window
wx.CallAfter(window.Destroy)
else:
log.info(f"closing window {window} during exit process")
log.debug(f"closing window {window} during exit process")
wx.CallAfter(window.Close)


class SysTrayIcon(wx.adv.TaskBarIcon):

def __init__(self, frame):
Expand Down Expand Up @@ -582,27 +596,7 @@ def wx_CallAfter_wrapper(func, *args, **kwargs):
wx.CallAfter = wx_CallAfter_wrapper

def terminate():
import brailleViewer
brailleViewer.destroyBrailleViewer()

for instance, state in gui.SettingsDialog._instances.items():
if state is gui.SettingsDialog._DIALOG_DESTROYED_STATE:
log.error(
"Destroyed but not deleted instance of settings dialog exists: {!r}".format(instance)
)
else:
log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance))
global mainFrame
# This is called after the main loop exits because WM_QUIT exits the main loop
# without destroying all objects correctly and we need to support WM_QUIT.
# Therefore, any request to exit should exit the main loop.
safeAppExit()
# #4460: We need another iteration of the main loop
# so that everything (especially the TaskBarIcon) is cleaned up properly.
# ProcessPendingEvents doesn't seem to work, but MainLoop does.
# Because the top window gets destroyed,
# MainLoop thankfully returns pretty quickly.
wx.GetApp().MainLoop()
mainFrame = None

def showGui():
Expand Down
2 changes: 1 addition & 1 deletion source/gui/startupDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def run(cls):
gui.mainFrame.prePopup()
d = cls(gui.mainFrame)
d.ShowModal()
d.Destroy()
wx.CallAfter(d.Destroy)
gui.mainFrame.postPopup()


Expand Down
13 changes: 10 additions & 3 deletions source/nvda.pyw
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,18 @@ for name in pathAppArgs:

def terminateRunningNVDA(window):
processID,threadID=winUser.getWindowThreadProcessID(window)
winUser.PostMessage(window,winUser.WM_QUIT,0,0)
try:
winUser.PostSafeQuitMessage(window)
except PermissionError:
# allow for updating between NVDA versions, deprecated in 2022.1
# in 2022.1 just call winUser.PostSafeQuitMessage(window) without the try/except
winUser.PostMessage(window, winUser.WM_QUIT, 0, 0)
h=winKernel.openProcess(winKernel.SYNCHRONIZE,False,processID)
if not h:
# The process is already dead.
return
try:
res=winKernel.waitForSingleObject(h,4000)
res = winKernel.waitForSingleObject(h, 5000)
if res==0:
# The process terminated within the timeout period.
return
Expand Down Expand Up @@ -251,7 +256,9 @@ if customVenvDetected:
log.warning("NVDA launched using a custom Python virtual environment.")
if globalVars.appArgs.changeScreenReaderFlag:
winUser.setSystemScreenReaderFlag(True)
#Accept wm_quit from other processes, even if running with higher privilages
# Accept wm_quit from other processes, even if running with higher privilages
# This allows for downgrading between NVDA versions, deprecated in 2022.1
# in 2022.1 remove the call to ChangeWindowMessageFilter (#12286)
if not ctypes.windll.user32.ChangeWindowMessageFilter(winUser.WM_QUIT,1):
raise WinError()
# Make this the last application to be shut down and don't display a retry dialog box.
Expand Down
14 changes: 14 additions & 0 deletions source/winUser.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,11 @@ class GUITHREADINFO(Structure):
# The height of the virtual screen, in pixels.
SM_CYVIRTUALSCREEN = 79

# Registers an application wide Window Message so that NVDA can be exited across instances
WM_EXIT_NVDA = user32.RegisterWindowMessageW("WM_EXIT_NVDA")
if not WM_EXIT_NVDA:
raise WinError()

def setSystemScreenReaderFlag(val):
user32.SystemParametersInfoW(SPI_SETSCREENREADER,val,0,SPIF_UPDATEINIFILE|SPIF_SENDCHANGE)

Expand Down Expand Up @@ -601,6 +606,15 @@ def PostMessage(hwnd, msg, wParam, lParam):
if not user32.PostMessageW(hwnd, msg, wParam, lParam):
raise WinError()


def PostSafeQuitMessage(hwnd: HWND):
"""
Posts a WM_EXIT_NVDA quit message across windows to exit NVDA safely from another instance
@param hwnd: Target NVDA window id
"""
if not user32.PostMessageW(hwnd, WM_EXIT_NVDA, None, None):
raise WinError()

user32.VkKeyScanExW.restype = SHORT
def VkKeyScanEx(ch, hkl):
res = user32.VkKeyScanExW(WCHAR(ch), hkl)
Expand Down