docs: def readBytes(self, size):
          """ A read can block for an unbounded amount of time, even if the
              kernel reports that the pipe handle is signalled, so we need to
              always perform our reads asynchronously
          """
  
          # try to satisfy the read from any buffered data
          if self._iobuf:
              if size >= len(self._iobuf):
                  res = self._iobuf
                  self.buf = None
                  return res
              res = self._iobuf[:size]
              self._iobuf = self._iobuf[size:]
              return res
  
          # We need to initiate a read
          buf = ctypes.create_string_buffer(size)
          olap = OVERLAPPED()
          olap.hEvent = self._waitable
  
          log("made read buff of size %d", size)
  
          # ReadFile docs warn against sending in the nread parameter for async
          # operations, so we always collect it via GetOverlappedResultEx
          immediate = ReadFile(self.pipe, buf, size, None, olap)
  
          if not immediate:
              err = GetLastError()
              if err != ERROR_IO_PENDING:
                  self._raise_win_err("failed to read %d bytes" % size, err)
  
          nread = wintypes.DWORD()
          if not self._get_overlapped_result_ex(
              self.pipe, olap, nread, 0 if immediate else self.timeout, True
          ):
              err = GetLastError()
              CancelIoEx(self.pipe, olap)
  
              if err == WAIT_TIMEOUT:
                  log("GetOverlappedResultEx timedout")
                  raise SocketTimeout(
                      "timed out after waiting %dms for read" % self.timeout
                  )
  
              log("GetOverlappedResultEx reports error %d", err)
              self._raise_win_err("error while waiting for read", err)
  
          nread = nread.value
          if nread == 0:
              # Docs say that named pipes return 0 byte when the other end did
              # a zero byte write.  Since we don't ever do that, the only
              # other way this shows up is if the client has gotten in a weird
              # state, so let's bail out
              CancelIoEx(self.pipe, olap)
              raise IOError("Async read yielded 0 bytes; unpossible!")
  
          # Holds precisely the bytes that we read from the prior request
          buf = buf[:nread]
  
          returned_size = min(nread, size)
          if returned_size == nread:
              return buf
  
          # keep any left-overs around for a later read to consume
          self._iobuf = buf[returned_size:]
          return buf[:returned_size]