Skip to content

Commit

Permalink
When installing add-ons in add-on store make sure that restart dialog…
Browse files Browse the repository at this point in the history
… is shown only when the install is done (#15852)

Fixes #15613

Summary of the issue:
When installing add-ons developers may want to present a GUI prompt as part of the install tasks. When installing from the external bundle this prompt is blocking, in other words user has to answer it before installation is complete and NVDA asks to be restarted. In comparison installations from the add-on store shows the prompt, but then the restart prompt was shown, making it impossible to interact with the add-ons installation GUI.

Description of user facing changes
When installing add-ons from the store the restart message is shown only when all installations are done, giving user a chance to interact with eventual GUI prompts they raise.

Description of development approach
Similar to the code path used when installing external bundles, call to the addonHandler.installAddonBundle is executed in a separate, blocking threat using ExecAndPump. ExecAndPump was also moved from gui to systemUtils, as it has no relation to GUI, other than the fact that it was used only in this package.
  • Loading branch information
lukaszgo1 committed Nov 28, 2023
1 parent 8052b68 commit 641c2fd
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 39 deletions.
3 changes: 2 additions & 1 deletion source/addonStore/install.py
Expand Up @@ -12,6 +12,7 @@
Optional,
)

import systemUtils
from logHandler import log

from .dataManager import (
Expand Down Expand Up @@ -83,7 +84,7 @@ def installAddon(addonPath: PathLike) -> None:
prevAddon = _getPreviouslyInstalledAddonById(bundle)

try:
installAddonBundle(bundle)
systemUtils.ExecAndPump(installAddonBundle, bundle)
if prevAddon:
prevAddon.requestRemove()
except AddonError: # Handle other exceptions as they are known
Expand Down
43 changes: 9 additions & 34 deletions source/gui/__init__.py
Expand Up @@ -5,9 +5,7 @@
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

import time
import os
import threading
import ctypes
import wx
import wx.adv
Expand Down Expand Up @@ -116,6 +114,15 @@ def __getattr__(attrName: str) -> Any:
stack_info=True,
)
return SettingsPanel
if attrName == "ExecAndPump" and NVDAState._allowDeprecatedAPI():
log.warning(
"Importing ExecAndPump from here is deprecated. "
"Import ExecAndPump from systemUtils instead. ",
# Include stack info so testers can report warning to add-on author.
stack_info=True,
)
import systemUtils
return systemUtils.ExecAndPump
raise AttributeError(f"module {repr(__name__)} has no attribute {repr(attrName)}")


Expand Down Expand Up @@ -771,38 +778,6 @@ def run():
wx.CallAfter(run)


class ExecAndPump(threading.Thread):
"""Executes the given function with given args and kwargs in a background thread while blocking and pumping in the current thread."""

def __init__(self,func,*args,**kwargs):
self.func=func
self.args=args
self.kwargs=kwargs
fname = repr(func)
super().__init__(
name=f"{self.__class__.__module__}.{self.__class__.__qualname__}({fname})"
)
self.threadExc=None
self.start()
time.sleep(0.1)
threadHandle=ctypes.c_int()
threadHandle.value=ctypes.windll.kernel32.OpenThread(0x100000,False,self.ident)
msg=ctypes.wintypes.MSG()
while ctypes.windll.user32.MsgWaitForMultipleObjects(1,ctypes.byref(threadHandle),False,-1,255)==1:
while ctypes.windll.user32.PeekMessageW(ctypes.byref(msg),None,0,0,1):
ctypes.windll.user32.TranslateMessage(ctypes.byref(msg))
ctypes.windll.user32.DispatchMessageW(ctypes.byref(msg))
if self.threadExc:
raise self.threadExc

def run(self):
try:
self.func(*self.args,**self.kwargs)
except Exception as e:
self.threadExc=e
log.debugWarning("task had errors",exc_info=True)


class IndeterminateProgressDialog(wx.ProgressDialog):

def __init__(self, parent: wx.Window, title: str, message: str):
Expand Down
3 changes: 2 additions & 1 deletion source/gui/addonGui.py
Expand Up @@ -20,6 +20,7 @@
from .dpiScalingHelper import DpiScalingHelperMixinWithoutInit
import gui.contextHelp
import ui
import systemUtils


