ptyprocess.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. # -*- coding: utf-8 -*-
  2. # Standard library imports
  3. import os
  4. import shlex
  5. import signal
  6. import socket
  7. import subprocess
  8. import threading
  9. import time
  10. from shutil import which
  11. # Local imports
  12. from .winpty import PTY
  13. class PtyProcess(object):
  14. """This class represents a process running in a pseudoterminal.
  15. The main constructor is the :meth:`spawn` classmethod.
  16. """
  17. def __init__(self, pty):
  18. assert isinstance(pty, PTY)
  19. self.pty = pty
  20. self.pid = pty.pid
  21. # self.fd = pty.fd
  22. self.argv = None
  23. self.env = None
  24. self.launch_dir = None
  25. self.read_blocking = bool(int(os.environ.get('PYWINPTY_BLOCK', 1)))
  26. self.closed = False
  27. self.flag_eof = False
  28. # Used by terminate() to give kernel time to update process status.
  29. # Time in seconds.
  30. self.delayafterterminate = 0.1
  31. # Used by close() to give kernel time to update process status.
  32. # Time in seconds.
  33. self.delayafterclose = 0.1
  34. # Set up our file reader sockets.
  35. self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  36. self._server.bind(("127.0.0.1", 0))
  37. address = self._server.getsockname()
  38. self._server.listen(1)
  39. # Read from the pty in a thread.
  40. self._thread = threading.Thread(target=_read_in_thread,
  41. args=(address, self.pty, self.read_blocking))
  42. self._thread.daemon = True
  43. self._thread.start()
  44. self.fileobj, _ = self._server.accept()
  45. self.fd = self.fileobj.fileno()
  46. @classmethod
  47. def spawn(cls, argv, cwd=None, env=None, dimensions=(24, 80),
  48. backend=None):
  49. """Start the given command in a child process in a pseudo terminal.
  50. This does all the setting up the pty, and returns an instance of
  51. PtyProcess.
  52. Dimensions of the psuedoterminal used for the subprocess can be
  53. specified as a tuple (rows, cols), or the default (24, 80) will be
  54. used.
  55. """
  56. if isinstance(argv, str):
  57. argv = shlex.split(argv, posix=False)
  58. if not isinstance(argv, (list, tuple)):
  59. raise TypeError("Expected a list or tuple for argv, got %r" % argv)
  60. # Shallow copy of argv so we can modify it
  61. _argv: list[str] = list(argv[:])
  62. command = _argv[0]
  63. env = env or os.environ
  64. path = env.get('PATH', os.defpath)
  65. command_with_path = which(command, path=path)
  66. if command_with_path is None:
  67. raise FileNotFoundError(
  68. 'The command was not found or was not ' +
  69. 'executable: %s.' % command
  70. )
  71. command = command_with_path
  72. _argv[0] = command
  73. cmdline = ' ' + subprocess.list2cmdline(_argv[1:])
  74. cwd = cwd or os.getcwd()
  75. backend = backend or os.environ.get('PYWINPTY_BACKEND', None)
  76. backend = int(backend) if backend is not None else backend
  77. proc = PTY(dimensions[1], dimensions[0],
  78. backend=backend)
  79. # Create the environment string.
  80. envStrs = []
  81. for (key, value) in env.items():
  82. envStrs.append('%s=%s' % (key, value))
  83. env = '\0'.join(envStrs) + '\0'
  84. # command = bytes(command, encoding)
  85. # cwd = bytes(cwd, encoding)
  86. # cmdline = bytes(cmdline, encoding)
  87. # env = bytes(env, encoding)
  88. if len(_argv) == 1:
  89. proc.spawn(command, cwd=cwd, env=env)
  90. else:
  91. proc.spawn(command, cwd=cwd, env=env, cmdline=cmdline)
  92. inst = cls(proc)
  93. inst._winsize = dimensions
  94. # Set some informational attributes
  95. inst.argv = _argv
  96. if env is not None:
  97. inst.env = env
  98. if cwd is not None:
  99. inst.launch_dir = cwd
  100. return inst
  101. @property
  102. def exitstatus(self):
  103. """The exit status of the process.
  104. """
  105. return self.pty.get_exitstatus()
  106. def fileno(self):
  107. """This returns the file descriptor of the pty for the child.
  108. """
  109. return self.fd
  110. def close(self, force=False):
  111. """This closes the connection with the child application. Note that
  112. calling close() more than once is valid. This emulates standard Python
  113. behavior with files. Set force to True if you want to make sure that
  114. the child is terminated (SIGKILL is sent if the child ignores
  115. SIGINT)."""
  116. if not self.closed:
  117. self.fileobj.close()
  118. self._server.close()
  119. # Give kernel time to update process status.
  120. time.sleep(self.delayafterclose)
  121. if self.isalive():
  122. if not self.terminate(force):
  123. raise IOError('Could not terminate the child.')
  124. self.fd = -1
  125. self.closed = True
  126. # del self.pty
  127. def __del__(self):
  128. """This makes sure that no system resources are left open. Python only
  129. garbage collects Python objects. OS file descriptors are not Python
  130. objects, so they must be handled explicitly. If the child file
  131. descriptor was opened outside of this class (passed to the constructor)
  132. then this does not close it.
  133. """
  134. # It is possible for __del__ methods to execute during the
  135. # teardown of the Python VM itself. Thus self.close() may
  136. # trigger an exception because os.close may be None.
  137. try:
  138. self.close()
  139. except Exception:
  140. pass
  141. def flush(self):
  142. """This does nothing. It is here to support the interface for a
  143. File-like object. """
  144. pass
  145. def isatty(self):
  146. """This returns True if the file descriptor is open and connected to a
  147. tty(-like) device, else False."""
  148. return self.isalive()
  149. def read(self, size=1024):
  150. """Read and return at most ``size`` characters from the pty.
  151. Can block if there is nothing to read. Raises :exc:`EOFError` if the
  152. terminal was closed.
  153. """
  154. # try:
  155. # data = self.pty.read(size, blocking=self.read_blocking)
  156. # except Exception as e:
  157. # if "EOF" in str(e):
  158. # raise EOFError(e) from e
  159. # return data
  160. data = self.fileobj.recv(size)
  161. if not data:
  162. self.flag_eof = True
  163. raise EOFError('Pty is closed')
  164. if data == b'0011Ignore':
  165. data = b''
  166. err = True
  167. while err and data:
  168. try:
  169. data.decode('utf-8')
  170. err = False
  171. except UnicodeDecodeError:
  172. data += self.fileobj.recv(1)
  173. return data.decode('utf-8')
  174. def readline(self):
  175. """Read one line from the pseudoterminal as bytes.
  176. Can block if there is nothing to read. Raises :exc:`EOFError` if the
  177. terminal was closed.
  178. """
  179. buf = []
  180. while 1:
  181. try:
  182. ch = self.read(1)
  183. except EOFError:
  184. return ''.join(buf)
  185. buf.append(ch)
  186. if ch == '\n':
  187. return ''.join(buf)
  188. def write(self, s):
  189. """Write the string ``s`` to the pseudoterminal.
  190. Returns the number of bytes written.
  191. """
  192. if not self.pty.isalive():
  193. raise EOFError('Pty is closed')
  194. nbytes = self.pty.write(s)
  195. return nbytes
  196. def terminate(self, force=False):
  197. """This forces a child process to terminate."""
  198. if not self.isalive():
  199. return True
  200. self.kill(signal.SIGINT)
  201. try:
  202. self.pty.cancel_io()
  203. except Exception:
  204. pass
  205. time.sleep(self.delayafterterminate)
  206. if not self.isalive():
  207. return True
  208. if force:
  209. self.kill(signal.SIGTERM)
  210. time.sleep(self.delayafterterminate)
  211. if not self.isalive():
  212. return True
  213. else:
  214. return False
  215. def wait(self):
  216. """This waits until the child exits. This is a blocking call. This will
  217. not read any data from the child.
  218. """
  219. while self.isalive():
  220. time.sleep(0.1)
  221. return self.exitstatus
  222. def isalive(self):
  223. """This tests if the child process is running or not. This is
  224. non-blocking. If the child was terminated then this will read the
  225. exitstatus or signalstatus of the child. This returns True if the child
  226. process appears to be running or False if not.
  227. """
  228. alive = self.pty.isalive()
  229. self.closed = not alive
  230. return alive
  231. def kill(self, sig):
  232. """Kill the process with the given signal.
  233. """
  234. if self.pid is None:
  235. return
  236. os.kill(self.pid, sig)
  237. def sendcontrol(self, char):
  238. '''Helper method that wraps send() with mnemonic access for sending control
  239. character to the child (such as Ctrl-C or Ctrl-D). For example, to send
  240. Ctrl-G (ASCII 7, bell, '\a')::
  241. child.sendcontrol('g')
  242. See also, sendintr() and sendeof().
  243. '''
  244. char = char.lower()
  245. a = ord(char)
  246. if 97 <= a <= 122:
  247. a = a - ord('a') + 1
  248. byte = bytes([a]).decode("ascii")
  249. return self.pty.write(byte), byte
  250. d = {'@': 0, '`': 0,
  251. '[': 27, '{': 27,
  252. '\\': 28, '|': 28,
  253. ']': 29, '}': 29,
  254. '^': 30, '~': 30,
  255. '_': 31,
  256. '?': 127}
  257. if char not in d:
  258. return 0, ''
  259. byte = bytes([d[char]]).decode("ascii")
  260. return self.pty.write(byte), byte
  261. def sendeof(self):
  262. """This sends an EOF to the child. This sends a character which causes
  263. the pending parent output buffer to be sent to the waiting child
  264. program without waiting for end-of-line. If it is the first character
  265. of the line, the read() in the user program returns 0, which signifies
  266. end-of-file. This means to work as expected a sendeof() has to be
  267. called at the beginning of a line. This method does not send a newline.
  268. It is the responsibility of the caller to ensure the eof is sent at the
  269. beginning of a line."""
  270. # Send control character 4 (Ctrl-D)
  271. self.pty.write('\x04')
  272. def sendintr(self):
  273. """This sends a SIGINT to the child. It does not require
  274. the SIGINT to be the first character on a line. """
  275. # Send control character 3 (Ctrl-C)
  276. self.pty.write('\x03')
  277. def eof(self):
  278. """This returns True if the EOF exception was ever raised.
  279. """
  280. return self.flag_eof
  281. def getwinsize(self):
  282. """Return the window size of the pseudoterminal as a tuple (rows, cols).
  283. """
  284. return self._winsize
  285. def setwinsize(self, rows, cols):
  286. """Set the terminal window size of the child tty.
  287. """
  288. self._winsize = (rows, cols)
  289. self.pty.set_size(cols, rows)
  290. def _read_in_thread(address, pty: PTY, blocking: bool):
  291. """Read data from the pty in a thread.
  292. """
  293. client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  294. client.connect(address)
  295. call = 0
  296. while 1:
  297. try:
  298. data = pty.read(blocking=blocking) or '0011Ignore'
  299. try:
  300. client.send(bytes(data, 'utf-8'))
  301. except socket.error:
  302. break
  303. # Handle end of file.
  304. if pty.iseof():
  305. try:
  306. client.send(b'')
  307. except socket.error:
  308. pass
  309. break
  310. call += 1
  311. except Exception as e:
  312. break
  313. time.sleep(1e-3)
  314. client.close()