Skip to content

Commit c7522f8

Browse files
authored
Merge 40947b5 into 814f11e
2 parents 814f11e + 40947b5 commit c7522f8

6 files changed

Lines changed: 231 additions & 18 deletions

File tree

source/core.py

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,85 @@ def _handleNVDAModuleCleanupBeforeGUIExit():
419419
brailleViewer.destroyBrailleViewer()
420420

421421

422+
def _pollForForegroundHWND() -> int:
423+
"""
424+
@note: The foreground window should usually be fetched on the first try,
425+
however it may take longer if Windows is taking a long time changing window focus.
426+
Times out after 20 seconds (MAX_WAIT_TIME_SECS).
427+
After timing out, NVDA will give up trying to start and exit.
428+
"""
429+
import ui
430+
from utils.blockUntilConditionMet import blockUntilConditionMet
431+
import winUser
432+
433+
# winUser.getForegroundWindow may return NULL in certain circumstances,
434+
# such as when a window is losing activation.
435+
# This should not remain the case for an extended period of time.
436+
# If NVDA is taking longer than expected to fetch the foreground window, perform a warning.
437+
# We must wait a long time after this warning to
438+
# allow for braille / speech to be understood before exiting.
439+
# Unfortunately we cannot block with a dialog as NVDA cannot read dialogs yet.
440+
WARN_AFTER_SECS = 5
441+
MAX_WAIT_TIME_SECS = 20
442+
443+
success, foregroundHWND = blockUntilConditionMet(
444+
getValue=winUser.getForegroundWindow,
445+
giveUpAfterSeconds=WARN_AFTER_SECS,
446+
)
447+
if success:
448+
return foregroundHWND
449+
ui.message(_(
450+
# Translators: Message when NVDA is having an issue starting up
451+
"NVDA is failing to fetch the foreground window. "
452+
"If this continues, NVDA will quit starting in %d seconds." % (MAX_WAIT_TIME_SECS - WARN_AFTER_SECS)
453+
))
454+
455+
success, foregroundHWND = blockUntilConditionMet(
456+
getValue=winUser.getForegroundWindow,
457+
giveUpAfterSeconds=MAX_WAIT_TIME_SECS - WARN_AFTER_SECS,
458+
)
459+
if success:
460+
return foregroundHWND
461+
log.critical("NVDA could not fetch the foreground window. Exiting NVDA.")
462+
# Raising exception here causes core.main to exit and NVDA to fail to start
463+
raise NVDANotInitializedError("Could not fetch foreground window")
464+
465+
466+
def _initializeObjectCaches():
467+
"""
468+
Caches the desktop object.
469+
This may make information from the desktop window available on the lock screen,
470+
however no known exploit is known for this.
471+
2023.1 plans to ensure the desktopObject is available only when signed-in.
472+
473+
Also initializes other object caches to the foreground window.
474+
Previously the object that was cached was the desktopObject,
475+
however this may leak secure information to the lock screen.
476+
The foreground window is set as the object cache,
477+
as the foreground window would already be accessible on the lock screen (e.g. Magnifier).
478+
It also is more intuitive that NVDA focuses the foreground window,
479+
as opposed to the desktop object.
480+
481+
@note: The foreground window should usually be fetched on the first try,
482+
however it may take longer if Windows is taking a long time changing window focus.
483+
Times out after 20 seconds (MAX_WAIT_TIME_SECS in _pollForForegroundHWND).
484+
After timing out, NVDA will give up trying to start and exit.
485+
"""
486+
import api
487+
import NVDAObjects
488+
import winUser
489+
490+
desktopObject = NVDAObjects.window.Window(windowHandle=winUser.getDesktopWindow())
491+
api.setDesktopObject(desktopObject)
492+
493+
foregroundHWND = _pollForForegroundHWND()
494+
foregroundObject = NVDAObjects.window.Window(windowHandle=foregroundHWND)
495+
api.setForegroundObject(foregroundObject)
496+
api.setFocusObject(foregroundObject)
497+
api.setNavigatorObject(foregroundObject)
498+
api.setMouseObject(foregroundObject)
499+
500+
422501
def main():
423502
"""NVDA's core main loop.
424503
This initializes all modules such as audio, IAccessible, keyboard, mouse, and GUI.
@@ -667,14 +746,8 @@ def handlePowerStatusChange(self):
667746
log.debug("Initializing garbageHandler")
668747
garbageHandler.initialize()
669748

670-
import api
671-
import winUser
672-
import NVDAObjects.window
673-
desktopObject=NVDAObjects.window.Window(windowHandle=winUser.getDesktopWindow())
674-
api.setDesktopObject(desktopObject)
675-
api.setFocusObject(desktopObject)
676-
api.setNavigatorObject(desktopObject)
677-
api.setMouseObject(desktopObject)
749+
_initializeObjectCaches()
750+
678751
import JABHandler
679752
log.debug("initializing Java Access Bridge support")
680753
try:
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# A part of NonVisual Desktop Access (NVDA)
2+
# Copyright (C) 2020-2022 NV Access Limited
3+
# This file may be used under the terms of the GNU General Public License, version 2 or later.
4+
# For more details see: https://www.gnu.org/licenses/gpl-2.0.html
5+
6+
7+
from time import (
8+
perf_counter as timer,
9+
sleep,
10+
)
11+
from typing import (
12+
Any,
13+
Callable,
14+
Optional,
15+
Tuple,
16+
)
17+
18+
19+
def blockUntilConditionMet(
20+
getValue: Callable[[], Any],
21+
giveUpAfterSeconds: float,
22+
shouldStopEvaluator: Callable[[Any], bool] = lambda value: bool(value),
23+
intervalBetweenSeconds: float = 0.1,
24+
) -> Tuple[
25+
bool, # Was evaluator met?
26+
Optional[Any] # None or the value when the evaluator was met
27+
]:
28+
"""Repeatedly tries to get a value up until a time limit expires.
29+
Tries are separated by a time interval.
30+
The call will block until shouldStopEvaluator returns True when given the value,
31+
the default evaluator just returns the value converted to a boolean.
32+
@return: A tuple, (True, value) if evaluator condition is met, otherwise (False, None)
33+
"""
34+
assert intervalBetweenSeconds > 0.001
35+
SLEEP_TIME = intervalBetweenSeconds * 0.5
36+
startTime = timer()
37+
lastRunTime = startTime
38+
firstRun = True # ensure we start immediately
39+
while (timer() - startTime) < giveUpAfterSeconds:
40+
if firstRun or (timer() - lastRunTime) > intervalBetweenSeconds:
41+
firstRun = False
42+
lastRunTime = timer()
43+
val = getValue()
44+
if shouldStopEvaluator(val):
45+
return True, val
46+
sleep(SLEEP_TIME)
47+
48+
return False, None

source/winUser.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
#dll handles
1919
user32=windll.user32
2020

21+
# rather than using the ctypes.c_void_p type, which may encourage attempting to dereference
22+
# what may be an invalid or illegal pointer, we'll treat it as an opaque value.
23+
HWNDVal = int
24+
2125
LRESULT=c_long
2226
HCURSOR=c_long
2327

@@ -458,7 +462,7 @@ def isDescendantWindow(parentHwnd,childHwnd):
458462
return False
459463

460464

461-
def getForegroundWindow() -> HWND:
465+
def getForegroundWindow() -> HWNDVal:
462466
return user32.GetForegroundWindow()
463467

464468
def setForegroundWindow(hwnd):
@@ -467,9 +471,11 @@ def setForegroundWindow(hwnd):
467471
def setFocus(hwnd):
468472
user32.SetFocus(hwnd)
469473

470-
def getDesktopWindow():
474+
475+
def getDesktopWindow() -> HWNDVal:
471476
return user32.GetDesktopWindow()
472477

478+
473479
def getControlID(hwnd):
474480
return user32.GetWindowLongW(hwnd,GWL_ID)
475481

tests/system/libraries/SystemTestSpy/blockUntilConditionMet.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2020 NV Access Limited
2+
# Copyright (C) 2020-2022 NV Access Limited
33
# This file may be used under the terms of the GNU General Public License, version 2 or later.
44
# For more details see: https://www.gnu.org/licenses/gpl-2.0.html
55

@@ -8,15 +8,22 @@
88
package. This enables sharing utility methods between the global plugin and other Robot Framework libraries.
99
"""
1010