def promptUserForRestart():
Expand Down Expand Up @@ -217,7 +218,7 @@ def doneAndDestroy(window):
try:
# Use context manager to ensure that `done` and `Destroy` are called on the progress dialog afterwards
with doneAndDestroy(progressDialog):
gui.ExecAndPump(addonHandler.installAddonBundle, bundle)
systemUtils.ExecAndPump(addonHandler.installAddonBundle, bundle)
if prevAddon:
from addonStore.dataManager import addonDataManager
assert addonDataManager
Expand Down
4 changes: 2 additions & 2 deletions source/gui/installerGui.py
Expand Up @@ -71,7 +71,7 @@ def doInstall(
installedUserConfigPath=config.getInstalledUserConfigPath()
if installedUserConfigPath:
if _canPortableConfigBeCopied():
gui.ExecAndPump(installer.copyUserConfig, installedUserConfigPath)
systemUtils.ExecAndPump(installer.copyUserConfig, installedUserConfigPath)
except Exception as e:
res=e
log.error("Failed to execute installer",exc_info=True)
Expand Down Expand Up @@ -457,7 +457,7 @@ def doCreatePortable(
_("Please wait while a portable copy of NVDA is created.")
)
try:
gui.ExecAndPump(installer.createPortableCopy, portableDirectory, copyUserConfig)
systemUtils.ExecAndPump(installer.createPortableCopy, portableDirectory, copyUserConfig)
except Exception as e:
log.error("Failed to create portable copy", exc_info=True)
d.done()
Expand Down
3 changes: 2 additions & 1 deletion source/gui/settingsDialogs.py
Expand Up @@ -36,6 +36,7 @@
)
import languageHandler
import speech
import systemUtils
import gui
import gui.contextHelp
import globalVars
Expand Down Expand Up @@ -918,7 +919,7 @@ def onCopySettings(self,evt):
)
while True:
try:
gui.ExecAndPump(config.setSystemConfigToCurrentConfig)
systemUtils.ExecAndPump(config.setSystemConfigToCurrentConfig)
res=True
break
except installer.RetriableFailure:
Expand Down
43 changes: 43 additions & 0 deletions source/systemUtils.py
Expand Up @@ -6,19 +6,28 @@

""" System related functions."""
import ctypes
import time
import threading
from collections.abc import (
Callable,
)
from ctypes import (
byref,
create_unicode_buffer,
sizeof,
windll,
)
from typing import (
Any,
)
import winKernel
import winreg
import shellapi
import winUser
import functools
import shlobj
from os import startfile
from logHandler import log
from NVDAState import WritePaths


Expand Down Expand Up @@ -194,3 +203,37 @@ def _isSystemClockSecondsVisible() -> bool:
return False
except OSError:
return False


class ExecAndPump(threading.Thread):
"""Executes the given function with given args and kwargs in a background thread,
while blocking and pumping in the current thread.
"""

def __init__(self, func: Callable[..., Any], *args, **kwargs) -> None:
self.func = func
self.args = args
self.kwargs = kwargs
fname = repr(func)
super().__init__(
name=f"{self.__class__.__module__}.{self.__class__.__qualname__}({fname})"
)
self.threadExc: Exception | None = None
self.start()
time.sleep(0.1)
threadHandle = ctypes.c_int()
threadHandle.value = winKernel.kernel32.OpenThread(0x100000, False, self.ident)
msg = ctypes.wintypes.MSG()
while winUser.user32.MsgWaitForMultipleObjects(1, ctypes.byref(threadHandle), False, -1, 255) == 1:
while winUser.user32.PeekMessageW(ctypes.byref(msg), None, 0, 0, 1):
winUser.user32.TranslateMessage(ctypes.byref(msg))
winUser.user32.DispatchMessageW(ctypes.byref(msg))
if self.threadExc:
raise self.threadExc

def run(self):
try:
self.func(*self.args, **self.kwargs)
except Exception as e:
self.threadExc = e
log.debugWarning("task had errors", exc_info=True)
2 changes: 2 additions & 0 deletions user_docs/en/changes.t2t
Expand Up @@ -58,6 +58,7 @@ It is also updated with UIA enabled, when typing text and braille is tethered to
- NVDA no longer sometimes freezes when speaking a large amount of text. (#15752, @jcsteh)
- More objects which contain useful text are detected, and text content is displayed in braille. (#15605)
- When reinstalling an incompatible add-on it is no longer forcefully disabled. (#15584, @lukaszgo1)
- When installing add-ons in the Add-on Store, install prompts are no longer overlapped by the restart dialog. (#15613, @lukaszgo1)
- When accessing Microsoft Edge using UI Automation, NVDA is able to activate more controls in browse mode. (#14612)
- NVDA no longer freezes briefly when multiple sounds are played in rapid succession. (#15757, @jcsteh)
- NVDA will not fail to start anymore when the configuration file is corrupted, but it will restore the configuration to default as it did in the past. (#15690, @CyrilleB79)
Expand Down Expand Up @@ -218,6 +219,7 @@ Code which imports from one of them, should instead import from the replacement
- The ``bdDetect.KEY_*`` constants have been deprecated.
Use ``bdDetect.DeviceType.*`` instead. (#15772, @LeonarddeR).
- The ``bdDetect.DETECT_USB`` and ``bdDetect.DETECT_BLUETOOTH`` constants have been deprecated with no public replacement. (#15772, @LeonarddeR).
- Using ``gui.ExecAndPump`` is deprecated - please use ``systemUtils.ExecAndPump`` instead. (#15852, @lukaszgo1)
-


Expand Down

0 comments on commit 641c2fd

Please sign in to comment.