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+ from __future__ import annotations
1314import sys
1415import ctypes
1516from ctypes import byref
1617from ctypes .wintypes import DWORD
1718from typing import Optional , Any , Union , Tuple , Callable
18-
19+ import weakref
1920import serial
2021from serial .win32 import OVERLAPPED , FILE_FLAG_OVERLAPPED , INVALID_HANDLE_VALUE , ERROR_IO_PENDING , COMMTIMEOUTS , CreateFile , SetCommTimeouts
2122import winKernel
2223import braille
2324from logHandler import log
2425import config
2526import 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
2942def _isDebug ():
3043 return config .conf ["debugLog" ]["hwIo" ]
3144
45+
3246class 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+
176216class 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 ()
0 commit comments