Skip to content

Commit 9e9742e

Browse files
authored
Merge 95ec538 into 9958841
2 parents 9958841 + 95ec538 commit 9e9742e

7 files changed

Lines changed: 253 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: 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: 88 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,53 @@
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, apcsWillBeStronglyReferenced
28+
import NVDAState
29+
30+
def __getattr__(attrName: str) -> Any:
31+
"""Module level `__getattr__` used to preserve backward compatibility."""
32+
if attrName == "LPOVERLAPPED_COMPLETION_ROUTINE" and NVDAState._allowDeprecatedAPI():
33+
log.warning(
34+
"Importing LPOVERLAPPED_COMPLETION_ROUTINE from hwIO.base is deprecated. "
35+
"Import LPOVERLAPPED_COMPLETION_ROUTINE from hwIo.ioThread instead."
36+
)
37+
from .ioThread import LPOVERLAPPED_COMPLETION_ROUTINE
38+
return LPOVERLAPPED_COMPLETION_ROUTINE
39+
raise AttributeError(f"module {repr(__name__)} has no attribute {repr(attrName)}")
2640

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

2942
def _isDebug():
3043
return config.conf["debugLog"]["hwIo"]
3144

45+
3246
class IoBase(object):
3347
"""Base class for raw I/O.
3448
This watches for data of a specified size and calls a callback when it is received.
3549
"""
50+
_ioThreadRef: weakref.ReferenceType[IoThread]
3651

3752
def __init__(
3853
self,
3954
fileHandle: Union[ctypes.wintypes.HANDLE],
4055
onReceive: Callable[[bytes], None],
4156
writeFileHandle: Optional[ctypes.wintypes.HANDLE] = None,
4257
onReceiveSize: int = 1,
43-
onReadError: Optional[Callable[[int], bool]] = None
58+
onReadError: Optional[Callable[[int], bool]] = None,
59+
ioThread: Optional[IoThread] = None,
4460
):
4561
"""Constructor.
4662
@param fileHandle: A handle to an open I/O device opened for overlapped I/O.
@@ -50,8 +66,10 @@ def __init__(
5066
@param writeFileHandle: A handle to an open output device opened for overlapped I/O.
5167
@param onReceiveSize: The size (in bytes) of the data with which to call C{onReceive}.
5268
@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
69+
and returns True if the I/O loop should exit cleanly or False if an
70+
exception should be thrown
71+
@param ioThread: If provided, the I/O thread used for background reads.
72+
if C{None}, defaults to L{hwIo.bgThread}
5573
"""
5674
self._file = fileHandle
5775
self._onReceive = onReceive
@@ -61,18 +79,24 @@ def __init__(
6179
self._readBuf = ctypes.create_string_buffer(onReceiveSize)
6280
self._readOl = OVERLAPPED()
6381
self._recvEvt = winKernel.createEvent()
64-
self._ioDoneInst = LPOVERLAPPED_COMPLETION_ROUTINE(self._ioDone)
6582
self._writeOl = OVERLAPPED()
83+
if ioThread is None:
84+
from . import bgThread as ioThread
85+
self._ioThreadRef = weakref.ref(ioThread)
6686
# Do the initial read.
6787
self._initialRead()
6888

6989
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.
90+
"""Performs the initial background read by queuing it as an APC to the IO background thread
91+
provided at initialization time.
7392
"""
74-
from . import bgThread
75-
bgThread.queueAsApc(lambda param: self._asyncRead())
93+
ioThread = self._ioThreadRef()
94+
if not ioThread:
95+
raise RuntimeError("I/O thread is no longer available")
96+
if apcsWillBeStronglyReferenced:
97+
ioThread.queueAsApc(self._asyncReadBackwardsCompat)
98+
else:
99+
ioThread.queueAsApc(self._asyncRead)
76100

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

140-
def _asyncRead(self):
164+
def _asyncRead(self, param: Optional[int] = None):
165+
ioThread = self._ioThreadRef()
166+
if not ioThread:
167+
raise RuntimeError("I/O thread is no longer available")
141168
# Wait for _readSize bytes of data.
142169
# _ioDone will call onReceive once it is received.
143170
# 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)
171+
ctypes.windll.kernel32.ReadFileEx(
172+
self._file,
173+
self._readBuf,
174+
self._readSize,
175+
byref(self._readOl),
176+
ioThread.getCompletionRoutine(self._ioDone)
177+
)
178+
179+
if apcsWillBeStronglyReferenced:
180+
def _asyncReadBackwardsCompat(self, param: Optional[int] = None):
181+
"""Backwards compatible wrapper around L{_asyncRead} that calls it without param.
182+
"""
183+
self._asyncRead()
145184

146185
def _ioDone(self, error, numberOfBytes: int, overlapped):
147186
if not self._onReceive:
@@ -173,21 +212,31 @@ def _notifyReceive(self, data: bytes):
173212
except:
174213
log.error("", exc_info=True)
175214

215+
176216
class Serial(IoBase):
177217
"""Raw I/O for serial devices.
178218
This extends pyserial to call a callback when data is received.
179219
"""
180220

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

207261
def read(self, size=1) -> bytes:
208262
data = self._ser.read(size)
@@ -257,16 +311,19 @@ def __init__(
257311
self, path: str, epIn: int, epOut: int,
258312
onReceive: Callable[[bytes], None],
259313
onReceiveSize: int = 1,
260-
onReadError: Optional[Callable[[int], bool]] = None
314+
onReadError: Optional[Callable[[int], bool]] = None,
315+
ioThread: Optional[IoThread] = None,
261316
):
262317
"""Constructor.
263318
@param path: The device path.
264319
@param epIn: The endpoint to read data from.
265320
@param epOut: The endpoint to write data to.
266321
@param onReceive: A callable taking a received input report as its only argument.
267322
@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.
323+
It takes an error code and returns True if the error has been handled,
324+
allowing the read loop to exit cleanly, or False if an exception should be thrown.
325+
@param ioThread: If provided, the I/O thread used for background reads.
326+
if C{None}, defaults to L{hwIo.bgThread}
270327
"""
271328
if _isDebug():
272329
log.debug("Opening device %s" % path)
@@ -284,9 +341,14 @@ def __init__(
284341
if _isDebug():
285342
log.debug("Open write handle failed: %s" % ctypes.WinError())
286343
raise ctypes.WinError()
287-
super(Bulk, self).__init__(readHandle, onReceive,
288-
writeFileHandle=writeHandle, onReceiveSize=onReceiveSize,
289-
onReadError=onReadError)
344+
super().__init__(
345+
readHandle,
346+
onReceive,
347+
writeFileHandle=writeHandle,
348+
onReceiveSize=onReceiveSize,
349+
onReadError=onReadError,
350+
ioThread=ioThread
351+
)
290352

291353
def close(self):
292354
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)