コード例 #1
0
ファイル: sock.py プロジェクト: versiledev/versile-python
class VClientSocketAgent(VClientSocket):
    """A :class:`VClientSocket` with a byte producer/consumer interface.

    :param max_read:  max bytes fetched per socket read
    :type  max_read:  int
    :param max_write: max bytes written per socket send
    :type  max_write: int
    :param wbuf_len:  buffer size of data held for writing (or None)
    :type  wbuf_len:  int

    *max_read* is also the maximum size of the buffer for data read
    from the socket (so maximum bytes read in one read operation is
    *max_read* minus the amount of data currently held in the receive
    buffer).

    If *wbuf_len* is None then *max_write* is used as the buffer size.

    """
    def __init__(self,
                 reactor,
                 sock=None,
                 hc_pol=None,
                 close_cback=None,
                 connected=False,
                 max_read=0x4000,
                 max_write=0x4000,
                 wbuf_len=None):
        self._max_read = max_read
        self._max_write = max_write
        self._wbuf = VByteBuffer()
        if wbuf_len is None:
            wbuf_len = max_write
        self._wbuf_len = wbuf_len

        self._ci = None
        self._ci_eod = False
        self._ci_eod_clean = None
        self._ci_producer = None
        self._ci_consumed = 0
        self._ci_lim_sent = 0
        self._ci_aborted = False

        self._pi = None
        self._pi_closed = False
        self._pi_consumer = None
        self._pi_produced = 0
        self._pi_prod_lim = 0
        self._pi_buffer = VByteBuffer()
        self._pi_aborted = False

        # Parent __init__ must be called after local attributes are
        # initialized due to overloaded methods called during construction
        super_init = super(VClientSocketAgent, self).__init__
        super_init(reactor=reactor,
                   sock=sock,
                   hc_pol=hc_pol,
                   close_cback=close_cback,
                   connected=connected)

    @property
    def byte_consume(self):
        """Holds a :class:`IVByteConsumer` interface to the socket."""
        if not self._ci:
            ci = _VSocketConsumer(self)
            self._ci = weakref.ref(ci)
            return ci
        else:
            ci = self._ci()
            if ci:
                return ci
            else:
                ci = _VSocketConsumer(self)
                self._ci = weakref.ref(ci)
                return ci

    @property
    def byte_produce(self):
        """Holds a :class:`IVByteProducer` interface to the socket."""
        if not self._pi:
            pi = _VSocketProducer(self)
            self._pi = weakref.ref(pi)
            return pi
        else:
            pi = self._pi()
            if pi:
                return pi
            else:
                pi = _VSocketProducer(self)
                self._pi = weakref.ref(pi)
                return pi

    @property
    def byte_io(self):
        """Byte interface (\ :class:`versile.reactor.io.VByteIOPair`\ )."""
        return VByteIOPair(self.byte_consume, self.byte_produce)

    def _can_connect(self, peer):
        """Called internally to validate whether a connection can be made.

        :param peer: peer to connect to
        :type  peer: :class:`versile.common.peer.VPeer`
        :returns:    True if connection is allowed
        :rtype:      bool

        If the socket is connected to a byte consumer, sends a control
        request for 'can_connect(peer)'. If that control message
        returns a boolean, this is used as the _can_connect
        result. Otherwise, True is returned.

        """
        if self._pi_consumer:
            try:
                return self._pi_consumer.control.can_connect(peer)
            except VIOMissingControl:
                return True
        else:
            return True

    def _sock_was_connected(self):
        super(VClientSocketAgent, self)._sock_was_connected()
        if self._p_consumer:
            # Notify producer-connected chain about 'connected' status
            control = self._p_consumer.control

            def notify():
                try:
                    control.connected(self._sock_peer)
                except VIOMissingControl:
                    pass

            self.reactor.schedule(0.0, notify)

    @peer
    def _c_consume(self, buf, clim):
        if self._ci_eod:
            raise VIOClosed('Consumer already reached end-of-data')
        elif not self._ci_producer:
            raise VIOError('No connected producer')
        elif self._ci_consumed >= self._ci_lim_sent:
            raise VIOError('Consume limit exceeded')
        elif not buf:
            raise VIOError('No data to consume')

        max_cons = self._wbuf_len - len(self._wbuf)
        max_cons = min(max_cons, self._ci_lim_sent - self._ci_consumed)
        if clim is not None and clim > 0:
            max_cons = min(max_cons, clim)

        was_empty = not self._wbuf
        indata = buf.pop(max_cons)
        self._wbuf.append(indata)
        self._ci_consumed += len(indata)
        if was_empty:
            self.start_writing(internal=True)
        return self._ci_lim_sent

    def _c_end_consume(self, clean):
        if self._ci_eod:
            return
        self._ci_eod = True
        self._ci_eod_clean = clean

        if not self._wbuf:
            self.close_output(VFIOCompleted())
            if self._ci_producer:
                self._ci_producer.abort()
                self._c_detach()

    def _c_abort(self, force=False):
        if not self._ci_aborted or force:
            self._ci_aborted = True
            self._ci_eod = True
            self._ci_consumed = self._ci_lim_sent = 0
            self._wbuf.clear()
            if not self._sock_out_closed:
                self.close_output(VFIOCompleted())
            if self._ci_producer:
                self._ci_producer.abort()
                self._c_detach()

    def _c_attach(self, producer, rthread=False):
        # Ensure 'attach' is performed in reactor thread
        if not rthread:
            self.reactor.execute(self._c_attach, producer, rthread=True)
            return

        if self._ci_producer is producer:
            return
        elif self._ci_producer:
            raise VIOError('Producer already attached')

        self._ci_producer = producer
        if self._sock_out_closed:
            self.reactor.schedule(0.0, self._c_abort, True)
        else:
            self._ci_consumed = self._ci_lim_sent = 0
            producer.attach(self.byte_consume)
            self._ci_lim_sent = self._wbuf_len
            producer.can_produce(self._ci_lim_sent)

        # Notify attached chain
        try:
            producer.control.notify_consumer_attached(self.byte_consume)
        except VIOMissingControl:
            pass

    def _c_detach(self, rthread=False):
        # Ensure 'detach' is performed in reactor thread
        if not rthread:
            self.reactor.execute(self._c_detach, rthread=True)
            return

        if self._ci_producer:
            prod, self._ci_producer = self._ci_producer, None
            prod.detach()
            self._ci_consumed = self._ci_lim_sent = 0

    def active_do_write(self):
        if self._wbuf:
            data = self._wbuf.peek(self._max_write)
            try:
                num_written = self.write_some(data)
            except VIOException:
                self._c_abort()
            else:
                if num_written > 0:
                    self._wbuf.remove(num_written)
                    if self._ci_producer:
                        self._ci_lim_sent = (self._ci_consumed +
                                             self._wbuf_len - len(self._wbuf))
                        self._ci_producer.can_produce(self._ci_lim_sent)
                if not self._wbuf:
                    self.stop_writing()
                    if self._ci_eod:
                        self._c_abort()
        else:
            self.stop_writing()

    def _output_was_closed(self, reason):
        # No more output will be written, abort consumer
        self._c_abort()

    @property
    def _c_control(self):
        return self._c_get_control()

    # Implements _c_control in order to be able to override _c_control
    # behavior by overloading as a regular method
    def _c_get_control(self):
        return VIOControl()

    @property
    def _c_producer(self):
        return self._ci_producer

    @property
    def _c_flows(self):
        return tuple()

    @property
    def _c_twoway(self):
        return True

    @property
    def _c_reverse(self):
        return self.byte_produce()

    @peer
    def _p_can_produce(self, limit):
        if not self._pi_consumer:
            raise VIOError('No connected consumer')

        if limit is None or limit < 0:
            if (not self._pi_prod_lim is None and not self._pi_prod_lim < 0):
                if self._pi_produced >= self._pi_prod_lim:
                    self.start_reading(internal=True)
                self._pi_prod_lim = limit
        else:
            if (self._pi_prod_lim is not None
                    and 0 <= self._pi_prod_lim < limit):
                if self._pi_produced >= self._pi_prod_lim:
                    self.start_reading(internal=True)
                self._pi_prod_lim = limit

    def _p_abort(self, force=False):
        if not self._pi_aborted or force:
            self._pi_aborted = True
            self._pi_produced = self._pi_prod_lim = 0
            if not self._sock_in_closed:
                self.close_input(VFIOCompleted())
            if self._pi_consumer:
                self._pi_consumer.abort()
                self._p_detach()

    def _p_attach(self, consumer, rthread=False):
        # Ensure 'attach' is performed in reactor thread
        if not rthread:
            self.reactor.execute(self._p_attach, consumer, rthread=True)
            return

        if self._pi_consumer is consumer:
            return
        elif self._pi_consumer:
            raise VIOError('Consumer already attached')

        self._pi_produced = self._pi_prod_lim = 0
        self._pi_consumer = consumer
        consumer.attach(self.byte_produce)

        # Notify attached chain
        try:
            consumer.control.notify_producer_attached(self.byte_produce)
        except VIOMissingControl:
            pass

        # If closed, pass notification
        if self._sock_in_closed:
            self.reactor.schedule(0.0, self._p_abort, True)

    def _p_detach(self, rthread=False):
        # Ensure 'detach' is performed in reactor thread
        if not rthread:
            self.reactor.execute(self._p_detach, rthread=True)
            return

        if self._pi_consumer:
            cons, self._pi_consumer = self._pi_consumer, None
            cons.detach()
            self._pi_produced = self._pi_prod_lim = 0

    def active_do_read(self):
        if not self._pi_consumer:
            self.stop_reading()

        if self._pi_prod_lim is not None and self._pi_prod_lim >= 0:
            max_read = self._pi_prod_lim - self._pi_produced
        else:
            max_read = self._max_read
        max_read = min(max_read, self._max_read)
        if max_read <= 0:
            self.stop_reading()
            return
        try:
            data = self.read_some(max_read)
        except Exception as e:
            self._p_abort()
        else:
            self._pi_buffer.append(data)
            if self._pi_buffer:
                self.pi_prod_lim = self._pi_consumer.consume(self._pi_buffer)

    def _input_was_closed(self, reason):
        if self._pi_consumer:
            # Notify consumer about end-of-data
            clean = isinstance(reason, VFIOCompleted)
            self._pi_consumer.end_consume(clean)
        else:
            self._p_abort()

    @property
    def _p_control(self):
        return self._p_get_control()

    # Implements _p_control in order to be able to override _p_control
    # behavior by overloading as a regular method
    def _p_get_control(self):
        class _Control(VIOControl):
            def __init__(self, sock):
                self.__sock = sock

            def req_producer_state(self, consumer):
                # Send notification of socket connect status
                def notify():
                    if self.__sock._sock_peer:
                        try:
                            consumer.control.connected(self.__sock._sock_peer)
                        except VIOMissingControl:
                            pass

                self.__sock.reactor.schedule(0.0, notify)

        return _Control(self)

    @property
    def _p_consumer(self):
        return self._pi_consumer

    @property
    def _p_flows(self):
        return tuple()

    @property
    def _p_twoway(self):
        return True

    @property
    def _p_reverse(self):
        return self.byte_consume()
