From 38a3ee8d63a0b280f76524803bac352c07f3b99e Mon Sep 17 00:00:00 2001
From: Ben Firshman <ben@firshman.co.uk>
Date: Mon, 27 Oct 2014 11:27:20 +0000
Subject: [PATCH] Use upstream dockerpty 0.3.2

This reverts commit 60411e9f05ac2b9245053491cef7c3cda9e923f1.

Closes #556

Signed-off-by: Ben Firshman <ben@firshman.co.uk>

Conflicts:
	requirements.txt
	setup.py
---
 fig/cli/main.py                    |   2 +-
 fig/packages/__init__.py           |   0
 fig/packages/dockerpty/__init__.py |  27 ---
 fig/packages/dockerpty/io.py       | 294 -----------------------------
 fig/packages/dockerpty/pty.py      | 235 -----------------------
 fig/packages/dockerpty/tty.py      | 130 -------------
 requirements.txt                   |   1 +
 script/test                        |  11 +-
 setup.py                           |   1 +
 tests/integration/cli_test.py      |  14 +-
 10 files changed, 12 insertions(+), 703 deletions(-)
 delete mode 100644 fig/packages/__init__.py
 delete mode 100644 fig/packages/dockerpty/__init__.py
 delete mode 100644 fig/packages/dockerpty/io.py
 delete mode 100644 fig/packages/dockerpty/pty.py
 delete mode 100644 fig/packages/dockerpty/tty.py

diff --git a/fig/cli/main.py b/fig/cli/main.py
index 2ce8cfc32..e31ba539e 100644
--- a/fig/cli/main.py
+++ b/fig/cli/main.py
@@ -7,7 +7,7 @@ import signal
 from operator import attrgetter
 
 from inspect import getdoc
-from fig.packages import dockerpty
+import dockerpty
 
 from .. import __version__
 from ..project import NoSuchService, ConfigurationError
