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

Set foreground object rather than desktop object when initializing object cache #14301

Merged
merged 18 commits into from Oct 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
91 changes: 83 additions & 8 deletions source/core.py
Expand Up @@ -419,6 +419,87 @@ def _handleNVDAModuleCleanupBeforeGUIExit():
brailleViewer.destroyBrailleViewer()


def _pollForForegroundHWND() -> int:
"""
@note: The foreground window should usually be fetched on the first try,
however it may take longer if Windows is taking a long time changing window focus.
Times out after 20 seconds (MAX_WAIT_TIME_SECS).
After timing out, NVDA will give up trying to start and exit.
"""
import ui
from utils.blockUntilConditionMet import blockUntilConditionMet
import winUser

# winUser.getForegroundWindow may return NULL in certain circumstances,
# such as when a window is losing activation.
# This should not remain the case for an extended period of time.
# If NVDA is taking longer than expected to fetch the foreground window, perform a warning.
# We must wait a long time after this warning to
# allow for braille / speech to be understood before exiting.
# Unfortunately we cannot block with a dialog as NVDA cannot read dialogs yet.
WARN_AFTER_SECS = 5
MAX_WAIT_TIME_SECS = 20

success, foregroundHWND = blockUntilConditionMet(
getValue=winUser.getForegroundWindow,
giveUpAfterSeconds=WARN_AFTER_SECS,
)
if success:
return foregroundHWND

exitAfterWarningSecs = MAX_WAIT_TIME_SECS - WARN_AFTER_SECS
ui.message(_(
# Translators: Message when NVDA is having an issue starting up
"NVDA is failing to fetch the foreground window. "
"If this continues, NVDA will quit starting in %d seconds." % exitAfterWarningSecs
))

success, foregroundHWND = blockUntilConditionMet(
getValue=winUser.getForegroundWindow,
giveUpAfterSeconds=exitAfterWarningSecs,
)
if success:
return foregroundHWND
log.critical("NVDA could not fetch the foreground window. Exiting NVDA.")
# Raising exception here causes core.main to exit and NVDA to fail to start
raise NVDANotInitializedError("Could not fetch foreground window")


def _initializeObjectCaches():
seanbudd marked this conversation as resolved.
Show resolved Hide resolved
"""
Caches the desktop object.
This may make information from the desktop window available on the lock screen,
however no known exploit is known for this.
2023.1 plans to ensure the desktopObject is available only when signed-in.

Also initializes other object caches to the foreground window.
feerrenrut marked this conversation as resolved.
Show resolved Hide resolved
Previously the object that was cached was the desktopObject,
however this may leak secure information to the lock screen.
The foreground window is set as the object cache,
as the foreground window would already be accessible on the lock screen (e.g. Magnifier).
It also is more intuitive that NVDA focuses the foreground window,
as opposed to the desktop object.

@note: The foreground window should usually be fetched on the first try,
however it may take longer if Windows is taking a long time changing window focus.
Times out after 20 seconds (MAX_WAIT_TIME_SECS in _pollForForegroundHWND).
After timing out, NVDA will give up trying to start and exit.
"""
import api
import NVDAObjects
import winUser

desktopObject = NVDAObjects.window.Window(windowHandle=winUser.getDesktopWindow())
api.setDesktopObject(desktopObject)

foregroundHWND = _pollForForegroundHWND()
foregroundObject = NVDAObjects.window.Window(windowHandle=foregroundHWND)
api.setForegroundObject(foregroundObject)
api.setFocusObject(foregroundObject)
api.setNavigatorObject(foregroundObject)
api.setMouseObject(foregroundObject)