コード例 #2
0
ファイル: message.py プロジェクト: versiledev/versile-python
class VMessageDecrypter(object):
    """Decrypter for encrypted messages with data integrity check.

    Decodes encrypted plaintext messages in the format encrypted by
    :class:`VMessageEncrypter`\ .

    :param decrypter:   transform for decryption
    :type  decrypter:   :class:`versile.crypto.VBlockTransform`
    :param hash_cls:    hash class for message integrity hash
    :type  hash_cls:    :class:`versile.crypto.VHash`
    :param mac_secret:  secret data for package authentication
    :type  mac_secret:  bytes

    """
    def __init__(self, decrypter, hash_cls, mac_secret):
        self._decrypter = decrypter
        self._hash_cls = hash_cls
        self._mac_secret = mac_secret
        self._max_plaintext_len = 0x10000  # HARDCODED 2-byte message length
        self._cipher_blocksize = decrypter.blocksize
        self._hash_len = hash_cls.digest_size()
        self._read_buf = VByteBuffer()
        self._in_buf = VByteBuffer()
        self._msg_buf = VByteBuffer()
        self._have_len = False
        self._plaintext_blocksize = None
        self._plaintext_len = None
        self._invalid = False
        self._result = None
        self._msg_num = 0

    def reset(self):
        """Resets the decrypter to read a new message.

        :raises: :class:`VCryptoException`

        Raises an exception if ongoing decryption is not completed.

        """
        if self._result is None:
            raise VCryptoException('Ongoing decryption not completed')
        self._read_buf.remove()
        self._in_buf.remove()
        self._msg_buf.remove()
        self._have_len = False
        self._plaintext_len = None
        self._invalid = False
        self._result = None

    def read(self, data):
        """Reads encrypted message data.

        :param data: input data to decrypt and decode
        :type  data: bytes, :class:`versile.common.util.VByteBuffer`
        :returns:    number of bytes read
        :rtype:      int

        Reads only as much data as is required to complete processing
        a complete single message. If data is of type
        :class:`versile.common.util.VByteBuffer` then the data that
        was read will be popped off the buffer.

        .. note::

            When decryption of one message has completed,
            :meth:`reset` must be called before a new message can be
            read.

        """
        if isinstance(data, bytes):
            read_buf = self._read_buf
            read_buf.remove()
            read_buf.append(data)
        elif isinstance(data, VByteBuffer):
            read_buf = data
        else:
            raise TypeError('Input must be bytes or VByteBuffer')

        num_read = 0
        _pbsize = self._plaintext_blocksize
        _cbsize = self._cipher_blocksize
        while read_buf and self._result is None and not self._invalid:
            # First decode single block to get blocksize
            if not self._have_len:
                max_read = _cbsize - len(self._in_buf)
                enc_data = read_buf.pop(max_read)
                self._in_buf.append(enc_data)
                num_read += len(enc_data)
                if len(self._in_buf) == _cbsize:
                    enc_data = self._in_buf.pop()
                    block = self._decrypter(enc_data)
                    if _pbsize is None:
                        _pbsize = len(block)
                        self._plaintext_blocksize = _pbsize
                    self._msg_buf.append(block)
                    len_bytes = self._msg_buf.peek(2)
                    if _pyver == 2:
                        self._plaintext_len = 1 + (
                            (_b_ord(len_bytes[0]) << 8) + _b_ord(len_bytes[1]))
                    else:
                        self._plaintext_len = 1 + (
                            (len_bytes[0] << 8) + len_bytes[1])
                    self._have_len = True

            # If we have first block, decrypt more blocks as available/needed
            if self._have_len:
                msg_len = 2 + self._plaintext_len + self._hash_len
                pad_len = msg_len % _pbsize
                if pad_len:
                    pad_len = _pbsize - pad_len
                    msg_len += pad_len
                msg_left = msg_len - len(self._msg_buf)
                blocks_left = msg_left // _pbsize
                input_left = (blocks_left * _cbsize - len(self._in_buf))
                in_data = read_buf.pop(input_left)
                num_read += len(in_data)
                self._in_buf.append(in_data)
                num_decode = len(self._in_buf)
                num_decode -= num_decode % _cbsize
                if num_decode > 0:
                    enc_data = self._in_buf.pop(num_decode)
                    self._msg_buf.append(self._decrypter(enc_data))
                elif len(self._msg_buf) != msg_len:
                    break

            if self._have_len and len(self._msg_buf) == msg_len:
                len_bytes = self._msg_buf.pop(2)
                plaintext = self._msg_buf.pop(self._plaintext_len)
                padding = self._msg_buf.pop(pad_len)
                msg_hash = self._msg_buf.pop(self._hash_len)

                _mac_msg = b''.join((posint_to_bytes(self._msg_num), len_bytes,
                                     plaintext, padding))
                if msg_hash == self._hash_cls.hmac(self._mac_secret, _mac_msg):
                    self._result = plaintext
                    self._msg_num += 1
                else:
                    self._invalid = True

        return num_read

    def done(self):
        """Returns True if decryption and decoding of a message was done.

        :returns: True if reading is done
        :rtype:   bool
        :raises:  :exc:`versile.crypto.VCryptoException`

        Raises an exception if the message failed to verify against
        the message hash, meaning the message cannot be trusted and
        could have been tampered with.

        """
        if self._invalid:
            raise VCryptoException('Message failed to verify')
        return (self._result is not None)

    def result(self):
        """Returns plaintext of a decrypted and decoded message.

        :returns: decoded plaintext
        :rtype:   bytes
        :raises:  :exc:`versile.crypto.VCryptoException`

        Should only be called if :meth:`done` indicates message
        parsing was completed, otherwise an exception may be raised
        due to incomplete message.

        Raises an exception if the message failed to verify against
        the message hash, meaning the message cannot be trusted and
        may have been tampered with.

        """
        if self._invalid:
            raise VCryptoException('Message failed to verify')
        elif self._result is None:
            raise VCryptoException('Message not yet fully decoded')
        return self._result

    def has_data(self):
        """Returns True if object holds any data

        :returns: True if holds data
        :rtype:   bool

        Returns False only if no data has been read since the object
        was instantiated or since the most recent :meth:`reset`\ .

        """
        return bool(self._read_buf or self._in_buf or self._msg_buf
                    or self._result)

    @property
    def max_plaintext_len(self):
        """Maximum plaintext length allowed in a message."""
        return self._max_plaintext_len
