pty.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. # dockerpty: pty.py
  2. #
  3. # Copyright 2014 Chris Corbyn <[email protected]>
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. import sys
  17. import signal
  18. from ssl import SSLError
  19. from . import io
  20. from . import tty
  21. class WINCHHandler(object):
  22. """
  23. WINCH Signal handler to keep the PTY correctly sized.
  24. """
  25. def __init__(self, pty):
  26. """
  27. Initialize a new WINCH handler for the given PTY.
  28. Initializing a handler has no immediate side-effects. The `start()`
  29. method must be invoked for the signals to be trapped.
  30. """
  31. self.pty = pty
  32. self.original_handler = None
  33. def __enter__(self):
  34. """
  35. Invoked on entering a `with` block.
  36. """
  37. self.start()
  38. return self
  39. def __exit__(self, *_):
  40. """
  41. Invoked on exiting a `with` block.
  42. """
  43. self.stop()
  44. def start(self):
  45. """
  46. Start trapping WINCH signals and resizing the PTY.
  47. This method saves the previous WINCH handler so it can be restored on
  48. `stop()`.
  49. """
  50. def handle(signum, frame):
  51. if signum == signal.SIGWINCH:
  52. self.pty.resize()
  53. self.original_handler = signal.signal(signal.SIGWINCH, handle)
  54. def stop(self):
  55. """
  56. Stop trapping WINCH signals and restore the previous WINCH handler.
  57. """
  58. if self.original_handler is not None:
  59. signal.signal(signal.SIGWINCH, self.original_handler)
  60. class PseudoTerminal(object):
  61. """
  62. Wraps the pseudo-TTY (PTY) allocated to a docker container.
  63. The PTY is managed via the current process' TTY until it is closed.
  64. Example:
  65. import docker
  66. from dockerpty import PseudoTerminal
  67. client = docker.Client()
  68. container = client.create_container(
  69. image='busybox:latest',
  70. stdin_open=True,
  71. tty=True,
  72. command='/bin/sh',
  73. )
  74. # hijacks the current tty until the pty is closed
  75. PseudoTerminal(client, container).start()
  76. Care is taken to ensure all file descriptors are restored on exit. For
  77. example, you can attach to a running container from within a Python REPL
  78. and when the container exits, the user will be returned to the Python REPL
  79. without adverse effects.
  80. """
  81. def __init__(self, client, container):
  82. """
  83. Initialize the PTY using the docker.Client instance and container dict.
  84. """
  85. self.client = client
  86. self.container = container
  87. self.raw = None
  88. def start(self, **kwargs):
  89. """
  90. Present the PTY of the container inside the current process.
  91. This will take over the current process' TTY until the container's PTY
  92. is closed.
  93. """
  94. pty_stdin, pty_stdout, pty_stderr = self.sockets()
  95. mappings = [
  96. (io.Stream(sys.stdin), pty_stdin),
  97. (pty_stdout, io.Stream(sys.stdout)),
  98. (pty_stderr, io.Stream(sys.stderr)),
  99. ]
  100. pumps = [io.Pump(a, b) for (a, b) in mappings if a and b]
  101. if not self.container_info()['State']['Running']:
  102. self.client.start(self.container, **kwargs)
  103. flags = [p.set_blocking(False) for p in pumps]
  104. try:
  105. with WINCHHandler(self):
  106. self._hijack_tty(pumps)
  107. finally:
  108. if flags:
  109. for (pump, flag) in zip(pumps, flags):
  110. io.set_blocking(pump, flag)
  111. def israw(self):
  112. """
  113. Returns True if the PTY should operate in raw mode.
  114. If the container was not started with tty=True, this will return False.
  115. """
  116. if self.raw is None:
  117. info = self.container_info()
  118. self.raw = sys.stdout.isatty() and info['Config']['Tty']
  119. return self.raw
  120. def sockets(self):
  121. """
  122. Returns a tuple of sockets connected to the pty (stdin,stdout,stderr).
  123. If any of the sockets are not attached in the container, `None` is
  124. returned in the tuple.
  125. """
  126. info = self.container_info()
  127. def attach_socket(key):
  128. if info['Config']['Attach{0}'.format(key.capitalize())]:
  129. socket = self.client.attach_socket(
  130. self.container,
  131. {key: 1, 'stream': 1, 'logs': 1},
  132. )
  133. stream = io.Stream(socket)
  134. if info['Config']['Tty']:
  135. return stream
  136. else:
  137. return io.Demuxer(stream)
  138. else:
  139. return None
  140. return map(attach_socket, ('stdin', 'stdout', 'stderr'))
  141. def resize(self, size=None):
  142. """
  143. Resize the container's PTY.
  144. If `size` is not None, it must be a tuple of (height,width), otherwise
  145. it will be determined by the size of the current TTY.
  146. """
  147. if not self.israw():
  148. return
  149. size = size or tty.size(sys.stdout)
  150. if size is not None:
  151. rows, cols = size
  152. try:
  153. self.client.resize(self.container, height=rows, width=cols)
  154. except IOError: # Container already exited
  155. pass
  156. def container_info(self):
  157. """
  158. Thin wrapper around client.inspect_container().
  159. """
  160. return self.client.inspect_container(self.container)
  161. def _hijack_tty(self, pumps):
  162. with tty.Terminal(sys.stdin, raw=self.israw()):
  163. self.resize()
  164. while True:
  165. _ready = io.select(pumps, timeout=60)
  166. try:
  167. if all([p.flush() is None for p in pumps]):
  168. break
  169. except SSLError as e:
  170. if 'The operation did not complete' not in e.strerror:
  171. raise e