Skip to content

Commit cfefdb5

Browse files
authored
Merge 2143414 into 26439b1
2 parents 26439b1 + 2143414 commit cfefdb5

2 files changed

Lines changed: 99 additions & 98 deletions

File tree

source/bdDetect.py

Lines changed: 92 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
import itertools
1616
from collections import namedtuple, defaultdict, OrderedDict
1717
import threading
18-
18+
from concurrent.futures import ThreadPoolExecutor
19+
import queue
1920
import typing
2021
import hwPortUtils
2122
import braille
22-
import winKernel
2323
import winUser
2424
from logHandler import log
2525
import config
@@ -238,121 +238,122 @@ class Detector(object):
238238
This should only be used by the L{braille} module.
239239
"""
240240

241-
def __init__(self, usb=True, bluetooth=True, limitToDevices=None):
241+
def __init__(
242+
self,
243+
usb: bool = True,
244+
bluetooth: bool = True,
245+
limitToDevices: typing.Optional[typing.List[str]] = None
246+
):
242247
"""Constructor.
243248
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}.
249+
On an initialized instance, these initial arguments can be overridden by calling
250+
L{_queueBgScan} or L{rescan}.
245251
@param usb: Whether this instance should detect USB devices initially.
246-
@type usb: bool
247252
@param bluetooth: Whether this instance should detect Bluetooth devices initially.
248-
@type bluetooth: bool
249253
@param limitToDevices: Drivers to which detection should be limited initially.
250254
C{None} if no driver filtering should occur.
251255
"""
252-
self._BgScanApc = winKernel.PAPCFUNC(self._bgScan)
256+
self._executor = ThreadPoolExecutor(1)
253257
self._btDevsLock = threading.Lock()
254258
self._btDevs = 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+
self._queueBgScan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices)
266267

267268
@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.
269+
def _scanQueued(self) -> bool:
270+
return not self._executor._work_queue.empty()
271+
272+
def _queueBgScan(
273+
self,
274+
usb: bool = False,
275+
bluetooth: bool = False,
276+
limitToDevices: typing.Optional[typing.List[str]] = None
277+
):
278+
"""Queues a scan for devices.
281279
If a scan is already in progress, a new scan will be queued after the current scan.
282280
To explicitely cancel a scan in progress, use L{rescan}.
283281
@param usb: Whether USB devices should be detected for this and subsequent scans.
284-
@type usb: bool
285282
@param bluetooth: Whether Bluetooth devices should be detected for this and subsequent scans.
286-
@type bluetooth: bool
287283
@param limitToDevices: Drivers to which detection should be limited for this and subsequent scans.
288284
C{None} if no driver filtering should occur.
289285
"""
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)
286+
self._detectUsb = usb
287+
self._detectBluetooth = bluetooth
288+
self._limitToDevices = limitToDevices
289+
if not self._scanQueued:
290+
self._executor.submit(self._bgScan, usb, bluetooth, limitToDevices)
302291

303292
def _stopBgScan(self):
304293
"""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
308294
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
295+
# Cancel queued scans
296+
try:
297+
while self._scanQueued:
298+
workItem = self._executor._work_queue.get_nowait()
299+
workItem.future.cancel()
300+
except queue.Empty:
301+
pass
302+
303+
# C901 '_bgScan' is too complex
304+
# Note: when working on _bgScan, look for opportunities to simplify
305+
# and move logic out into smaller helper functions.
306+
def _bgScan( # noqa: C901
307+
self,
308+
detectUsb: bool,
309+
detectBluetooth: bool,
310+
limitToDevices: typing.Optional[typing.List[str]]
311+
):
312+
"""Performs the actual background scan.
313+
this function should be run on a background thread.
314+
@param usb: Whether USB devices should be detected for this particular scan.
315+
@param bluetooth: Whether Bluetooth devices should be detected for this particular scan.
316+
@param limitToDevices: Drivers to which detection should be limited for this scan.
317+
C{None} if no driver filtering should occur.
318+
"""
319+
# Clear the stop event before a scan is started.
320+
# Since a scan can take some time to complete, another thread can set the stop event to cancel it.
321+
self._stopEvent.clear()
322+
if detectUsb:
323+
if self._stopEvent.isSet():
324+
return
325+
for driver, match in getDriversForConnectedUsbDevices():
326+
if self._stopEvent.isSet():
327+
return
328+
if (limitToDevices and driver not in limitToDevices):
329+
continue
330+
if braille.handler.setDisplayByName(driver, detected=match):
331+
return
332+
if detectBluetooth:
333+
if self._stopEvent.isSet():
334+
return
335+
with self._btDevsLock:
336+
if self._btDevs is None:
337+
btDevs = list(getDriversForPossibleBluetoothDevices())
338+
# Cache Bluetooth devices for next time.
339+
btDevsCache = []
340+
else:
341+
btDevs = self._btDevs
342+
btDevsCache = btDevs
343+
for driver, match in btDevs:
344+
if self._stopEvent.isSet():
345+
return
346+
if (self._limitToDevices and driver not in limitToDevices):
347+
continue
348+
if btDevsCache is not btDevs:
349+
btDevsCache.append((driver, match))
350+
if braille.handler.setDisplayByName(driver, detected=match):
351+
return
352+
if self._stopEvent.isSet():
353+
return
354+
if btDevsCache is not btDevs:
355+
with self._btDevsLock:
356+
self._btDevs = btDevsCache
356357

357358
def rescan(self, usb=True, bluetooth=True, limitToDevices=None):
358359
"""Stop a current scan when in progress, and start scanning from scratch.
@@ -367,7 +368,7 @@ def rescan(self, usb=True, bluetooth=True, limitToDevices=None):
367368
with self._btDevsLock:
368369
# A Bluetooth com port or HID device might have been added.
369370
self._btDevs = None
370-
self._startBgScan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices)
371+
self._queueBgScan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices)
371372

372373
def handleWindowMessage(self, msg=None, wParam=None):
373374
if msg == winUser.WM_DEVICECHANGE and wParam == DBT_DEVNODES_CHANGED:
@@ -382,12 +383,13 @@ def pollBluetoothDevices(self):
382383
with self._btDevsLock:
383384
if not self._btDevs:
384385
return
385-
self._startBgScan(bluetooth=self._detectBluetooth, limitToDevices=self._limitToDevices)
386+
self._queueBgScan(bluetooth=self._detectBluetooth, limitToDevices=self._limitToDevices)
386387

387388
def terminate(self):
388389
appModuleHandler.post_appSwitch.unregister(self.pollBluetoothDevices)
389390
messageWindow.pre_handleWindowMessage.unregister(self.handleWindowMessage)
390391
self._stopBgScan()
392+
self._executor.shutdown(wait=False)
391393

392394

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

source/braille.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
@@ -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

@@ -2265,7 +2263,6 @@ 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:
22712268
self.setDisplayByName("noBraille", isFallback=True)
@@ -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

0 commit comments

Comments
 (0)