コード例 #3
0
ファイル: pipe.py プロジェクト: versiledev/versile-python
class VPipeAgent(object):
    """Byte producer/consumer interface to a pipe reader/writer pair.

    :param read_fd:   read pipe file descriptor
    :type  read_fd:   int
    :param write_fd:  write pipe file descriptor
    :type  write_fd:  int
    :param max_read:  max bytes fetched per pipe read
    :type  max_read:  int
    :param max_write: max bytes written per pipe write
    :type  max_write: int
    :param wbuf_len:  buffer size of data held for writing (or None)
    :type  wbuf_len:  int

    The agent creates a :class:`VPipeReader` and :class:`VPipeWriter`
    for the provided pipe read/write descriptors which it uses for
    reactor driven pipe I/O communication.

    *max_read* is also the maximum size of the buffer for data read
    from the socket (so maximum bytes read in one read operation is
    *max_read* minus the amount of data currently held in the receive
    buffer).

    If *wbuf_len* is None then *max_write* is used as the buffer size.

    """
    def __init__(self,
                 reactor,
                 read_fd,
                 write_fd,
                 max_read=0x4000,
                 max_write=0x4000,
                 wbuf_len=None):
        self.__reactor = reactor

        self._reader = _VAgentReader(self, reactor, read_fd)
        self._writer = _VAgentWriter(self, reactor, write_fd)
        self._reader.set_pipe_peer(self._writer)
        self._writer.set_pipe_peer(self._reader)

        self._max_read = max_read
        self._max_write = max_write
        self._wbuf = VByteBuffer()
        if wbuf_len is None:
            wbuf_len = max_write
        self._wbuf_len = wbuf_len

        self._ci = None
        self._ci_eod = False
        self._ci_eod_clean = None
        self._ci_producer = None
        self._ci_consumed = 0
        self._ci_lim_sent = 0
        self._ci_aborted = False

        self._pi = None
        self._pi_closed = False
        self._pi_consumer = None
        self._pi_produced = 0
        self._pi_prod_lim = 0
        self._pi_buffer = VByteBuffer()
        self._pi_aborted = False

    @property
    def byte_consume(self):
        """Holds a :class:`IVByteConsumer` interface to the pipe reader."""
        if not self._ci:
            ci = _VPipeConsumer(self)
            self._ci = weakref.ref(ci)
            return ci
        else:
            ci = self._ci()
            if ci:
                return ci
            else:
                ci = _VPipeConsumer(self)
                self._ci = weakref.ref(ci)
                return ci

    @property
    def byte_produce(self):
        """Holds a :class:`IVByteProducer` interface to the pipe writer."""
        if not self._pi:
            pi = _VPipeProducer(self)
            self._pi = weakref.ref(pi)
            return pi
        else:
            pi = self._pi()
            if pi:
                return pi
            else:
                pi = _VPipeProducer(self)
                self._pi = weakref.ref(pi)
                return pi

    @property
    def byte_io(self):
        """Byte interface (\ :class:`versile.reactor.io.VByteIOPair`\ )."""
        return VByteIOPair(self.byte_consume, self.byte_produce)

    @property
    def reactor(self):
        """Holds the reactor of the associated reader."""
        return self.__reactor

    @peer
    def _c_consume(self, buf, clim):
        if self._ci_eod:
            raise VIOClosed('Consumer already reached end-of-data')
        elif not self._ci_producer:
            raise VIOError('No connected producer')
        elif self._ci_consumed >= self._ci_lim_sent:
            raise VIOError('Consume limit exceeded')
        elif not buf:
            raise VIOError('No data to consume')

        max_cons = self._wbuf_len - len(self._wbuf)
        max_cons = min(max_cons, self._ci_lim_sent - self._ci_consumed)
        if clim is not None and clim > 0:
            max_cons = min(max_cons, clim)

        was_empty = not self._wbuf
        indata = buf.pop(max_cons)
        self._wbuf.append(indata)
        self._ci_consumed += len(indata)
        if was_empty:
            self._writer.start_writing(internal=True)
        return self._ci_lim_sent

    def _c_end_consume(self, clean):
        if self._ci_eod:
            return
        self._ci_eod = True
        self._ci_eod_clean = clean

        if not self._wbuf:
            self._writer.close_output(VFIOCompleted())
            if self._ci_producer:
                self._ci_producer.abort()
                self._c_detach()

    def _c_abort(self):
        if not self._ci_aborted:
            self._ci_aborted = True
            self._ci_eod = True
            self._ci_consumed = self._ci_lim_sent = 0
            self._wbuf.clear()
            self._writer.close_output(VFIOCompleted())
            if self._ci_producer:
                self._ci_producer.abort()
                self._c_detach()

    def _c_attach(self, producer, rthread=False):
        # Ensure 'attach' is performed in reactor thread
        if not rthread:
            self.reactor.execute(self._c_attach, producer, rthread=True)
            return

        if self._ci_producer is producer:
            return
        elif self._ci_producer:
            raise VIOError('Producer already attached')

        self._ci_producer = producer
        self._ci_consumed = self._ci_lim_sent = 0
        producer.attach(self.byte_consume)
        self._ci_lim_sent = self._wbuf_len
        producer.can_produce(self._ci_lim_sent)

        # Notify attached chain
        try:
            producer.control.notify_consumer_attached(self.byte_consume)
        except VIOMissingControl:
            pass

    def _c_detach(self, rthread=False):
        # Ensure 'detach' is performed in reactor thread
        if not rthread:
            self.reactor.execute(self._c_detach, rthread=True)
            return

        if self._ci_producer:
            prod, self._ci_producer = self._ci_producer, None
            prod.detach()
            self._ci_consumed = self._ci_lim_sent = 0

    def _do_write(self):
        if self._wbuf:
            data = self._wbuf.peek(self._max_write)
            try:
                num_written = self._writer.write_some(data)
            except VIOException:
                self._c_abort()
            else:
                if num_written > 0:
                    self._wbuf.remove(num_written)
                    if self._ci_producer:
                        self._ci_lim_sent = (self._ci_consumed +
                                             self._wbuf_len - len(self._wbuf))
                        self._ci_producer.can_produce(self._ci_lim_sent)
                if not self._wbuf:
                    self._writer.stop_writing()
                    if self._ci_eod:
                        self._c_abort()
        else:
            self._writer.stop_writing()

    def _output_was_closed(self, reason):
        # No more output will be written, abort consumer
        self._c_abort()

    @property
    def _c_control(self):
        return VIOControl()

    @property
    def _c_producer(self):
        return self._ci_producer

    @property
    def _c_flows(self):
        return tuple()

    @property
    def _c_twoway(self):
        return True

    @property
    def _c_reverse(self):
        return self.byte_produce()

    @peer
    def _p_can_produce(self, limit):
        if not self._pi_consumer:
            raise VIOError('No connected consumer')

        if limit is None or limit < 0:
            if (not self._pi_prod_lim is None and not self._pi_prod_lim < 0):
                if self._pi_produced >= self._pi_prod_lim:
                    self._reader.start_reading(internal=True)
                self._pi_prod_lim = limit
        else:
            if (self._pi_prod_lim is not None
                    and 0 <= self._pi_prod_lim < limit):
                if self._pi_produced >= self._pi_prod_lim:
                    self._reader.start_reading(internal=True)
                self._pi_prod_lim = limit

    def _p_abort(self):
        if not self._pi_aborted:
            self._pi_aborted = True
            self._pi_produced = self._pi_prod_lim = 0
            self._reader.close_input(VFIOCompleted())
            if self._pi_consumer:
                self._pi_consumer.abort()
                self._p_detach()

    def _p_attach(self, consumer, rthread=False):
        # Ensure 'attach' is performed in reactor thread
        if not rthread:
            self.reactor.execute(self._p_attach, consumer, rthread=True)
            return

        if self._pi_consumer is consumer:
            return
        elif self._pi_consumer:
            raise VIOError('Consumer already attached')

        self._pi_produced = self._pi_prod_lim = 0
        self._pi_consumer = consumer
        consumer.attach(self.byte_produce)

        # Notify attached chain
        try:
            consumer.control.notify_producer_attached(self.byte_produce)
        except VIOMissingControl:
            pass

    def _p_detach(self, rthread=False):
        # Ensure 'detach' is performed in reactor thread
        if not rthread:
            self.reactor.execute(self._p_detach, rthread=True)
            return

        if self._pi_consumer:
            cons, self._pi_consumer = self._pi_consumer, None
            cons.detach()
            self._pi_produced = self._pi_prod_lim = 0

    def _do_read(self):
        if not self._pi_consumer:
            self._reader.stop_reading()

        if self._pi_prod_lim is not None and self._pi_prod_lim >= 0:
            max_read = self._pi_prod_lim - self._pi_produced
        else:
            max_read = self._max_read
        max_read = min(max_read, self._max_read)
        if max_read <= 0:
            self._reader.stop_reading()
            return
        try:
            data = self._reader.read_some(max_read)
        except Exception as e:
            self._p_abort()
        else:
            self._pi_buffer.append(data)
            if self._pi_buffer:
                self.pi_prod_lim = self._pi_consumer.consume(self._pi_buffer)

    def _input_was_closed(self, reason):
        if self._pi_consumer:
            # Notify consumer about end-of-data
            clean = isinstance(reason, VFIOCompleted)
            self._pi_consumer.end_consume(clean)
        else:
            self._p_abort()

    @property
    def _p_control(self):
        class _Control(VIOControl):
            def __init__(self, obj):
                self._obj = obj

            def req_producer_state(self, consumer):
                # Send 'connected' notification if pipe is not closed
                def notify():
                    if (self._obj._reader._fd >= 0
                            and self._obj._writer._fd >= 0):
                        try:
                            consumer.control.connected(VPipePeer())
                        except VIOMissingControl:
                            pass

                self._obj.reactor.schedule(0.0, notify)

        return _Control(self)

    @property
    def _p_consumer(self):
        return self._pi_consumer

    @property
    def _p_flows(self):
        return tuple()

    @property
    def _p_twoway(self):
        return True

    @property
    def _p_reverse(self):
        return self.byte_consume()