def main():
"""NVDA's core main loop.
This initializes all modules such as audio, IAccessible, keyboard, mouse, and GUI.
Expand Down Expand Up @@ -667,14 +748,8 @@ def handlePowerStatusChange(self):
log.debug("Initializing garbageHandler")
garbageHandler.initialize()

import api
import winUser
import NVDAObjects.window
desktopObject=NVDAObjects.window.Window(windowHandle=winUser.getDesktopWindow())
api.setDesktopObject(desktopObject)
api.setFocusObject(desktopObject)
api.setNavigatorObject(desktopObject)
api.setMouseObject(desktopObject)
_initializeObjectCaches()

import JABHandler
log.debug("initializing Java Access Bridge support")
try:
Expand Down
50 changes: 50 additions & 0 deletions source/utils/blockUntilConditionMet.py
@@ -0,0 +1,50 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2020-2022 NV Access Limited
# This file may be used under the terms of the GNU General Public License, version 2 or later.
# For more details see: https://www.gnu.org/licenses/gpl-2.0.html


from time import (
perf_counter as timer,
sleep,
)
from typing import (
Any,
Callable,
Optional,
Tuple,
)


def blockUntilConditionMet(
getValue: Callable[[], Any],
giveUpAfterSeconds: float,
shouldStopEvaluator: Callable[[Any], bool] = lambda value: bool(value),
intervalBetweenSeconds: float = 0.1,
) -> Tuple[
bool, # Was evaluator met?
Optional[Any] # None or the value when the evaluator was met
]:
"""Repeatedly tries to get a value up until a time limit expires.
Tries are separated by a time interval.
The call will block until shouldStopEvaluator returns True when given the value,
the default evaluator just returns the value converted to a boolean.
@return: A tuple, (True, value) if evaluator condition is met, otherwise (False, None)
"""
minIntervalBetweenSeconds = 0.001
assert intervalBetweenSeconds > minIntervalBetweenSeconds
lastSleepTime = startTime = timer()
while (timer() - startTime) < giveUpAfterSeconds:
val = getValue()
if shouldStopEvaluator(val):
return True, val
timeElapsedSinceLastSleep = timer() - lastSleepTime
sleepTime = max(
# attempt to keep a regular period between polling
intervalBetweenSeconds - timeElapsedSinceLastSleep,
minIntervalBetweenSeconds,
)
sleep(sleepTime)
lastSleepTime = timer()

return False, None
10 changes: 8 additions & 2 deletions source/winUser.py
Expand Up @@ -18,6 +18,10 @@
#dll handles
user32=windll.user32

# rather than using the ctypes.c_void_p type, which may encourage attempting to dereference
# what may be an invalid or illegal pointer, we'll treat it as an opaque value.
HWNDVal = int

LRESULT=c_long
HCURSOR=c_long

Expand Down Expand Up @@ -458,7 +462,7 @@ def isDescendantWindow(parentHwnd,childHwnd):
return False


def getForegroundWindow() -> HWND:
def getForegroundWindow() -> HWNDVal:
return user32.GetForegroundWindow()

def setForegroundWindow(hwnd):
Expand All @@ -467,9 +471,11 @@ def setForegroundWindow(hwnd):
def setFocus(hwnd):
user32.SetFocus(hwnd)

def getDesktopWindow():

def getDesktopWindow() -> HWNDVal:
return user32.GetDesktopWindow()
seanbudd marked this conversation as resolved.
Show resolved Hide resolved


def getControlID(hwnd):
return user32.GetWindowLongW(hwnd,GWL_ID)

Expand Down
61 changes: 35 additions & 26 deletions tests/system/libraries/SystemTestSpy/blockUntilConditionMet.py
@@ -1,5 +1,5 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2020 NV Access Limited
# Copyright (C) 2020-2022 NV Access Limited
# This file may be used under the terms of the GNU General Public License, version 2 or later.
# For more details see: https://www.gnu.org/licenses/gpl-2.0.html

Expand All @@ -8,45 +8,54 @@
package. This enables sharing utility methods between the global plugin and other Robot Framework libraries.
"""

from time import sleep as _sleep
from time import perf_counter as _timer
from typing import Any, Callable, Optional, Tuple
from time import (
perf_counter as _timer,
sleep as _sleep,
)
from typing import (
Any,
Callable,
Optional,
Tuple,
)


def _blockUntilConditionMet(
getValue: Callable[[], Any],
giveUpAfterSeconds: float,
shouldStopEvaluator=lambda value: bool(value),
shouldStopEvaluator: Callable[[Any], bool] = lambda value: bool(value),
intervalBetweenSeconds: float = 0.1,
errorMessage: Optional[str] = None
) -> Tuple[
bool, # Was evaluator met?
Optional[Any] # None or the value when the evaluator was met
]:
"""Repeatedly tries to get a value up until a time limit expires. Tries are separated by
a time interval. The call will block until shouldStopEvaluator returns True when given the value,
"""Repeatedly tries to get a value up until a time limit expires.
Tries are separated by a time interval.
The call will block until shouldStopEvaluator returns True when given the value,
the default evaluator just returns the value converted to a boolean.
@param errorMessage Use 'None' to suppress the exception.
@return A tuple, (True, value) if evaluator condition is met, otherwise (False, None)
@raises RuntimeError if the time limit expires and an errorMessage is given.
@param errorMessage: Use 'None' to suppress the exception.
@return: A tuple, (True, value) if evaluator condition is met, otherwise (False, None)
@raises: AssertionError if the time limit expires and an errorMessage is given.
"""
assert callable(getValue)
assert callable(shouldStopEvaluator)
assert intervalBetweenSeconds > 0.001
SLEEP_TIME = intervalBetweenSeconds * 0.5
startTime = _timer()
lastRunTime = startTime
firstRun = True # ensure we start immediately
minIntervalBetweenSeconds = 0.001
assert intervalBetweenSeconds > minIntervalBetweenSeconds
lastSleepTime = startTime = _timer()
while (_timer() - startTime) < giveUpAfterSeconds:
if firstRun or (_timer() - lastRunTime) > intervalBetweenSeconds:
firstRun = False
lastRunTime = _timer()
val = getValue()
if shouldStopEvaluator(val):
return True, val
_sleep(SLEEP_TIME)
val = getValue()
if shouldStopEvaluator(val):
return True, val
timeElapsedSinceLastSleep = _timer() - lastSleepTime
sleepTime = max(
# attempt to keep a regular period between polling
intervalBetweenSeconds - timeElapsedSinceLastSleep,
minIntervalBetweenSeconds,
)
_sleep(sleepTime)
lastSleepTime = _timer()

else:
if errorMessage:
raise AssertionError(errorMessage)
return False, None
if errorMessage:
raise AssertionError(errorMessage)
return False, None