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.
1010See L{braille.BrailleDisplayDriver.isThreadSafe}.
1111"""
1212
13+ # "annotations" Needed to provide the inner type for weakref.ReferenceType.
14+ from __future__ import annotations
1315import sys
1416import ctypes
1517from ctypes import byref
1618from ctypes .wintypes import DWORD
1719from typing import Optional , Any , Union , Tuple , Callable
18-
20+ import weakref
1921import serial
2022from serial .win32 import OVERLAPPED , FILE_FLAG_OVERLAPPED , INVALID_HANDLE_VALUE , ERROR_IO_PENDING , COMMTIMEOUTS , CreateFile , SetCommTimeouts
2123import winKernel
2224import braille
2325from logHandler import log
2426import config
2527import 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
2943def _isDebug ():
3044 return config .conf ["debugLog" ]["hwIo" ]
3145
46+
3247class 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+
176217class 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 ()
0 commit comments