"""
The Flow module lets you connect to processes or network services using a
unified API. It is primarily designed for synchronous communication flows.

It is based around the central :class:`Flow` class which uses a ``Channel``
to connect to a process. The :class:`Flow` class then uses the primitives
exposed by the ``Channel`` to provide a high level API for reading/receiving
and writing/sending data.


Examples:
    >>> from pwny import *
    >>> f = Flow.connect_tcp('ced.pwned.systems', 80)
    >>> f.writelines([
    ...     b'GET / HTTP/1.0',
    ...     b'Host: ced.pwned.systems',
    ...     b'',
    ... ])
    >>> line = f.readline().strip()
    >>> print(line == b'HTTP/1.0 200 OK')
    True
    >>> f.until(b'\\r\\n\\r\\n')
    >>> f.read_eof(echo=True)
    ... lots of html ...

    >>> from pwny import *
    >>> f = Flow.execute('cat')
    >>> f.writeline(b'hello')
    >>> f.readline(echo=True)
"""

import subprocess
import sys
import socket


__all__ = [
    'ProcessChannel',
    'SocketChannel',
    'TCPSocketChannel',
    'Flow',
]


class ProcessChannel(object):
    """ProcessChannel(executable, argument..., redirect_stderr=False)

    This channel type allows controlling processes. It uses python's
    ``subprocess.Popen`` class to execute a process and allows you to
    communicate with it.

    Args:
        executable(str): The executable to start.
        argument...(list of str): The arguments to pass to the executable.
        redirect_stderr(bool): Whether to also capture the output of stderr.
    """

    def __init__(self, executable, *arguments, **kwargs):
        if kwargs.get('redirect_stderr'):
            stderr = subprocess.STDOUT
        else:
            stderr = None

        self._process = subprocess.Popen(
            (executable,) + tuple(arguments),
            bufsize=0,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=stderr,
        )

    def read(self, n):
        """
        Read *n* bytes from the subprocess' output channel.

        Args:
            n(int): The number of bytes to read.

        Returns:
            bytes: *n* bytes of output.

        Raises:
            EOFError: If the process exited.
        """

        d = b''
        while n:
            try:
                block = self._process.stdout.read(n)
            except ValueError:
                block = None
            if not block:
                self._process.poll()
                raise EOFError('Process ended')
            d += block
            n -= len(block)
        return d

    def write(self, data):
        """
        Write *n* bytes to the subprocess' input channel.

        Args:
            data(bytes): The data to write.

        Raises:
            EOFError: If the process exited.
        """

        self._process.poll()
        if self._process.returncode is not None:
            raise EOFError('Process ended')
        self._process.stdin.write(data)

    def close(self):
        """
        Wait for the subprocess to exit.
        """

        self._process.communicate()

    def kill(self):
        """
        Terminate the subprocess.
        """

        self._process.kill()


class SocketChannel(object):
    """
    This channel type allows controlling sockets.

    Args:
        socket(socket.socket): The (already connected) socket to control.
    """

    def __init__(self, sock):
        self._socket = sock

    def read(self, n):
        """
        Receive *n* bytes from the socket.

        Args:
            n(int): The number of bytes to read.

        Returns:
            bytes: *n* bytes read from the socket.

        Raises:
            EOFError: If the socket was closed.
        """

        d = b''
        while n:
            try:
                block = self._socket.recv(n)
            except socket.error:
                block = None
            if not block:
                raise EOFError('Socket closed')
            d += block
            n -= len(block)
        return d

    def write(self, data):
        """
        Send *n* bytes to socket.

        Args:
            data(bytes): The data to send.

        Raises: