Skip to content

Commit 400e746

Browse files
authored
Merge bfd9840 into 8b1b943
2 parents 8b1b943 + bfd9840 commit 400e746

7 files changed

Lines changed: 217 additions & 46 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: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
used, however for more advanced requirements these utilities can be used directly.
88
"""
99

10+
from __future__ import annotations
1011
import weakref
1112
import inspect
1213
from typing import (
@@ -41,11 +42,18 @@ class BoundMethodWeakref(Generic[HandlerT]):
4142
"""
4243
handlerKey: Tuple[int, int]
4344

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)
45+
def __init__(
46+
self,
47+
target: HandlerT,
48+
onDelete: Optional[Callable[[BoundMethodWeakref], None]] = None
49+
):
50+
if onDelete:
51+
def onRefDelete(weak):
52+
"""Calls onDelete for our BoundMethodWeakref when one of the individual weakrefs (instance or function) dies.
53+
"""
54+
onDelete(self)
55+
else:
56+
onRefDelete = None
4957
inst = target.__self__
5058
func = target.__func__
5159
self.weakInst = weakref.ref(inst, onRefDelete)

source/hwIo/base.py

Lines changed: 68 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,43 @@
1010
See L{braille.BrailleDisplayDriver.isThreadSafe}.
1111
"""
1212

13+
from __future__ import annotations
1314
import sys
1415
import ctypes
1516
from ctypes import byref
1617
from ctypes.wintypes import DWORD
1718
from typing import Optional, Any, Union, Tuple, Callable
18-
19+
import weakref
1920
import serial
2021
from serial.win32 import OVERLAPPED, FILE_FLAG_OVERLAPPED, INVALID_HANDLE_VALUE, ERROR_IO_PENDING, COMMTIMEOUTS, CreateFile, SetCommTimeouts
2122
import winKernel
2223
import braille
2324
from logHandler import log
2425
import config
2526
import time
27+
from .ioThread import IoThread
28+
# LPOVERLAPPED_COMPLETION_ROUTINE is imported for backwards compatibility.
29+
from .ioThread import LPOVERLAPPED_COMPLETION_ROUTINE # NOQA: F401
2630

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

2932
def _isDebug():
3033
return config.conf["debugLog"]["hwIo"]
3134

35+
3236
class IoBase(object):
3337
"""Base class for raw I/O.
3438
This watches for data of a specified size and calls a callback when it is received.
3539
"""
40+
_ioThreadRef: weakref.ReferenceType[IoThread]
3641

3742
def __init__(
3843
self,
3944
fileHandle: Union[ctypes.wintypes.HANDLE],
4045
onReceive: Callable[[bytes], None],
4146
writeFileHandle: Optional[ctypes.wintypes.HANDLE] = None,
4247
onReceiveSize: int = 1,
43-
onReadError: Optional[Callable[[int], bool]] = None
48+
onReadError: Optional[Callable[[int], bool]] = None,
49+
ioThread: Optional[IoThread] = None,
4450
):
4551
"""Constructor.
4652
@param fileHandle: A handle to an open I/O device opened for overlapped I/O.
@@ -50,8 +56,10 @@ def __init__(
5056
@param writeFileHandle: A handle to an open output device opened for overlapped I/O.
5157
@param onReceiveSize: The size (in bytes) of the data with which to call C{onReceive}.
5258
@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
59+
and returns True if the I/O loop should exit cleanly or False if an
60+
exception should be thrown
61+
@param ioThread: If provided, the I/O thread used for background reads.
62+
if C{None}, defaults to L{hwIo.bgThread}
5563
"""
5664
self._file = fileHandle
5765
self._onReceive = onReceive
@@ -61,18 +69,21 @@ def __init__(
6169
self._readBuf = ctypes.create_string_buffer(onReceiveSize)
6270
self._readOl = OVERLAPPED()
6371
self._recvEvt = winKernel.createEvent()
64-
self._ioDoneInst = LPOVERLAPPED_COMPLETION_ROUTINE(self._ioDone)
6572
self._writeOl = OVERLAPPED()
73+
if ioThread is None:
74+
from . import bgThread as ioThread
75+
self._ioThreadRef = weakref.ref(ioThread)
6676
# Do the initial read.
6777
self._initialRead()
6878

6979
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.
80+
"""Performs the initial background read by queuing it as an APC to the IO background thread
81+
provided at initialization time.
7382
"""
74-
from . import bgThread
75-
bgThread.queueAsApc(lambda param: self._asyncRead())
83+
ioThread = self._ioThreadRef()
84+
if not ioThread:
85+
raise RuntimeError("I/O thread is no longer available")
86+
ioThread.queueAsApc(self._asyncRead)
7687

7788
def waitForRead(self, timeout:Union[int, float]) -> bool:
7889
"""Wait for a chunk of data to be received and processed.
@@ -137,11 +148,20 @@ def __del__(self):
137148
if _isDebug():
138149
log.debugWarning("Couldn't delete object gracefully", exc_info=True)
139150

140-
def _asyncRead(self):
151+
def _asyncRead(self, param: Optional[int] = None):
152+
ioThread = self._ioThreadRef()
153+
if not ioThread:
154+
raise RuntimeError("I/O thread is no longer available")
141155
# Wait for _readSize bytes of data.
142156
# _ioDone will call onReceive once it is received.
143157
# 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)
158+
ctypes.windll.kernel32.ReadFileEx(
159+
self._file,
160+
self._readBuf,
161+
self._readSize,
162+
byref(self._readOl),
163+
ioThread.getCompletionRoutine(self._ioDone)
164+
)
145165

146166
def _ioDone(self, error, numberOfBytes: int, overlapped):
147167
if not self._onReceive:
@@ -173,21 +193,30 @@ def _notifyReceive(self, data: bytes):
173193
except:
174194
log.error("", exc_info=True)
175195

196+
176197
class Serial(IoBase):
177198
"""Raw I/O for serial devices.
178199
This extends pyserial to call a callback when data is received.
179200
"""
180201

181202
def __init__(
182-
self,
183-
*args,
184-
onReceive: Callable[[bytes], None],
185-
**kwargs):
203+
self,
204+
*args,
205+
onReceive: Callable[[bytes], None],
206+
onReadError: Optional[Callable[[int], bool]] = None,
207+
ioThread: Optional[IoThread] = None,
208+
**kwargs
209+
):
186210
"""Constructor.
187211
Pass the arguments you would normally pass to L{serial.Serial}.
188-
There is also one additional required keyword argument.
212+
There are also some additional keyword arguments ( the first is required).
189213
@param onReceive: A callable taking a byte of received data as its only argument.
190214
This callable can then call C{read} to get additional data if desired.
215+
@param onReadError: If provided, a callback that takes the error code for a failed read
216+
and returns True if the I/O loop should exit cleanly or False if an
217+
exception should be thrown
218+
@param ioThread: If provided, the I/O thread used for background reads.
219+
if C{None}, defaults to L{hwIo.bgThread}
191220
"""
192221
self._ser = None
193222
self.port = args[0] if len(args) >= 1 else kwargs["port"]
@@ -202,7 +231,12 @@ def __init__(
202231
self._origTimeout = self._ser.timeout
203232
# We don't want a timeout while we're waiting for data.
204233
self._setTimeout(None)
205-
super(Serial, self).__init__(self._ser._port_handle, onReceive)
234+
super().__init__(
235+
self._ser._port_handle,
236+
onReceive,
237+
onReadError=onReadError,
238+
ioThread=ioThread
239+
)
206240

207241
def read(self, size=1) -> bytes:
208242
data = self._ser.read(size)
@@ -257,16 +291,19 @@ def __init__(
257291
self, path: str, epIn: int, epOut: int,
258292
onReceive: Callable[[bytes], None],
259293
onReceiveSize: int = 1,
260-
onReadError: Optional[Callable[[int], bool]] = None
294+
onReadError: Optional[Callable[[int], bool]] = None,
295+
ioThread: Optional[IoThread] = None,
261296
):
262297
"""Constructor.
263298
@param path: The device path.
264299
@param epIn: The endpoint to read data from.
265300
@param epOut: The endpoint to write data to.
266301
@param onReceive: A callable taking a received input report as its only argument.
267302
@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.
303+
It takes an error code and returns True if the error has been handled,
304+
allowing the read loop to exit cleanly, or False if an exception should be thrown.
305+
@param ioThread: If provided, the I/O thread used for background reads.
306+
if C{None}, defaults to L{hwIo.bgThread}
270307
"""
271308
if _isDebug():
272309
log.debug("Opening device %s" % path)
@@ -284,9 +321,14 @@ def __init__(
284321
if _isDebug():
285322
log.debug("Open write handle failed: %s" % ctypes.WinError())
286323
raise ctypes.WinError()
287-
super(Bulk, self).__init__(readHandle, onReceive,
288-
writeFileHandle=writeHandle, onReceiveSize=onReceiveSize,
289-
onReadError=onReadError)
324+
super().__init__(
325+
readHandle,
326+
onReceive,
327+
writeFileHandle=writeHandle,
328+
onReceiveSize=onReceiveSize,
329+
onReadError=onReadError,
330+
ioThread=ioThread
331+
)
290332

291333
def close(self):
292334
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)