Skip to content

Commit 8a08c1d

Browse files
authored
Merge aca2479 into 2dbdb2b
2 parents 2dbdb2b + aca2479 commit 8a08c1d

3 files changed

Lines changed: 106 additions & 106 deletions

File tree

source/bdDetect.py

Lines changed: 91 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@
1515
import itertools
1616
from collections import namedtuple, defaultdict, OrderedDict
1717
import threading
18-
18+
from concurrent.futures import ThreadPoolExecutor, Future
1919
import typing
2020
import hwPortUtils
2121
import braille
22-
import winKernel
2322
import winUser
2423
from logHandler import log
2524
import config
@@ -238,121 +237,119 @@ class Detector(object):
238237
This should only be used by the L{braille} module.
239238
"""
240239

241-
def __init__(self, usb=True, bluetooth=True, limitToDevices=None):
240+
def __init__(
241+
self,
242+
usb: bool = True,
243+
bluetooth: bool = True,
244+
limitToDevices: typing.Optional[typing.List[str]] = None
245+
):
242246
"""Constructor.
243247
The keyword arguments initialize the detector in a particular state.
244-
On an initialized instance, these initial arguments can be overridden by calling L{_startBgScan} or L{rescan}.
248+
On an initialized instance, these initial arguments can be overridden by calling
249+
L{_queueBgScan} or L{rescan}.
245250
@param usb: Whether this instance should detect USB devices initially.
246-
@type usb: bool
247251
@param bluetooth: Whether this instance should detect Bluetooth devices initially.
248-
@type bluetooth: bool
249252
@param limitToDevices: Drivers to which detection should be limited initially.
250253
C{None} if no driver filtering should occur.
251254
"""
252-
self._BgScanApc = winKernel.PAPCFUNC(self._bgScan)
255+
self._executor = ThreadPoolExecutor(1)
253256
self._btDevsLock = threading.Lock()
254-
self._btDevs = None
257+
self._btDevs: typing.Optional[typing.Tuple[str, DeviceMatch]] = None
258+
self._queuedFuture: typing.Optional[Future] = None
255259
messageWindow.pre_handleWindowMessage.register(self.handleWindowMessage)
256260
appModuleHandler.post_appSwitch.register(self.pollBluetoothDevices)
257261
self._stopEvent = threading.Event()
258-
self._queuedScanLock = threading.Lock()
259-
self._scanQueued = False
260262
self._detectUsb = usb
261263
self._detectBluetooth = bluetooth
262264
self._limitToDevices = limitToDevices
263-
self._runningApcLock = threading.Lock()
264265
# Perform initial scan.
265-
self._startBgScan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices)
266-
267-
@property
268-
def _scanQueuedSafe(self):
269-
"""Returns L{_scanQueued} in a thread safe way by using L{_queuedScanLock}."""
270-
with self._queuedScanLock:
271-
return self._scanQueued
272-
273-
@_scanQueuedSafe.setter
274-
def _scanQueuedSafe(self, state):
275-
"""Sets L{_scanQueued} in a thread safe way by using L{_queuedScanLock}."""
276-
with self._queuedScanLock:
277-
self._scanQueued = state
278-
279-
def _startBgScan(self, usb=False, bluetooth=False, limitToDevices=None):
280-
"""Starts a scan for devices.
266+
self._queueBgScan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices)
267+
268+
def _queueBgScan(
269+
self,
270+
usb: bool = False,
271+
bluetooth: bool = False,
272+
limitToDevices: typing.Optional[typing.List[str]] = None
273+
):
274+
"""Queues a scan for devices.
281275
If a scan is already in progress, a new scan will be queued after the current scan.
282276
To explicitely cancel a scan in progress, use L{rescan}.
283277
@param usb: Whether USB devices should be detected for this and subsequent scans.
284-
@type usb: bool
285278
@param bluetooth: Whether Bluetooth devices should be detected for this and subsequent scans.
286-
@type bluetooth: bool
287279
@param limitToDevices: Drivers to which detection should be limited for this and subsequent scans.
288280
C{None} if no driver filtering should occur.
289281
"""
290-
with self._queuedScanLock:
291-
self._detectUsb = usb
292-
self._detectBluetooth = bluetooth
293-
self._limitToDevices = limitToDevices
294-
if not self._scanQueued:
295-
self._scanQueued = True
296-
if self._runningApcLock.locked():
297-
# There's currently a scan in progress.
298-
# Since the scan is embeded in a loop, it will automatically do another scan,
299-
# unless a display has been found.
300-
return
301-
braille._BgThread.queueApc(self._BgScanApc)
282+
self._detectUsb = usb
283+
self._detectBluetooth = bluetooth
284+
self._limitToDevices = limitToDevices
285+
if self._queuedFuture:
286+
# This will cancel a queued scan (i.e. not the currently running scan, if any)
287+
# If this future belongs to a scan that is currently running or finished, this does nothing.
288+
self._queuedFuture.cancel()
289+
self._queuedFuture = self._executor.submit(self._bgScan, usb, bluetooth, limitToDevices)
302290