diff --git a/fig/packages/__init__.py b/fig/packages/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/fig/packages/dockerpty/__init__.py b/fig/packages/dockerpty/__init__.py
deleted file mode 100644
index a5d707a47..000000000
--- a/fig/packages/dockerpty/__init__.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# dockerpty.
-#
-# Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from .pty import PseudoTerminal
-
-
-def start(client, container):
-    """
-    Present the PTY of the container inside the current process.
-
-    This is just a wrapper for PseudoTerminal(client, container).start()
-    """
-
-    PseudoTerminal(client, container).start()
diff --git a/fig/packages/dockerpty/io.py b/fig/packages/dockerpty/io.py
deleted file mode 100644
index c31c54010..000000000
--- a/fig/packages/dockerpty/io.py
+++ /dev/null
@@ -1,294 +0,0 @@
-# dockerpty: io.py
-#
-# Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import os
-import fcntl
-import errno
-import struct
-import select as builtin_select
-
-
-def set_blocking(fd, blocking=True):
-    """
-    Set the given file-descriptor blocking or non-blocking.
-
-    Returns the original blocking status.
-    """
-
-    old_flag = fcntl.fcntl(fd, fcntl.F_GETFL)
-
-    if blocking:
-        new_flag = old_flag &~ os.O_NONBLOCK
-    else:
-        new_flag = old_flag | os.O_NONBLOCK
-
-    fcntl.fcntl(fd, fcntl.F_SETFL, new_flag)
-
-    return not bool(old_flag & os.O_NONBLOCK)
-
-
-def select(read_streams, timeout=0):
-    """
-    Select the streams from `read_streams` that are ready for reading.
-
-    Uses `select.select()` internally but returns a flat list of streams.
-    """
-
-    write_streams = []
-    exception_streams = []
-
-    try:
-        return builtin_select.select(
-            read_streams,
-            write_streams,
-            exception_streams,
-            timeout,
-        )[0]
-    except builtin_select.error as e:
-        # POSIX signals interrupt select()
-        if e[0] == errno.EINTR:
-            return []
-        else:
-            raise e
-
-
-class Stream(object):
-    """
-    Generic Stream class.
-
-    This is a file-like abstraction on top of os.read() and os.write(), which
-    add consistency to the reading of sockets and files alike.
-    """
-
-
-    """
-    Recoverable IO/OS Errors.
-    """
-    ERRNO_RECOVERABLE = [
-        errno.EINTR,
-        errno.EDEADLK,
-        errno.EWOULDBLOCK,
-    ]
-
-
-    def __init__(self, fd):
-        """
-        Initialize the Stream for the file descriptor `fd`.
-
-        The `fd` object must have a `fileno()` method.
-        """
-        self.fd = fd
-
-
-    def fileno(self):
-        """
-        Return the fileno() of the file descriptor.
-        """
-
-        return self.fd.fileno()
-
-
-    def set_blocking(self, value):
-        if hasattr(self.fd, 'setblocking'):
-            self.fd.setblocking(value)
-            return True
-        else:
-            return set_blocking(self.fd, value)
-
-
-    def read(self, n=4096):
-        """
-        Return `n` bytes of data from the Stream, or None at end of stream.
-        """
-
-        try:
-            if hasattr(self.fd, 'recv'):
-                return self.fd.recv(n)
-            return os.read(self.fd.fileno(), n)
-        except EnvironmentError as e:
-            if e.errno not in Stream.ERRNO_RECOVERABLE:
-                raise e
-
-
-    def write(self, data):
-        """
-        Write `data` to the Stream.
-        """
-
-        if not data:
-            return None
-
-        while True:
-            try:
-                if hasattr(self.fd, 'send'):
-                    self.fd.send(data)
-                    return len(data)
-                os.write(self.fd.fileno(), data)
-                return len(data)
-            except EnvironmentError as e:
-                if e.errno not in Stream.ERRNO_RECOVERABLE:
-                    raise e
-
-    def __repr__(self):
-        return "{cls}({fd})".format(cls=type(self).__name__, fd=self.fd)
-
-
-class Demuxer(object):
-    """
-    Wraps a multiplexed Stream to read in data demultiplexed.
-
-    Docker multiplexes streams together when there is no PTY attached, by
-    sending an 8-byte header, followed by a chunk of data.
-
-    The first 4 bytes of the header denote the stream from which the data came
-    (i.e. 0x01 = stdout, 0x02 = stderr). Only the first byte of these initial 4
-    bytes is used.
-
-    The next 4 bytes indicate the length of the following chunk of data as an
-    integer in big endian format. This much data must be consumed before the
-    next 8-byte header is read.
-    """
-
-    def __init__(self, stream):
-        """
-        Initialize a new Demuxer reading from `stream`.
-        """
-
-        self.stream = stream
-        self.remain = 0
-
-
-    def fileno(self):
-        """
-        Returns the fileno() of the underlying Stream.
-
-        This is useful for select() to work.
-        """
-
-        return self.stream.fileno()
-
-
-    def set_blocking(self, value):
-        return self.stream.set_blocking(value)
-
-
-    def read(self, n=4096):
-        """
-        Read up to `n` bytes of data from the Stream, after demuxing.
-
-        Less than `n` bytes of data may be returned depending on the available
-        payload, but the number of bytes returned will never exceed `n`.
-
-        Because demuxing involves scanning 8-byte headers, the actual amount of
-        data read from the underlying stream may be greater than `n`.
-        """
-
-        size = self._next_packet_size(n)
-
-        if size <= 0:
-            return
-        else:
-            return self.stream.read(size)
-
-
-    def write(self, data):
-        """
-        Delegates the the underlying Stream.
-        """
-
-        return self.stream.write(data)
-
-
-    def _next_packet_size(self, n=0):
-        size = 0
-
-        if self.remain > 0:
-            size = min(n, self.remain)
-            self.remain -= size
-        else:
-            data = self.stream.read(8)
-            if data is None:
-                return 0
-            if len(data) == 8:
-                __, actual = struct.unpack('>BxxxL', data)
-                size = min(n, actual)
-                self.remain = actual - size
-
-        return size
-
-    def __repr__(self):
-        return "{cls}({stream})".format(cls=type(self).__name__,
-                                        stream=self.stream)
-
-
-class Pump(object):
-    """
-    Stream pump class.
-
-    A Pump wraps two Streams, reading from one and and writing its data into
-    the other, much like a pipe but manually managed.
-
-    This abstraction is used to facilitate piping data between the file
-    descriptors associated with the tty and those associated with a container's
-    allocated pty.
-
-    Pumps are selectable based on the 'read' end of the pipe.
-    """
-
-    def __init__(self, from_stream, to_stream):
-        """
-        Initialize a Pump with a Stream to read from and another to write to.
-        """
-
-        self.from_stream = from_stream
-        self.to_stream = to_stream
-
-
-    def fileno(self):
-        """
-        Returns the `fileno()` of the reader end of the Pump.
-
-        This is useful to allow Pumps to function with `select()`.
-        """
-
-        return self.from_stream.fileno()
-
-
-    def set_blocking(self, value):
-        return self.from_stream.set_blocking(value)
-
-
-    def flush(self, n=4096):
-        """
-        Flush `n` bytes of data from the reader Stream to the writer Stream.
-
-        Returns the number of bytes that were actually flushed. A return value
-        of zero is not an error.
-
-        If EOF has been reached, `None` is returned.
-        """
-
-        try:
-            return self.to_stream.write(self.from_stream.read(n))
-        except OSError as e:
-            if e.errno != errno.EPIPE:
-                raise e
-
-    def __repr__(self):
-        return "{cls}(from={from_stream}, to={to_stream})".format(
-            cls=type(self).__name__,
-            from_stream=self.from_stream,
-            to_stream=self.to_stream)
diff --git a/fig/packages/dockerpty/pty.py b/fig/packages/dockerpty/pty.py
deleted file mode 100644
index 4e11ca0aa..000000000
--- a/fig/packages/dockerpty/pty.py
+++ /dev/null
@@ -1,235 +0,0 @@
-# dockerpty: pty.py
-#
-# Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import sys
-import signal
-from ssl import SSLError
-
-from . import io
-from . import tty
-
-
-class WINCHHandler(object):
-    """
-    WINCH Signal handler to keep the PTY correctly sized.
-    """
-
-    def __init__(self, pty):
-        """
-        Initialize a new WINCH handler for the given PTY.
-
-        Initializing a handler has no immediate side-effects. The `start()`
-        method must be invoked for the signals to be trapped.
-        """
-
-        self.pty = pty
-        self.original_handler = None
-
-
-    def __enter__(self):
-        """
-        Invoked on entering a `with` block.
-        """
-
-        self.start()
-        return self
-
-
-    def __exit__(self, *_):
-        """
-        Invoked on exiting a `with` block.
-        """
-
-        self.stop()
-
-
-    def start(self):
-        """
-        Start trapping WINCH signals and resizing the PTY.
-
-        This method saves the previous WINCH handler so it can be restored on
-        `stop()`.
-        """
-
-        def handle(signum, frame):
-            if signum == signal.SIGWINCH:
-                self.pty.resize()
-
-        self.original_handler = signal.signal(signal.SIGWINCH, handle)
-
-
-    def stop(self):
-        """
-        Stop trapping WINCH signals and restore the previous WINCH handler.
-        """
-
-        if self.original_handler is not None:
-            signal.signal(signal.SIGWINCH, self.original_handler)
-
-
-class PseudoTerminal(object):
-    """
-    Wraps the pseudo-TTY (PTY) allocated to a docker container.
-
-    The PTY is managed via the current process' TTY until it is closed.
-
-    Example:
-
-        import docker
-        from dockerpty import PseudoTerminal
-
-        client = docker.Client()
-        container = client.create_container(
-            image='busybox:latest',
-            stdin_open=True,
-            tty=True,
-            command='/bin/sh',
-        )
-
-        # hijacks the current tty until the pty is closed
-        PseudoTerminal(client, container).start()
-
-    Care is taken to ensure all file descriptors are restored on exit. For
-    example, you can attach to a running container from within a Python REPL
-    and when the container exits, the user will be returned to the Python REPL
-    without adverse effects.
-    """
-
-
-    def __init__(self, client, container):
-        """
-        Initialize the PTY using the docker.Client instance and container dict.
-        """
-
-        self.client = client
-        self.container = container
-        self.raw = None
-
-
-    def start(self, **kwargs):
-        """
-        Present the PTY of the container inside the current process.
-
-        This will take over the current process' TTY until the container's PTY
-        is closed.
-        """
-
-        pty_stdin, pty_stdout, pty_stderr = self.sockets()
-
-        mappings = [
-            (io.Stream(sys.stdin), pty_stdin),
-            (pty_stdout, io.Stream(sys.stdout)),
-            (pty_stderr, io.Stream(sys.stderr)),
-        ]
-
-        pumps = [io.Pump(a, b) for (a, b) in mappings if a and b]
-
-        if not self.container_info()['State']['Running']:
-            self.client.start(self.container, **kwargs)
-
-        flags = [p.set_blocking(False) for p in pumps]
-
-        try:
-            with WINCHHandler(self):
-                self._hijack_tty(pumps)
-        finally:
-            if flags:
-                for (pump, flag) in zip(pumps, flags):
-                    io.set_blocking(pump, flag)
-
-
-    def israw(self):
-        """
-        Returns True if the PTY should operate in raw mode.
-
-        If the container was not started with tty=True, this will return False.
-        """
-
-        if self.raw is None:
-            info = self.container_info()
-            self.raw = sys.stdout.isatty() and info['Config']['Tty']
-
-        return self.raw
-
-
-    def sockets(self):
-        """
-        Returns a tuple of sockets connected to the pty (stdin,stdout,stderr).
-
-        If any of the sockets are not attached in the container, `None` is
-        returned in the tuple.
-        """
-
-        info = self.container_info()
-
-        def attach_socket(key):
-            if info['Config']['Attach{0}'.format(key.capitalize())]:
-                socket = self.client.attach_socket(
-                    self.container,
-                    {key: 1, 'stream': 1, 'logs': 1},
-                )
-                stream = io.Stream(socket)
-
-                if info['Config']['Tty']:
-                    return stream
-                else:
-                    return io.Demuxer(stream)
-            else:
-                return None
-
-        return map(attach_socket, ('stdin', 'stdout', 'stderr'))
-
-
-    def resize(self, size=None):
-        """
-        Resize the container's PTY.
-
-        If `size` is not None, it must be a tuple of (height,width), otherwise
-        it will be determined by the size of the current TTY.
-        """
-
-        if not self.israw():
-            return
-
-        size = size or tty.size(sys.stdout)
-
-        if size is not None:
-            rows, cols = size
-            try:
-                self.client.resize(self.container, height=rows, width=cols)
-            except IOError: # Container already exited
-                pass
-
-
-    def container_info(self):
-        """
-        Thin wrapper around client.inspect_container().
-        """
-
-        return self.client.inspect_container(self.container)
-
-
-    def _hijack_tty(self, pumps):
-        with tty.Terminal(sys.stdin, raw=self.israw()):
-            self.resize()
-            while True:
-                _ready = io.select(pumps, timeout=60)
-                try:
-                    if all([p.flush() is None for p in pumps]):
-                        break
-                except SSLError as e:
-                    if 'The operation did not complete' not in e.strerror:
-                        raise e
diff --git a/fig/packages/dockerpty/tty.py b/fig/packages/dockerpty/tty.py
deleted file mode 100644
index bd2ccb5ad..000000000
--- a/fig/packages/dockerpty/tty.py
+++ /dev/null
@@ -1,130 +0,0 @@
-# dockerpty: tty.py
-#
-# Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from __future__ import absolute_import
-
-import os
-import termios
-import tty
-import fcntl
-import struct
-
-
-def size(fd):
-    """
-    Return a tuple (rows,cols) representing the size of the TTY `fd`.
-
-    The provided file descriptor should be the stdout stream of the TTY.
-
-    If the TTY size cannot be determined, returns None.
-    """
-
-    if not os.isatty(fd.fileno()):
-        return None
-
-    try:
-        dims = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, 'hhhh'))
-    except:
-        try:
-            dims = (os.environ['LINES'], os.environ['COLUMNS'])
-        except:
-            return None
-
-    return dims
-
-
-class Terminal(object):
-    """
-    Terminal provides wrapper functionality to temporarily make the tty raw.
-
-    This is useful when streaming data from a pseudo-terminal into the tty.
-
-    Example:
-
-        with Terminal(sys.stdin, raw=True):
-            do_things_in_raw_mode()
-    """
-
-    def __init__(self, fd, raw=True):
-        """
-        Initialize a terminal for the tty with stdin attached to `fd`.
-
-        Initializing the Terminal has no immediate side effects. The `start()`
-        method must be invoked, or `with raw_terminal:` used before the
-        terminal is affected.
-        """
-
-        self.fd = fd
-        self.raw = raw
-        self.original_attributes = None
-
-
-    def __enter__(self):
-        """
-        Invoked when a `with` block is first entered.
-        """
-
-        self.start()
-        return self
-
-
-    def __exit__(self, *_):
-        """
-        Invoked when a `with` block is finished.
-        """
-
-        self.stop()
-
-
-    def israw(self):
-        """
-        Returns True if the TTY should operate in raw mode.
-        """
-
-        return self.raw
-
-
-    def start(self):
-        """
-        Saves the current terminal attributes and makes the tty raw.
-
-        This method returns None immediately.
-        """
-
-        if os.isatty(self.fd.fileno()) and self.israw():
-            self.original_attributes = termios.tcgetattr(self.fd)
-            tty.setraw(self.fd)
-
-
-    def stop(self):
-        """
-        Restores the terminal attributes back to before setting raw mode.
-
-        If the raw terminal was not started, does nothing.
-        """
-
-        if self.original_attributes is not None:
-            termios.tcsetattr(
-                self.fd,
-                termios.TCSADRAIN,
-                self.original_attributes,
-            )
-
-    def __repr__(self):
-        return "{cls}({fd}, raw={raw})".format(
-            cls=type(self).__name__,
-            fd=self.fd,
-            raw=self.raw)
diff --git a/requirements.txt b/requirements.txt
index 1ed9a95dc..59aa90f02 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,6 @@
 PyYAML==3.10
 docker-py==0.5.3
