| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 | # dockerpty: pty.py## Copyright 2014 Chris Corbyn <[email protected]>## 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 sysimport signalfrom ssl import SSLErrorfrom . import iofrom . import ttyclass 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
 |