303291
def _stopBgScan(self):
304292
"""Stops the current scan as soon as possible and prevents a queued scan to start."""
305-
if not self._runningApcLock.locked():
306-
# No scan to stop
307-
return
308293
self._stopEvent.set()
309-
self._scanQueuedSafe = False
310-
311-
def _bgScan(self, param):
312-
if self._runningApcLock.locked():
313-
log.debugWarning("Braille display detection background scan APC executed while one is already running")
314-
return
315-
with self._runningApcLock:
316-
while self._scanQueuedSafe:
317-
# Clear the stop event before a scan is started.
318-
# Since a scan can take some time to complete, another thread can set the stop event to cancel it.
319-
self._stopEvent.clear()
320-
with self._queuedScanLock:
321-
self._scanQueued = False
322-
detectUsb = self._detectUsb
323-
detectBluetooth = self._detectBluetooth
324-
limitToDevices = self._limitToDevices
325-
if detectUsb:
326-
if self._stopEvent.isSet():
327-
continue
328-
for driver, match in getDriversForConnectedUsbDevices():
329-
if self._stopEvent.isSet() or (self._limitToDevices and driver not in self._limitToDevices):
330-
continue
331-
if braille.handler.setDisplayByName(driver, detected=match):
332-
return
333-
if detectBluetooth:
334-
if self._stopEvent.isSet():
335-
continue
336-
with self._btDevsLock:
337-
if self._btDevs is None:
338-
btDevs = list(getDriversForPossibleBluetoothDevices())
339-
# Cache Bluetooth devices for next time.
340-
btDevsCache = []
341-
else:
342-
btDevs = self._btDevs
343-
btDevsCache = btDevs
344-
for driver, match in btDevs:
345-
if self._stopEvent.isSet() or (self._limitToDevices and driver not in self._limitToDevices):
346-
continue
347-
if btDevsCache is not btDevs:
348-
btDevsCache.append((driver, match))
349-
if braille.handler.setDisplayByName(driver, detected=match):
350-
return
351-
if self._stopEvent.isSet():
352-
continue
353-
if btDevsCache is not btDevs:
354-
with self._btDevsLock:
355-
self._btDevs = btDevsCache
294+
if self._queuedFuture:
295+
# This will cancel a queued scan (i.e. not the currently running scan, if any)
296+
# If this future belongs to a scan that is currently running or finished, this does nothing.
297+
self._queuedFuture.cancel()
298+
299+
# C901 '_bgScan' is too complex
300+
# Note: when working on _bgScan, look for opportunities to simplify
301+
# and move logic out into smaller helper functions.
302+
def _bgScan( # noqa: C901
303+
self,
304+
detectUsb: bool,
305+
detectBluetooth: bool,
306+
limitToDevices: typing.Optional[typing.List[str]]
307+
):
308+
"""Performs the actual background scan.
309+
this function should be run on a background thread.
310+
@param usb: Whether USB devices should be detected for this particular scan.
311+
@param bluetooth: Whether Bluetooth devices should be detected for this particular scan.
312+
@param limitToDevices: Drivers to which detection should be limited for this scan.
313+
C{None} if no driver filtering should occur.
314+
"""
315+
# Clear the stop event before a scan is started.
316+
# Since a scan can take some time to complete, another thread can set the stop event to cancel it.
317+
self._stopEvent.clear()
318+
if detectUsb:
319+
if self._stopEvent.isSet():
320+
return
321+
for driver, match in getDriversForConnectedUsbDevices():
322+
if self._stopEvent.isSet():
323+
return
324+
if (limitToDevices and driver not in limitToDevices):
325+
continue
326+
if braille.handler.setDisplayByName(driver, detected=match):
327+
return
328+
if detectBluetooth:
329+
if self._stopEvent.isSet():
330+
return
331+
with self._btDevsLock:
332+
if self._btDevs is None:
333+
btDevs = list(getDriversForPossibleBluetoothDevices())
334+
# Cache Bluetooth devices for next time.
335+
btDevsCache = []
336+
else:
337+
btDevs = self._btDevs
338+
btDevsCache = btDevs
339+
for driver, match in btDevs:
340+
if self._stopEvent.isSet():
341+
return
342+
if (self._limitToDevices and driver not in limitToDevices):
343+
continue
344+
if btDevsCache is not btDevs:
345+
btDevsCache.append((driver, match))
346+
if braille.handler.setDisplayByName(driver, detected=match):
347+
return
348+
if self._stopEvent.isSet():
349+
return
350+
if btDevsCache is not btDevs:
351+
with self._btDevsLock:
352+
self._btDevs = btDevsCache
356353