+dockerpty==0.3.2
 docopt==0.6.1
 requests==2.2.1
 six==1.7.3
diff --git a/script/test b/script/test
index 2ed3f6b4f..79cc7e6b2 100755
--- a/script/test
+++ b/script/test
@@ -1,12 +1,5 @@
 #!/bin/sh
 set -ex
-
-target="tests"
-
-if [[ -n "$@" ]]; then
-  target="$@"
-fi
-
 docker build -t fig .
-docker run -v /var/run/docker.sock:/var/run/docker.sock fig flake8 --exclude=packages fig
-docker run -v /var/run/docker.sock:/var/run/docker.sock fig nosetests $target
+docker run -v /var/run/docker.sock:/var/run/docker.sock fig flake8 fig
+docker run -v /var/run/docker.sock:/var/run/docker.sock fig nosetests $@
diff --git a/setup.py b/setup.py
index a4aa23a62..fe839f5ad 100644
--- a/setup.py
+++ b/setup.py
@@ -31,6 +31,7 @@ install_requires = [
     'texttable >= 0.8.1, < 0.9',
     'websocket-client >= 0.11.0, < 0.12',
     'docker-py >= 0.5.3, < 0.6',
+    'dockerpty >= 0.3.2, < 0.4',
     'six >= 1.3.0, < 2',
 ]
 
diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py
index ceb93f62b..3929502d7 100644
--- a/tests/integration/cli_test.py
+++ b/tests/integration/cli_test.py
@@ -139,13 +139,13 @@ class CLITestCase(DockerClientTestCase):
 
         self.assertEqual(old_ids, new_ids)
 
-    @patch('fig.packages.dockerpty.start')
+    @patch('dockerpty.start')
     def test_run_service_without_links(self, mock_stdout):
         self.command.base_dir = 'tests/fixtures/links-figfile'
         self.command.dispatch(['run', 'console', '/bin/true'], None)
         self.assertEqual(len(self.project.containers()), 0)
 
-    @patch('fig.packages.dockerpty.start')
+    @patch('dockerpty.start')
     def test_run_service_with_links(self, __):
         self.command.base_dir = 'tests/fixtures/links-figfile'
         self.command.dispatch(['run', 'web', '/bin/true'], None)
@@ -154,14 +154,14 @@ class CLITestCase(DockerClientTestCase):
         self.assertEqual(len(db.containers()), 1)
         self.assertEqual(len(console.containers()), 0)
 
-    @patch('fig.packages.dockerpty.start')
+    @patch('dockerpty.start')
     def test_run_with_no_deps(self, __):
         self.command.base_dir = 'tests/fixtures/links-figfile'
         self.command.dispatch(['run', '--no-deps', 'web', '/bin/true'], None)
         db = self.project.get_service('db')
         self.assertEqual(len(db.containers()), 0)
 
