Skip to content

Commit 157546c

Browse files
authored
Merge 5a1a485 into bc3e97d
2 parents bc3e97d + 5a1a485 commit 157546c

7 files changed

Lines changed: 256 additions & 55 deletions

File tree

source/braille.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1983,7 +1983,6 @@ def __init__(self):
19831983

19841984
self.queuedWriteLock = threading.Lock()
19851985
self.ackTimerHandle = winKernel.createWaitableTimer()
1986-
self._ackTimeoutResetterApc = winKernel.PAPCFUNC(self._ackTimeoutResetter)
19871986

19881987
brailleViewer.postBrailleViewerToolToggledAction.register(self._onBrailleViewerChangedState)
19891988

@@ -2599,13 +2598,12 @@ def _bgThreadExecutor(self, param: int):
25992598
if self.display.receivesAckPackets:
26002599
self.display._awaitingAck = True
26012600
SECOND_TO_MS = 1000
2602-
winKernel.setWaitableTimer(
2601+
hwIo.bgThread.setWaitableTimer(
26032602
self.ackTimerHandle,
26042603
# Wait twice the display driver timeout for acknowledgement packets
26052604
# Note: timeout is in seconds whereas setWaitableTimer expects milliseconds
26062605
int(self.display.timeout * 2 * SECOND_TO_MS),
2607-
0,
2608-
self._ackTimeoutResetterApc
2606+
self._ackTimeoutResetter
26092607
)
26102608

26112609
def _ackTimeoutResetter(self, param: int):

source/extensionPoints/util.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
used, however for more advanced requirements these utilities can be used directly.
88
"""
99

10+
11+
# "annotations" Needed to reference BoundMethodWeakref in one of the init params of itself.
12+
from __future__ import annotations
1013
import weakref
1114
import inspect
1215
from typing import (
@@ -41,11 +44,18 @@ class BoundMethodWeakref(Generic[HandlerT]):
4144
"""
4245
handlerKey: Tuple[int, int]
4346

44-
def __init__(self, target: HandlerT, onDelete):
45-
def onRefDelete(weak):
46-
"""Calls onDelete for our BoundMethodWeakref when one of the individual weakrefs (instance or function) dies.
47-
"""
48-
onDelete(self)
47+
def __init__(
48+
self,
49+
target: HandlerT,
50+
onDelete: Optional[Callable[[BoundMethodWeakref], None]] = None
51+
):
52+
if onDelete:
53+
def onRefDelete(weak):
54+
"""Calls onDelete for our BoundMethodWeakref when one of the individual weakrefs (instance or function) dies.
55+
"""
56+
onDelete(self)
57+
else:
58+
onRefDelete = None
4959
inst = target.__self__
5060
func = target.__func__
5161
self.weakInst = weakref.ref(inst, onRefDelete)

source/hwIo/base.py

Lines changed: 89 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# A part of NonVisual Desktop Access (NVDA)
22
# This file is covered by the GNU General Public License.
33
# See the file COPYING for more details.
4-
# Copyright (C) 2015-2018 NV Access Limited, Babbage B.V.
4+
# Copyright (C) 2015-2023 NV Access Limited, Babbage B.V., Leonard de Ruijter
55

66

77
"""Raw input/output for braille displays via serial and HID.
@@ -10,37 +10,54 @@
1010
See L{braille.BrailleDisplayDriver.isThreadSafe}.
1111
"""
1212

13+
# "annotations" Needed to provide the inner type for weakref.ReferenceType.
14+
from __future__ import annotations
1315
import sys
1416
import ctypes
1517
from ctypes import byref
1618
from ctypes.wintypes import DWORD
1719
from typing import Optional, Any, Union, Tuple, Callable
18-
20+
import weakref
1921
import serial
2022
from serial.win32 import OVERLAPPED, FILE_FLAG_OVERLAPPED, INVALID_HANDLE_VALUE, ERROR_IO_PENDING, COMMTIMEOUTS, CreateFile, SetCommTimeouts
2123
import winKernel
2224
import braille
2325
from logHandler import log
2426
import config
2527
import time
28+
from .ioThread import IoThread, apcsWillBeStronglyReferenced
29+
import NVDAState
30+
31+
def __getattr__(attrName: str) -> Any:
32+
"""Module level `__getattr__` used to preserve backward compatibility."""
33+
if attrName == "LPOVERLAPPED_COMPLETION_ROUTINE" and NVDAState._allowDeprecatedAPI():
34+
log.warning(
35+
"Importing LPOVERLAPPED_COMPLETION_ROUTINE from hwIO.base is deprecated. "
36+
"Import LPOVERLAPPED_COMPLETION_ROUTINE from hwIo.ioThread instead."
37+
)
38+
from .ioThread import LPOVERLAPPED_COMPLETION_ROUTINE
39+
return LPOVERLAPPED_COMPLETION_ROUTINE
40+
raise AttributeError(f"module {repr(__name__)} has no attribute {repr(attrName)}")
2641

27-
LPOVERLAPPED_COMPLETION_ROUTINE = ctypes.WINFUNCTYPE(None, DWORD, DWORD, serial.win32.LPOVERLAPPED)
2842

2943
def _isDebug():
3044
return config.conf["debugLog"]["hwIo"]
3145

46+
3247
class IoBase(object):
3348
"""Base class for raw I/O.
3449
This watches for data of a specified size and calls a callback when it is received.
3550
"""
51+
_ioThreadRef: weakref.ReferenceType[IoThread]
3652

3753
def __init__(
3854
self,
3955
fileHandle: Union[ctypes.wintypes.HANDLE],
4056
onReceive: Callable[[bytes], None],
4157
writeFileHandle: Optional[ctypes.wintypes.HANDLE] = None,
4258
onReceiveSize: int = 1,
43-
onReadError: Optional[Callable[[int], bool]] = None
59+
onReadError: Optional[Callable[[int], bool]] = None,
60+
ioThread: Optional[IoThread] = None,
4461
):
4562
"""Constructor.
4663
@param fileHandle: A handle to an open I/O device opened for overlapped I/O.
@@ -50,8 +67,10 @@ def __init__(
5067
@param writeFileHandle: A handle to an open output device opened for overlapped I/O.
5168
@param onReceiveSize: The size (in bytes) of the data with which to call C{onReceive}.
5269
@param onReadError: If provided, a callback that takes the error code for a failed read
53-
and returns True if the I/O loop should exit cleanly or False if an
54-
exception should be thrown
70+
and returns True if the I/O loop should exit cleanly or False if an
71+
exception should be thrown
72+
@param ioThread: If provided, the I/O thread used for background reads.
73+
if C{None}, defaults to L{hwIo.bgThread}
5574
"""
5675
self._file = fileHandle
5776
self._onReceive = onReceive
@@ -61,18 +80,24 @@ def __init__(
6180
self._readBuf = ctypes.create_string_buffer(onReceiveSize)
6281
self._readOl = OVERLAPPED()
6382
self._recvEvt = winKernel.createEvent()
64-
self._ioDoneInst = LPOVERLAPPED_COMPLETION_ROUTINE(self._ioDone)
6583
self._writeOl = OVERLAPPED()
84+
if ioThread is None:
85+
from . import bgThread as ioThread
86+
self._ioThreadRef = weakref.ref(ioThread)
6687
# Do the initial read.
6788
self._initialRead()
6889

6990
def _initialRead(self):
70-
"""Performs the initial background read by queuing it as an APC to the IO background thread.
71-
This method is tied to the built-in i/o thread.
72-
It can be overridden to do an initial read on a different thread.
91+
"""Performs the initial background read by queuing it as an APC to the IO background thread
92+
provided at initialization time.
7393
"""
74-
from . import bgThread
75-
bgThread.queueAsApc(lambda param: self._asyncRead())
94+
ioThread = self._ioThreadRef()
95+
if not ioThread:
96+
raise RuntimeError("I/O thread is no longer available")
97+
if apcsWillBeStronglyReferenced:
98+
ioThread.queueAsApc(self._asyncReadBackwardsCompat)
99+
else:
100+
ioThread.queueAsApc(self._asyncRead)
76101

77102
def waitForRead(self, timeout:Union[int, float]) -> bool:
78103
"""Wait for a chunk of data to be received and processed.
@@ -137,11 +162,26 @@ def __del__(self):
137162
if _isDebug():
138163
log.debugWarning("Couldn't delete object gracefully", exc_info=True)
139164

140-
def _asyncRead(self):
165+
def _asyncRead(self, param: Optional[int] = None):
166+
ioThread = self._ioThreadRef()
167+
if not ioThread:
168+
raise RuntimeError("I/O thread is no longer available")
141169
# Wait for _readSize bytes of data.
142170
# _ioDone will call onReceive once it is received.
143171
# onReceive can then optionally read additional bytes if it knows these are coming.
144-
ctypes.windll.kernel32.ReadFileEx(self._file, self._readBuf, self._readSize, byref(self._readOl), self._ioDoneInst)
172+
ctypes.windll.kernel32.ReadFileEx(
173+
self._file,
174+
self._readBuf,
175+
self._readSize,
176+
byref(self._readOl),
177+
ioThread.getCompletionRoutine(self._ioDone)
178+
)
179+
180+
if apcsWillBeStronglyReferenced:
181+
def _asyncReadBackwardsCompat(self, param: Optional[int] = None):
182+
"""Backwards compatible wrapper around L{_asyncRead} that calls it without param.
183+
"""
184+
self._asyncRead()
145185

146186
def _ioDone(self, error, numberOfBytes: int, overlapped):
147187
if not self._onReceive:
@@ -173,21 +213,31 @@ def _notifyReceive(self, data: bytes):
173213
except:
174214
log.error("", exc_info=True)
175215

216+
176217
class Serial(IoBase):
177218
"""Raw I/O for serial devices.
178219
This extends pyserial to call a callback when data is received.
179220
"""
180221

181222
def __init__(
182-
self,
183-
*args,
184-
onReceive: Callable[[bytes], None],
185-
**kwargs):
223+
self,
224+
*args,
225+
onReceive: Callable[[bytes], None],
226+
onReadError: Optional[Callable[[int], bool]] = None,
227+
ioThread: Optional[IoThread] = None,
228+
**kwargs
229+
):
186230
"""Constructor.
187231
Pass the arguments you would normally pass to L{serial.Serial}.
188-
There is also one additional required keyword argument.
232+
There are also some additional keyword arguments (the first, onReceive, is required).
233+
189234
@param onReceive: A callable taking a byte of received data as its only argument.
190235
This callable can then call C{read} to get additional data if desired.
236+
@param onReadError: If provided, a callback that takes the error code for a failed read
237+
and returns True if the I/O loop should exit cleanly or False if an
238+
exception should be thrown
239+
@param ioThread: If provided, the I/O thread used for background reads.
240+
if C{None}, defaults to L{hwIo.bgThread}
191241
"""
192242
self._ser = None
193243
self.port = args[0] if len(args) >= 1 else kwargs["port"]
@@ -202,7 +252,12 @@ def __init__(
202252
self._origTimeout = self._ser.timeout
203253
# We don't want a timeout while we're waiting for data.
204254
self._setTimeout(None)
205-
super(Serial, self).__init__(self._ser._port_handle, onReceive)
255+
super().__init__(
256+
self._ser._port_handle,
257+
onReceive,
258+
onReadError=onReadError,
259+
ioThread=ioThread
260+
)
206261

207262
def read(self, size=1) -> bytes:
208263
data = self._ser.read(size)
@@ -257,16 +312,19 @@ def __init__(
257312
self, path: str, epIn: int, epOut: int,
258313
onReceive: Callable[[bytes], None],
259314
onReceiveSize: int = 1,
260-
onReadError: Optional[Callable[[int], bool]] = None
315+
onReadError: Optional[Callable[[int], bool]] = None,
316+
ioThread: Optional[IoThread] = None,
261317
):
262318
"""Constructor.
263319
@param path: The device path.
264320
@param epIn: The endpoint to read data from.
265321
@param epOut: The endpoint to write data to.
266322
@param onReceive: A callable taking a received input report as its only argument.
267323
@param onReadError: An optional callable that handles read errors.
268-
It takes an error code and returns True if the error has been handled,
269-
allowing the read loop to exit cleanly, or False if an exception should be thrown.
324+
It takes an error code and returns True if the error has been handled,
325+
allowing the read loop to exit cleanly, or False if an exception should be thrown.
326+
@param ioThread: If provided, the I/O thread used for background reads.
327+
if C{None}, defaults to L{hwIo.bgThread}
270328
"""
271329
if _isDebug():
272330
log.debug("Opening device %s" % path)
@@ -284,9 +342,14 @@ def __init__(
284342
if _isDebug():
285343
log.debug("Open write handle failed: %s" % ctypes.WinError())
286344
raise ctypes.WinError()
287-
super(Bulk, self).__init__(readHandle, onReceive,
288-
writeFileHandle=writeHandle, onReceiveSize=onReceiveSize,
289-
onReadError=onReadError)
345+
super().__init__(
346+
readHandle,
347+
onReceive,
348+
writeFileHandle=writeHandle,
349+
onReceiveSize=onReceiveSize,
350+
onReadError=onReadError,
351+
ioThread=ioThread
352+
)
290353

291354
def close(self):
292355
super(Bulk, self).close()

source/hwIo/hid.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
import ctypes
1313
from ctypes import byref
1414
from ctypes.wintypes import USHORT
15-
from typing import Tuple, Callable
15+
from typing import Tuple, Callable, Optional
16+
from .ioThread import IoThread
1617

1718
from serial.win32 import FILE_FLAG_OVERLAPPED, INVALID_HANDLE_VALUE, CreateFile
1819
import winKernel
@@ -126,12 +127,24 @@ class Hid(IoBase):
126127
"""
127128
_featureSize: int
128129

129-
def __init__(self, path: str, onReceive: Callable[[bytes], None], exclusive: bool = True):
130+
def __init__(
131+
self,
132+
path: str,
133+
onReceive: Callable[[bytes], None],
134+
exclusive: bool = True,
135+
onReadError: Optional[Callable[[int], bool]] = None,
136+
ioThread: Optional[IoThread] = None,
137+
):
130138
"""Constructor.
131139
@param path: The device path.
132140
This can be retrieved using L{hwPortUtils.listHidDevices}.
133141
@param onReceive: A callable taking a received input report as its only argument.
134142
@param exclusive: Whether to block other application's access to this device.
143+
@param onReadError: An optional callable that handles read errors.
144+
It takes an error code and returns True if the error has been handled,
145+
allowing the read loop to exit cleanly, or False if an exception should be thrown.
146+
@param ioThread: If provided, the I/O thread used for background reads.
147+
if C{None}, defaults to L{hwIo.bgThread}
135148
"""
136149
if _isDebug():
137150
log.debug("Opening device %s" % path)
@@ -168,7 +181,11 @@ def __init__(self, path: str, onReceive: Callable[[bytes], None], exclusive: boo
168181
self._readSize = caps.InputReportByteLength
169182
# Reading any less than caps.InputReportByteLength is an error.
170183
super().__init__(
171-
handle, onReceive, onReceiveSize=caps.InputReportByteLength
184+
handle,
185+
onReceive,
186+
onReceiveSize=caps.InputReportByteLength,
187+
onReadError=onReadError,
188+
ioThread=ioThread
172189
)
173190

174191
@property

0 commit comments

Comments
 (0)