11-
from time import sleep as _sleep
12-
from time import perf_counter as _timer
13-
from typing import Any, Callable, Optional, Tuple
11+
from time import (
12+
perf_counter as _timer,
13+
sleep as _sleep,
14+
)
15+
from typing import (
16+
Any,
17+
Callable,
18+
Optional,
19+
Tuple,
20+
)
1421

1522

1623
def _blockUntilConditionMet(
1724
getValue: Callable[[], Any],
1825
giveUpAfterSeconds: float,
19-
shouldStopEvaluator=lambda value: bool(value),
26+
shouldStopEvaluator: Callable[[Any], bool] = lambda value: bool(value),
2027
intervalBetweenSeconds: float = 0.1,
2128
errorMessage: Optional[str] = None
2229
) -> Tuple[
@@ -26,9 +33,9 @@ def _blockUntilConditionMet(
2633
"""Repeatedly tries to get a value up until a time limit expires. Tries are separated by
2734
a time interval. The call will block until shouldStopEvaluator returns True when given the value,
2835
the default evaluator just returns the value converted to a boolean.
29-
@param errorMessage Use 'None' to suppress the exception.
30-
@return A tuple, (True, value) if evaluator condition is met, otherwise (False, None)
31-
@raises RuntimeError if the time limit expires and an errorMessage is given.
36+
@param errorMessage: Use 'None' to suppress the exception.
37+
@return: A tuple, (True, value) if evaluator condition is met, otherwise (False, None)
38+
@raises: AssertionError if the time limit expires and an errorMessage is given.
3239
"""
3340
assert callable(getValue)
3441
assert callable(shouldStopEvaluator)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# A part of NonVisual Desktop Access (NVDA)
2+
# This file is covered by the GNU General Public License.
3+
# See the file COPYING for more details.
4+
# Copyright (C) 2022 NV Access Limited.
5+
6+
"""Unit tests for the blockUntilConditionMet submodule.
7+
"""
8+
9+
import unittest
10+
from unittest.mock import patch
11+
12+
from utils import blockUntilConditionMet as _moduleUnderTest
13+
14+
15+
class _FakeTimer():
16+
"""
17+
Simulate sleeping and getting the current time,
18+
so that the module under test is not dependent on real world time.
19+
"""
20+
def __init__(self) -> None:
21+
self._fakeTime: float = 0.0
22+
23+
def sleep(self, secs: float) -> None:
24+
self._fakeTime += secs
25+
26+
def time(self):
27+
return self._fakeTime
28+
29+
30+
class Test_blockUntilConditionMet(unittest.TestCase):
31+
def setUp(self) -> None:
32+
self._timer = _FakeTimer()
33+
34+
def test_condition_succeeds_before_timeout(self):
35+
giveUpAfterSecs = 5
36+
pollInterval = 1
37+
startTime = self._timer.time()
38+
39+
def succeedJustBeforeTimeOut(currentTime: float):
40+
return (currentTime - startTime) > (giveUpAfterSecs - pollInterval)
41+
42+
with patch("time.sleep", new_callable=lambda: self._timer.sleep):
43+
with patch("time.perf_counter", new_callable=lambda: self._timer.time):
44+
success, endTimeOrNone = _moduleUnderTest.blockUntilConditionMet(
45+
getValue=self._timer.time,
46+
giveUpAfterSeconds=giveUpAfterSecs,
47+
shouldStopEvaluator=succeedJustBeforeTimeOut,
48+
intervalBetweenSeconds=pollInterval,
49+
)
50+
timeElapsed = self._timer.time() - startTime
51+
self.assertTrue(
52+
success,
53+
msg=f"Test condition failed unexpectedly due to timeout. Elapsed time: {timeElapsed:.2f}s"
54+
)
55+
self.assertGreater(giveUpAfterSecs, timeElapsed)
56+
57+
def test_condition_fails_on_timeout(self):
58+
giveUpAfterSecs = 5
59+
pollInterval = 1
60+
startTime = self._timer.time()
61+
62+
def succeedJustAfterTimeOut(currentTime: float):
63+
return (currentTime - startTime) > (giveUpAfterSecs + pollInterval)
64+
65+
with patch("time.sleep", new_callable=lambda: self._timer.sleep):
66+
with patch("time.perf_counter", new_callable=lambda: self._timer.time):
67+
success, endTimeOrNone = _moduleUnderTest.blockUntilConditionMet(
68+
getValue=self._timer.time,
69+
giveUpAfterSeconds=giveUpAfterSecs,
70+
shouldStopEvaluator=succeedJustAfterTimeOut,
71+
intervalBetweenSeconds=pollInterval,
72+
)
73+
timeElapsed = self._timer.time() - startTime
74+
self.assertFalse(
75+
success,
76+
msg=f"Test condition succeeded unexpectedly before timeout. Elapsed time: {timeElapsed:.2f}s"
77+
)
78+
self.assertGreater(timeElapsed, giveUpAfterSecs)

user_docs/en/changes.t2t

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This is a minor release to fix regressions with 2022.3.1 and address a security
1111

1212
== Bug Fixes ==
1313
- Fixes a regression from 2022.3.1 where certain functionality was disabled on secure screens. (#14286)
14+
- Fixes a regression from 2022.3.1 where certain functionality was disabled after sign-in, if NVDA started on the lock screen. (#14301)
1415
-
1516

1617

0 commit comments

Comments
 (0)