357354
def rescan(self, usb=True, bluetooth=True, limitToDevices=None):
358355
"""Stop a current scan when in progress, and start scanning from scratch.
@@ -367,7 +364,7 @@ def rescan(self, usb=True, bluetooth=True, limitToDevices=None):
367364
with self._btDevsLock:
368365
# A Bluetooth com port or HID device might have been added.
369366
self._btDevs = None
370-
self._startBgScan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices)
367+
self._queueBgScan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices)
371368

372369
def handleWindowMessage(self, msg=None, wParam=None):
373370
if msg == winUser.WM_DEVICECHANGE and wParam == DBT_DEVNODES_CHANGED:
@@ -382,12 +379,13 @@ def pollBluetoothDevices(self):
382379
with self._btDevsLock:
383380
if not self._btDevs:
384381
return
385-
self._startBgScan(bluetooth=self._detectBluetooth, limitToDevices=self._limitToDevices)
382+
self._queueBgScan(bluetooth=self._detectBluetooth, limitToDevices=self._limitToDevices)
386383

387384
def terminate(self):
388385
appModuleHandler.post_appSwitch.unregister(self.pollBluetoothDevices)
389386
messageWindow.pre_handleWindowMessage.unregister(self.handleWindowMessage)
390387
self._stopBgScan()
388+
self._executor.shutdown(wait=False)
391389

392390

393391
def getConnectedUsbDevicesForDriver(driver) -> typing.Iterator[DeviceMatch]:

source/braille.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ def getDisplayList(excludeNegativeChecks=True) -> List[Tuple[str, str]]:
387387
continue
388388
try:
389389
if not excludeNegativeChecks or display.check():
390-
if display.name == "noBraille":
390+
if display.name == brailleDisplayDrivers.noBraille.BrailleDisplayDriver.name:
391391
lastDisplay = (display.name, display.description)
392392
else:
393393
displayList.append((display.name, display.description))
@@ -1788,7 +1788,6 @@ def __init__(self):
17881788
brailleViewer.postBrailleViewerToolToggledAction.register(self._onBrailleViewerChangedState)
17891789

17901790
def terminate(self):
1791-
bgThreadStopTimeout = 2.5 if self._detectionEnabled else None
17921791
self._disableDetection()
17931792
if self._messageCallLater:
17941793
self._messageCallLater.Stop()
@@ -1800,7 +1799,7 @@ def terminate(self):
18001799
if self.display:
18011800
self.display.terminate()
18021801
self.display = None
1803-
_BgThread.stop(timeout=bgThreadStopTimeout)
1802+
_BgThread.stop()
18041803
louisHelper.terminate()
18051804

18061805
def getTether(self):
@@ -1890,9 +1889,8 @@ def setDisplayByName( # noqa: C901
18901889
# Re-initialize with supported kwargs.
18911890
extensionPoints.callWithSupportedKwargs(newDisplay.__init__, **kwargs)
18921891
else:
1893-
if newDisplay.isThreadSafe and not detected:
1892+
if newDisplay.isThreadSafe:
18941893
# Start the thread if it wasn't already.
1895-
# Auto detection implies the thread is already started.
18961894
_BgThread.start()
18971895
try:
18981896
newDisplay = newDisplay(**kwargs)
@@ -1930,7 +1928,7 @@ def setDisplayByName( # noqa: C901
19301928
log.error("Error initializing display driver %s for kwargs %r"%(name,kwargs), exc_info=True)
19311929
elif bdDetect._isDebug():
19321930
log.debugWarning("Couldn't initialize display driver for kwargs %r"%(kwargs,), exc_info=True)
1933-
self.setDisplayByName("noBraille", isFallback=True)
1931+
self.setDisplayByName(brailleDisplayDrivers.noBraille.BrailleDisplayDriver.name, isFallback=True)
19341932
return False
19351933

19361934
def _onBrailleViewerChangedState(self, created):
@@ -1950,7 +1948,7 @@ def _updateDisplay(self):
19501948
blinkRate = config.conf["braille"]["cursorBlinkRate"]
19511949
if cursorShouldBlink and blinkRate:
19521950
self._cursorBlinkTimer = gui.NonReEntrantTimer(self._blink)
1953-
# This is called from the background thread when a display is auto detected.
1951+
# This is called from another thread when a display is auto detected.
19541952
# Make sure we start the blink timer from the main thread to avoid wx assertions
19551953
wx.CallAfter(self._cursorBlinkTimer.Start,blinkRate)
19561954

@@ -2255,7 +2253,7 @@ def handleDisplayUnavailable(self):
22552253
"""
22562254
log.error("Braille display unavailable. Disabling", exc_info=True)
22572255
self._detectionEnabled = config.conf["braille"]["display"] == AUTO_DISPLAY_NAME
2258-
self.setDisplayByName("noBraille", isFallback=True)
2256+
self.setDisplayByName(brailleDisplayDrivers.noBraille.BrailleDisplayDriver.name, isFallback=True)
22592257

