Source code for rpyc.core.stream

"""
An abstraction layer over OS-dependent file-like objects, that provides a
consistent view of a *duplex byte stream*.
"""
import sys
import io
import os
import socket
import errno
import threading
from rpyc.lib import safe_import, Timeout, worker, socket_backoff_connect
from rpyc.lib.compat import poll, select_error, BYTES_LITERAL, get_exc_errno
from rpyc.core.consts import STREAM_CHUNK
win32file = safe_import("win32file")
win32pipe = safe_import("win32pipe")
win32event = safe_import("win32event")
pywintypes = safe_import("pywintypes")
fcntl = safe_import("fcntl")
ssl = safe_import("ssl")


retry_errnos = (errno.EAGAIN, errno.EWOULDBLOCK)


if os.name == 'nt':
    def get_inheritable(channel):
        return os.get_handle_inheritable(channel.fileno())

    def set_inheritable(channel, inheritable):
        os.set_handle_inheritable(channel.fileno(), inheritable)
else:
    def get_inheritable(channel):
        return os.get_inheritable(channel.fileno())

    def set_inheritable(channel, inheritable):
        os.set_inheritable(channel.fileno(), inheritable)


[docs] class Stream: """Base Stream""" __slots__ = ('__socket_r', '__socket_w', '__predicate', '__cond', '__reading', '__polling', '__read_pause_depth', '__listening') def __init__(self): self.__cond = threading.Condition(threading.Lock()) self.__polling = False self.__reading = False self.__read_pause_depth = 0 self.__listening = False self.__predicate = None self.__socket_w, self.__socket_r = socket.socketpair() self.__socket_r.set_inheritable(False) self.__socket_w.set_inheritable(False) if hasattr(socket, 'SHUT_WR'): self.__socket_r.shutdown(socket.SHUT_WR) if hasattr(socket, 'SHUT_RD'): self.__socket_w.shutdown(socket.SHUT_RD) if fcntl: fd = self.__socket_r.fileno() flags = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) else: self.__socket_r.setblocking(False)
[docs] def close(self): """closes the stream, releasing any system resources associated with it""" def predicate(): return not self.__listening with self.__cond: socket_w = self.__socket_w if socket_w is not None: self.__socket_w = None socket_r = self.__socket_r self.__socket_r = None if not predicate(): socket_w.send(b'C') self.__cond.wait_for(predicate) if hasattr(socket, 'SHUT_RDWR'): socket_w.shutdown(socket.SHUT_RDWR) socket_w.close() if hasattr(socket, 'SHUT_RDWR'): socket_r.shutdown(socket.SHUT_RDWR) socket_r.close()
@property def closed(self): """tests whether the stream is closed or not""" raise NotImplementedError()
[docs] def fileno(self): """returns the stream's file descriptor""" raise NotImplementedError()
def acquire_read(self): def predicate(): return self.__read_pause_depth <= 0 and not self.__reading with self.__cond: self.__cond.wait_for(predicate) self.__reading = True def release_read(self): with self.__cond: if not self.__reading: raise RuntimeError('acquire_read not called before') self.__reading = False self.__cond.notify_all() def pause_read(self): def predicate(): return not (self.__reading or self.__polling) with self.__cond: self.__read_pause_depth += 1 if self.__polling and self.__socket_w is not None: self.__socket_w.send(b'P') self.__cond.wait_for(predicate) def resume_read(self): with self.__cond: if self.__read_pause_depth <= 0: raise RuntimeError('pause_write not called before') self.__read_pause_depth -= 1 if self.__read_pause_depth == 0: self.__cond.notify_all() def notify(self): with self.__cond: if (self.__socket_w is not None and self.__predicate is not None and self.__predicate()): self.__socket_w.send(b'N') self.__cond.notify_all()
[docs] def poll(self, timeout, predicate=None): """indicates whether the stream has data to read (within *timeout* seconds)""" timeout = Timeout(timeout) predicate_result = False def polling_or_predicate(): nonlocal predicate_result predicate_result = ( predicate is not None and predicate() ) return ( (self.__read_pause_depth <= 0 and not self.__polling) or predicate_result ) with self.__cond: result = self.__cond.wait_for(polling_or_predicate, timeout.timeleft()) if not result or predicate_result: return False assert ( self.__read_pause_depth <= 0 and not self.__polling ), f"unexpected state {(self.__read_pause_depth, self.__polling)=}" self.__polling = True self.__predicate = predicate socket_r = self.__socket_r self.__listening = socket_r is not None try: p = poll() # from lib.compat, it may be a select object on non-Unix platforms sfd = self.fileno() if socket_r is not None: wfd = socket_r.fileno() p.register(wfd, "r") else: wfd = None p.register(sfd, "r") while True: try: rl = p.poll(timeout.timeleft()) except select_error as ex: if ex.args[0] == errno.EINTR: continue else: raise if wfd is not None and any(wfd == fd for fd, _ in rl): try: c = socket_r.recv(1) except BlockingIOError: # actually should never come here but seen together # with gevent monkey patched poll on windows. So # something is not working right here. # Just ignore and continue for the moment continue except BaseException: raise if c == b'C': # notification for socket closing p.unregister(wfd) socket_r = None wfd = None with self.__cond: self.__listening = False self.__cond.notify_all() continue return False return any(sfd == fd for fd, _ in rl) except ValueError as ex: # if the underlying call is a select(), then the following errors may happen: # - "ValueError: filedescriptor cannot be a negative integer (-1)" # - "ValueError: filedescriptor out of range in select()" # let's translate them to select.error raise select_error(str(ex)) finally: with self.__cond: self.__predicate = None self.__polling = False self.__listening = False self.__cond.notify_all()
[docs] def read(self, count): """reads **exactly** *count* bytes, or raise EOFError :param count: the number of bytes to read :returns: read data """ raise NotImplementedError()
[docs] def write(self, data): """writes the entire *data*, or raise EOFError :param data: a string of binary data """ raise NotImplementedError()
def __enter__(self): return self def __exit__(self, *exc_info): self.close()
class ClosedFile: """Represents a closed file object (singleton)""" __slots__ = () def __getattr__(self, name): if name.startswith("__"): # issue 71 raise AttributeError("stream has been closed") raise EOFError("stream has been closed") def close(self): pass def notify(self): pass @property def closed(self): return True def fileno(self): raise EOFError("stream has been closed") ClosedFile = ClosedFile()
[docs] class SocketStream(Stream): """A stream over a socket""" __slots__ = ("sock",) MAX_IO_CHUNK = STREAM_CHUNK def __init__(self, sock): set_inheritable(sock, False) self.sock = sock super().__init__() @classmethod def _connect(cls, host, port, family=socket.AF_INET, socktype=socket.SOCK_STREAM, proto=0, timeout=3, nodelay=True, keepalive=False, attempts=6): family, socktype, proto, _, sockaddr = socket.getaddrinfo(host, port, family, socktype, proto)[0] s = socket_backoff_connect(family, socktype, proto, sockaddr, timeout, attempts) try: if nodelay: s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) if keepalive: s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) # Linux specific: after <keepalive> idle seconds, start sending keepalives every <keepalive> seconds. is_linux_socket = all( ( hasattr(socket, "TCP_KEEPIDLE"), hasattr(socket, "TCP_KEEPINTVL"), hasattr(socket, "TCP_KEEPCNT"), ) ) if is_linux_socket: # Drop connection after 5 failed keepalives # `keepalive` may be a bool or an integer if keepalive is True: keepalive = 60 if keepalive < 1: raise ValueError("Keepalive minimal value is 1 second") s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5) s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, keepalive) s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, keepalive) return s except BaseException: s.close() raise
[docs] @classmethod def connect(cls, host, port, **kwargs): """factory method that creates a ``SocketStream`` over a socket connected to *host* and *port* :param host: the host name :param port: the TCP port :param family: specify a custom socket family :param socktype: specify a custom socket type :param proto: specify a custom socket protocol :param timeout: connection timeout (default is 3 seconds) :param nodelay: set the TCP_NODELAY socket option :param keepalive: enable TCP keepalives. The value should be a boolean, but on Linux, it can also be an integer specifying the keepalive interval (in seconds) :param ipv6: if True, creates an IPv6 socket (``AF_INET6``); otherwise an IPv4 (``AF_INET``) socket is created :returns: a :class:`SocketStream` """ if kwargs.pop("ipv6", False): kwargs["family"] = socket.AF_INET6 return cls(cls._connect(host, port, **kwargs))
[docs] @classmethod def unix_connect(cls, path, timeout=3): """factory method that creates a ``SocketStream`` over a unix domain socket located in *path* :param path: the path to the unix domain socket :param timeout: socket timeout """ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: s.settimeout(timeout) s.connect(path) return cls(s) except BaseException: s.close() raise
[docs] @classmethod def ssl_connect(cls, host, port, ssl_kwargs, **kwargs): """factory method that creates a ``SocketStream`` over an SSL-wrapped socket, connected to *host* and *port* with the given credentials. :param host: the host name :param port: the TCP port :param ssl_kwargs: a dictionary of keyword arguments for ``ssl.SSLContext`` and ``ssl.SSLContext.wrap_socket`` :param kwargs: additional keyword arguments: ``family``, ``socktype``, ``proto``, ``timeout``, ``nodelay``, passed directly to the ``socket`` constructor, or ``ipv6``. :param ipv6: if True, creates an IPv6 socket (``AF_INET6``); otherwise an IPv4 (``AF_INET``) socket is created :returns: a :class:`SocketStream` """ if kwargs.pop("ipv6", False): kwargs["family"] = socket.AF_INET6 s = cls._connect(host, port, **kwargs) try: if "ssl_version" in ssl_kwargs: context = ssl.SSLContext(ssl_kwargs.pop("ssl_version")) else: context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH) certfile = ssl_kwargs.pop("certfile", None) keyfile = ssl_kwargs.pop("keyfile", None) if certfile is not None: context.load_cert_chain(certfile, keyfile=keyfile) ca_certs = ssl_kwargs.pop("ca_certs", None) if ca_certs is not None: context.load_verify_locations(ca_certs) ciphers = ssl_kwargs.pop("ciphers", None) if ciphers is not None: context.set_ciphers(ciphers) check_hostname = ssl_kwargs.pop("check_hostname", None) if check_hostname is not None: context.check_hostname = check_hostname cert_reqs = ssl_kwargs.pop("cert_reqs", None) if cert_reqs is not None: context.verify_mode = cert_reqs s2 = context.wrap_socket(s, server_hostname=host, **ssl_kwargs) return cls(s2) except BaseException: s.close() raise
@property def closed(self): return self.sock is ClosedFile
[docs] def close(self): sock, self.sock = self.sock, ClosedFile if sock is not ClosedFile: if hasattr(socket, 'SHUT_WR'): try: # inform peer that we are finished sending sock.shutdown(socket.SHUT_WR) while True: # wait for peer to close it's sending side as well buf = sock.recv(self.MAX_IO_CHUNK) if not buf: break except Exception: pass sock.close() super().close()
[docs] def fileno(self): try: fileno = self.sock.fileno() except socket.error as ex: self.close() if get_exc_errno(ex) == errno.EBADF: raise EOFError() else: raise if isinstance(fileno, int) and fileno == -1: raise EOFError("stream has been closed") return fileno
[docs] def read(self, count): data = [] while count > 0: # send and recv must not be done in parallel on TLS sockets self.acquire_read() try: buf = self.sock.recv(min(self.MAX_IO_CHUNK, count)) except socket.timeout: continue except socket.error as ex: if get_exc_errno(ex) in retry_errnos: # windows just has to be a bitch continue self.close() raise EOFError(ex) finally: self.release_read() if not buf: self.close() raise EOFError("connection closed by peer") data.append(buf) count -= len(buf) return BYTES_LITERAL("").join(data)
[docs] def write(self, data): try: while data: # send and recv must not be done in parallel on TLS sockets self.pause_read() try: count = self.sock.send(data[:self.MAX_IO_CHUNK]) finally: # resume reading self.resume_read() data = data[count:] except socket.error as ex: self.close() raise EOFError(ex)
[docs] class TunneledSocketStream(SocketStream): """A socket stream over an SSH tunnel (terminates the tunnel when the connection closes)""" __slots__ = ("__tun",) def __init__(self, sock): super().__init__(sock) self.tun = None
[docs] def close(self): super().close() if self.tun: self.tun.close()
@property def tun(self): return self.__tun @tun.setter def tun(self, value): if (value is not None and value is not ClosedFile and hasattr(value, 'fileno')): set_inheritable(value, False) self.__tun = value
[docs] class PipeStream(Stream): """A stream over two simplex pipes (one used to input, another for output)""" __slots__ = ("incoming", "outgoing", "__condition", "__ready", "__reader") MAX_IO_CHUNK = STREAM_CHUNK def __init__(self, incoming, outgoing): def rawstream(stream, mode): fileno = stream.fileno() try: stream.detach() except (AttributeError, io.UnsupportedOperation): return stream return open(fileno, mode, buffering=0) self.__redirect_inputs(incoming) self.__redirect_outputs(outgoing) outgoing.flush() rawincoming = rawstream(incoming, "rb") rawoutgoing = rawstream(outgoing, "wb") set_inheritable(rawincoming, False) set_inheritable(rawoutgoing, False) self.incoming = rawincoming self.outgoing = rawoutgoing self.__condition = threading.Condition(threading.Lock()) self.__ready = BYTES_LITERAL("") self.__reader = worker(self.__readthread, rawincoming) @staticmethod def __redirect_inputs(stream): inputs = ('stdin', '__stdin__') newinput = None for attr in inputs: if getattr(sys, attr) is stream: if newinput is None: newinput = open(os.devnull, "r") setattr(sys, attr, newinput) @staticmethod def __redirect_outputs(stream): outputs = ('stdout', '__stdout__', 'stderr', '__stderr__') newoutput = None for attr in outputs: if getattr(sys, attr) is stream: if newoutput is None: newoutput = open(os.devnull, "w") setattr(sys, attr, newoutput) def __del__(self): # this is called from garbage collection # garbage collection might kick in at any moment # Therefore we must be very careful what we call # from here self.close()
[docs] @classmethod def from_std(cls): """factory method that creates a PipeStream over the standard pipes (``stdin`` and ``stdout``) :returns: a :class:`PipeStream` instance """ return cls(sys.stdin, sys.stdout)
[docs] @classmethod def create_pair(cls): """factory method that creates two pairs of anonymous pipes, and creates two PipeStreams over them. Useful for ``fork()``. :returns: a tuple of two :class:`PipeStream` instances """ r1, w1 = os.pipe() r2, w2 = os.pipe() side1 = cls(os.fdopen(r1, "rb"), os.fdopen(w2, "wb")) side2 = cls(os.fdopen(r2, "rb"), os.fdopen(w1, "wb")) return side1, side2
@property def closed(self): with self.__condition: if self.__ready: return False return self.incoming is ClosedFile
[docs] def close(self): with self.__condition: incoming = self.incoming outgoing = self.outgoing self.incoming = ClosedFile self.outgoing = ClosedFile reader = self.__reader self.__reader = None self.__condition.notify_all() outgoing.close() if reader: reader.join(5) incoming.close() if reader and reader.is_alive(): reader.join() with self.__condition: self.__ready = BYTES_LITERAL("") self.__condition.notify_all()
[docs] def fileno(self): with self.__condition: return self.incoming.fileno()
def notify(self): with self.__condition: self.__condition.notify()
[docs] def poll(self, timeout, predicate=None): predicate_result = False def ready(): nonlocal predicate_result predicate_result = predicate is not None and predicate() return (self.__ready or self.incoming is ClosedFile or predicate_result) with self.__condition: result = self.__condition.wait_for(ready, timeout.timeleft()) if not result or predicate_result: return False if self.__ready: return True if self.incoming is ClosedFile: raise EOFError("stream has been closed") return False
def __readthread(self, incoming): fd = incoming.fileno() if fcntl: flags = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) else: try: incoming.setblocking(False) except AttributeError: pass p = poll() p.register(fd, "r") while True: try: p.poll() except select_error as ex: if ex.args[0] == errno.EINTR: continue buf = None else: try: buf = os.read(fd, self.MAX_IO_CHUNK) except OSError: buf = None with self.__condition: if buf: self.__ready = self.__ready + buf else: self.incoming = ClosedFile self.__condition.notify_all() if not buf: incoming.close() break
[docs] def read(self, count): try: with self.__condition: self.__condition.wait_for(lambda: len(self.__ready) >= count or self.incoming is ClosedFile) if len(self.__ready) < count: if len(self.__ready) > 0: self.__ready = BYTES_LITERAL("") self.__condition.notify_all() raise EOFError("stream has been closed") data = self.__ready[:count] self.__ready = self.__ready[count:] return data except EOFError: self.close() raise except EnvironmentError as ex: self.close() raise EOFError(ex)
[docs] def write(self, data): try: while data: chunk = data[:self.MAX_IO_CHUNK] written = os.write(self.outgoing.fileno(), chunk) data = data[written:] except EnvironmentError as ex: self.close() raise EOFError(ex)
[docs] class Win32PipeStream(Stream): """A stream over two simplex pipes (one used to input, another for output). This is an implementation for Windows pipes (which suck)""" __slots__ = ("incoming", "outgoing", "__fileno", "__keepalive") PIPE_BUFFER_SIZE = 130000 MAX_IO_CHUNK = STREAM_CHUNK def __init__(self, incoming, outgoing): import msvcrt self.__keepalive = (incoming, outgoing) if hasattr(incoming, "fileno"): self.__fileno = incoming.fileno() try: set_inheritable(incoming, False) except OSError: pass incoming = msvcrt.get_osfhandle(incoming.fileno()) if hasattr(outgoing, "fileno"): try: set_inheritable(outgoing, False) except OSError: pass outgoing = msvcrt.get_osfhandle(outgoing.fileno()) self.incoming = incoming self.outgoing = outgoing @classmethod def from_std(cls): pipestream = cls(sys.stdin, sys.stdout) sys.stdin = os.open(os.devnull, os.O_RDWR) sys.stdout = sys.stdin return pipestream @classmethod def create_pair(cls): r1, w1 = win32pipe.CreatePipe(None, cls.PIPE_BUFFER_SIZE) r2, w2 = win32pipe.CreatePipe(None, cls.PIPE_BUFFER_SIZE) return cls(r1, w2), cls(r2, w1)
[docs] def fileno(self): return self.__fileno
@property def closed(self): return self.incoming is ClosedFile
[docs] def close(self): if self.closed: return try: win32file.CloseHandle(self.outgoing) except Exception: pass self.outgoing = ClosedFile try: win32file.CloseHandle(self.incoming) except Exception: pass self.incoming = ClosedFile
[docs] def read(self, count): try: data = [] while count > 0: dummy, buf = win32file.ReadFile(self.incoming, int(min(self.MAX_IO_CHUNK, count))) count -= len(buf) data.append(buf) except TypeError as ex: if not self.closed: raise raise EOFError(ex) except win32file.error as ex: self.close() raise EOFError(ex) return BYTES_LITERAL("").join(data)
[docs] def write(self, data): try: while data: dummy, count = win32file.WriteFile(self.outgoing, data[:self.MAX_IO_CHUNK]) data = data[count:] except TypeError as ex: if not self.closed: raise raise EOFError(ex) except win32file.error as ex: self.close() raise EOFError(ex)
def notify(self): pass
[docs] def poll(self, timeout, predicate=None, interval=0.001): """a Windows version of select()""" timeout = Timeout(timeout) try: while True: if predicate is not None and predicate(): return False if win32pipe.PeekNamedPipe(self.incoming, 0)[1] != 0: return True if timeout.expired(): return False timeout.sleep(interval) except pywintypes.error as ex: if ex.args[0] == 109: # error: The pipe has been ended. raise EOFError(ex) raise except TypeError as ex: if not self.closed: raise raise EOFError(ex)
[docs] class NamedPipeStream(Win32PipeStream): """A stream over two named pipes (one used to input, another for output). Windows implementation.""" NAMED_PIPE_PREFIX = r'\\.\pipe\rpyc_' PIPE_IO_TIMEOUT = 3 CONNECT_TIMEOUT = 3 def __init__(self, handle, is_server_side): super().__init__(handle, handle) self.is_server_side = is_server_side self.read_overlapped = pywintypes.OVERLAPPED() self.read_overlapped.hEvent = win32event.CreateEvent(None, 1, 1, None) self.write_overlapped = pywintypes.OVERLAPPED() self.write_overlapped.hEvent = win32event.CreateEvent(None, 1, 1, None) self.poll_buffer = win32file.AllocateReadBuffer(1) self.poll_read = False @classmethod def from_std(cls): raise NotImplementedError() @classmethod def create_pair(cls): raise NotImplementedError()
[docs] @classmethod def create_server(cls, pipename, connect=True): """factory method that creates a server-side ``NamedPipeStream``, over a newly-created *named pipe* of the given name. :param pipename: the name of the pipe. It will be considered absolute if it starts with ``\\\\.``; otherwise ``\\\\.\\pipe\\rpyc`` will be prepended. :param connect: whether to connect on creation or not :returns: a :class:`NamedPipeStream` instance """ if not pipename.startswith("\\\\."): pipename = cls.NAMED_PIPE_PREFIX + pipename handle = win32pipe.CreateNamedPipe( pipename, win32pipe.PIPE_ACCESS_DUPLEX | win32file.FILE_FLAG_OVERLAPPED, win32pipe.PIPE_TYPE_BYTE | win32pipe.PIPE_READMODE_BYTE, 1, cls.PIPE_BUFFER_SIZE, cls.PIPE_BUFFER_SIZE, cls.PIPE_IO_TIMEOUT * 1000, None ) inst = cls(handle, True) if connect: inst.connect_server() return inst
[docs] def connect_server(self): """connects the server side of an unconnected named pipe (blocks until a connection arrives)""" if not self.is_server_side: raise ValueError("this must be the server side") win32pipe.ConnectNamedPipe(self.incoming, self.write_overlapped) win32event.WaitForSingleObject(self.write_overlapped.hEvent, win32event.INFINITE)
[docs] @classmethod def create_client(cls, pipename): """factory method that creates a client-side ``NamedPipeStream``, over a newly-created *named pipe* of the given name. :param pipename: the name of the pipe. It will be considered absolute if it starts with ``\\\\.``; otherwise ``\\\\.\\pipe\\rpyc`` will be prepended. :returns: a :class:`NamedPipeStream` instance """ if not pipename.startswith("\\\\."): pipename = cls.NAMED_PIPE_PREFIX + pipename handle = win32file.CreateFile( pipename, win32file.GENERIC_READ | win32file.GENERIC_WRITE, 0, None, win32file.OPEN_EXISTING, win32file.FILE_FLAG_OVERLAPPED, None ) return cls(handle, False)
[docs] def close(self): if self.closed: return if self.is_server_side: win32file.FlushFileBuffers(self.outgoing) win32pipe.DisconnectNamedPipe(self.outgoing) win32file.CloseHandle(self.write_overlapped.hEvent) win32file.CloseHandle(self.read_overlapped.hEvent) Win32PipeStream.close(self)
[docs] def read(self, count): try: if self.poll_read: win32file.GetOverlappedResult(self.incoming, self.read_overlapped, 1) data = [self.poll_buffer[:]] self.poll_read = False count -= 1 else: data = [] while count > 0: hr, buf = win32file.ReadFile(self.incoming, win32file.AllocateReadBuffer(int(min(self.MAX_IO_CHUNK, count))), self.read_overlapped) n = win32file.GetOverlappedResult(self.incoming, self.read_overlapped, 1) count -= n data.append(buf[:n]) except TypeError as ex: if not self.closed: raise raise EOFError(ex) except win32file.error as ex: self.close() raise EOFError(ex) return BYTES_LITERAL("").join(data)
[docs] def write(self, data): try: while data: dummy, count = win32file.WriteFile(self.outgoing, data[:self.MAX_IO_CHUNK], self.write_overlapped) data = data[count:] except TypeError as ex: if not self.closed: raise raise EOFError(ex) except win32file.error as ex: self.close() raise EOFError(ex)
[docs] def poll(self, timeout, predicate=None, interval=0.001): """Windows version of select()""" timeout = Timeout(timeout) try: wait_time = int(max(1, interval * 1000)) if not self.poll_read: try: hr, self.poll_buffer = win32file.ReadFile(self.incoming, self.poll_buffer, self.read_overlapped) except pywintypes.error as ex: if ex.args[0] in ( 109, # error: The pipe has been ended. 233, # error: No process is on the other end of the pipe. ): raise EOFError(ex) raise except TypeError as ex: if not self.closed: raise raise EOFError(ex) self.poll_read = True if hr == 0: return True while True: if predicate is not None and predicate(): return False res = win32event.WaitForSingleObject(self.read_overlapped.hEvent, wait_time) if res == win32event.WAIT_OBJECT_0: return True if timeout.expired(): return False except TypeError as ex: if not self.closed: raise raise EOFError(ex)
if sys.platform == "win32": PipeStream = Win32PipeStream # noqa: F811