-    @patch('fig.packages.dockerpty.start')
+    @patch('dockerpty.start')
     def test_run_does_not_recreate_linked_containers(self, __):
         self.command.base_dir = 'tests/fixtures/links-figfile'
         self.command.dispatch(['up', '-d', 'db'], None)
@@ -177,7 +177,7 @@ class CLITestCase(DockerClientTestCase):
 
         self.assertEqual(old_ids, new_ids)
 
-    @patch('fig.packages.dockerpty.start')
+    @patch('dockerpty.start')
     def test_run_without_command(self, __):
         self.command.base_dir = 'tests/fixtures/commands-figfile'
         self.check_build('tests/fixtures/simple-dockerfile', tag='figtest_test')
@@ -201,7 +201,7 @@ class CLITestCase(DockerClientTestCase):
             [u'/bin/true'],
         )
 
-    @patch('fig.packages.dockerpty.start')
+    @patch('dockerpty.start')
     def test_run_service_with_entrypoint_overridden(self, _):
         self.command.base_dir = 'tests/fixtures/dockerfile_with_entrypoint'
         name = 'service'
@@ -216,7 +216,7 @@ class CLITestCase(DockerClientTestCase):
             u'/bin/echo helloworld'
         )
 
-    @patch('fig.packages.dockerpty.start')
+    @patch('dockerpty.start')
     def test_run_service_with_environement_overridden(self, _):
         name = 'service'
         self.command.base_dir = 'tests/fixtures/environment-figfile'