22602258
def _enableDetection(self, usb=True, bluetooth=True, keepCurrentDisplay=False, limitToDevices=None):
22612259
"""Enables automatic detection of braille displays.
@@ -2265,10 +2263,9 @@ def _enableDetection(self, usb=True, bluetooth=True, keepCurrentDisplay=False, l
22652263
if self._detectionEnabled and self._detector:
22662264
self._detector.rescan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices)
22672265
return
2268-
_BgThread.start()
22692266
config.conf["braille"]["display"] = AUTO_DISPLAY_NAME
22702267
if not keepCurrentDisplay:
2271-
self.setDisplayByName("noBraille", isFallback=True)
2268+
self.setDisplayByName(brailleDisplayDrivers.noBraille.BrailleDisplayDriver.name, isFallback=True)
22722269
self._detector = bdDetect.Detector(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices)
22732270
self._detectionEnabled = True
22742271

@@ -2305,10 +2302,12 @@ def start(cls):
23052302

23062303
@classmethod
23072304
def queueApc(cls, func, param=0):
2305+
# Ensure the thread is running
2306+
cls.start()
23082307
ctypes.windll.kernel32.QueueUserAPC(func, cls.handle, param)
23092308

23102309
@classmethod
2311-
def stop(cls, timeout=None):
2310+
def stop(cls):
23122311
if not cls.thread:
23132312
return
23142313
cls.exit = True
@@ -2318,7 +2317,7 @@ def stop(cls, timeout=None):
23182317
cls.ackTimerHandle = None
23192318
# Wake up the thread. It will exit when it sees exit is True.
23202319
cls.queueApc(cls.executor)
2321-
cls.thread.join(timeout)
2320+
cls.thread.join()
23222321
cls.exit = False
23232322
winKernel.closeHandle(cls.handle)
23242323
cls.handle = None
@@ -2506,7 +2505,7 @@ def terminate(self):
25062505
self.display([0] * self.numCells)
25072506
except Exception:
25082507
# The display driver seems to be failing, but we're terminating anyway, so just ignore it.
2509-
log.error(f"Display driver {self} failed to display while terminating.", exc_info=True)
2508+
log.warning(f"Display driver {self} failed to display while terminating.", exc_info=True)
25102509

25112510
#: typing information for autoproperty _get_numCells
25122511
numCells: int

source/setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ def getRecursiveDataFiles(dest,source,excludes=()):
202202
"winxptheme",
203203
# numpy is an optional dependency of comtypes but we don't require it.
204204
"numpy",
205+
# multiprocessing isn't going to work in a frozen environment
206+
"multiprocessing",
207+
"concurrent.futures.process",
205208
],
206209
"packages": [
207210
"NVDAObjects",

0 commit comments

Comments
 (0)