_process_posix.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. """Posix-specific implementation of process utilities.
  2. This file is only meant to be imported by process.py, not by end-users.
  3. """
  4. #-----------------------------------------------------------------------------
  5. # Imports
  6. #-----------------------------------------------------------------------------
  7. # Stdlib
  8. import errno
  9. import os
  10. import subprocess as sp
  11. import sys
  12. # Our own
  13. from ._process_common import getoutput as getoutput, arg_split
  14. from IPython.utils.encoding import DEFAULT_ENCODING
  15. __all__ = ["getoutput", "arg_split", "system", "check_pid"]
  16. #-----------------------------------------------------------------------------
  17. # Function definitions
  18. #-----------------------------------------------------------------------------
  19. class ProcessHandler:
  20. """Execute subprocesses under the control of pexpect.
  21. """
  22. # Timeout in seconds to wait on each reading of the subprocess' output.
  23. # This should not be set too low to avoid cpu overusage from our side,
  24. # since we read in a loop whose period is controlled by this timeout.
  25. read_timeout: float = 0.05
  26. # Timeout to give a process if we receive SIGINT, between sending the
  27. # SIGINT to the process and forcefully terminating it.
  28. terminate_timeout: float = 0.2
  29. # File object where stdout and stderr of the subprocess will be written
  30. logfile = None
  31. # Shell to call for subprocesses to execute
  32. _sh: str | None = None
  33. @property
  34. def sh(self) -> str | None:
  35. if self._sh is None:
  36. import pexpect
  37. shell_name = os.environ.get("SHELL", "sh")
  38. self._sh = pexpect.which(shell_name)
  39. if self._sh is None:
  40. raise OSError('"{}" shell not found'.format(shell_name))
  41. return self._sh
  42. def __init__(self) -> None:
  43. """Arguments are used for pexpect calls."""
  44. self.logfile = sys.stdout
  45. def getoutput(self, cmd: str) -> str | None:
  46. """Run a command and return its stdout/stderr as a string.
  47. Parameters
  48. ----------
  49. cmd : str
  50. A command to be executed in the system shell.
  51. Returns
  52. -------
  53. output : str
  54. A string containing the combination of stdout and stderr from the
  55. subprocess, in whatever order the subprocess originally wrote to its
  56. file descriptors (so the order of the information in this string is the
  57. correct order as would be seen if running the command in a terminal).
  58. """
  59. import pexpect
  60. assert self.sh is not None
  61. try:
  62. res = pexpect.run(self.sh, args=["-c", cmd])
  63. assert isinstance(res, str)
  64. return res.replace("\r\n", "\n")
  65. except KeyboardInterrupt:
  66. print('^C', file=sys.stderr, end='')
  67. return None
  68. def system(self, cmd: str) -> int:
  69. """Execute a command in a subshell.
  70. Parameters
  71. ----------
  72. cmd : str
  73. A command to be executed in the system shell.
  74. Returns
  75. -------
  76. int : child's exitstatus
  77. """
  78. import pexpect
  79. # Get likely encoding for the output.
  80. enc = DEFAULT_ENCODING
  81. # Patterns to match on the output, for pexpect. We read input and
  82. # allow either a short timeout or EOF
  83. patterns = [pexpect.TIMEOUT, pexpect.EOF]
  84. # the index of the EOF pattern in the list.
  85. # even though we know it's 1, this call means we don't have to worry if
  86. # we change the above list, and forget to change this value:
  87. EOF_index = patterns.index(pexpect.EOF)
  88. # The size of the output stored so far in the process output buffer.
  89. # Since pexpect only appends to this buffer, each time we print we
  90. # record how far we've printed, so that next time we only print *new*
  91. # content from the buffer.
  92. out_size = 0
  93. assert self.sh is not None
  94. try:
  95. # Since we're not really searching the buffer for text patterns, we
  96. # can set pexpect's search window to be tiny and it won't matter.
  97. # We only search for the 'patterns' timeout or EOF, which aren't in
  98. # the text itself.
  99. #child = pexpect.spawn(pcmd, searchwindowsize=1)
  100. if hasattr(pexpect, 'spawnb'):
  101. child = pexpect.spawnb(self.sh, args=['-c', cmd]) # Pexpect-U
  102. else:
  103. child = pexpect.spawn(self.sh, args=['-c', cmd]) # Vanilla Pexpect
  104. flush = sys.stdout.flush
  105. while True:
  106. # res is the index of the pattern that caused the match, so we
  107. # know whether we've finished (if we matched EOF) or not
  108. res_idx = child.expect_list(patterns, self.read_timeout)
  109. print(child.before[out_size:].decode(enc, 'replace'), end='')
  110. flush()
  111. if res_idx==EOF_index:
  112. break
  113. # Update the pointer to what we've already printed
  114. out_size = len(child.before)
  115. except KeyboardInterrupt:
  116. # We need to send ^C to the process. The ascii code for '^C' is 3
  117. # (the character is known as ETX for 'End of Text', see
  118. # curses.ascii.ETX).
  119. child.sendline(chr(3))
  120. # Read and print any more output the program might produce on its
  121. # way out.
  122. try:
  123. out_size = len(child.before)
  124. child.expect_list(patterns, self.terminate_timeout)
  125. print(child.before[out_size:].decode(enc, 'replace'), end='')
  126. sys.stdout.flush()
  127. except KeyboardInterrupt:
  128. # Impatient users tend to type it multiple times
  129. pass
  130. finally:
  131. # Ensure the subprocess really is terminated
  132. child.terminate(force=True)
  133. # add isalive check, to ensure exitstatus is set:
  134. child.isalive()
  135. # We follow the subprocess pattern, returning either the exit status
  136. # as a positive number, or the terminating signal as a negative
  137. # number.
  138. # on Linux, sh returns 128+n for signals terminating child processes on Linux
  139. # on BSD (OS X), the signal code is set instead
  140. if child.exitstatus is None:
  141. # on WIFSIGNALED, pexpect sets signalstatus, leaving exitstatus=None
  142. if child.signalstatus is None:
  143. # this condition may never occur,
  144. # but let's be certain we always return an integer.
  145. return 0
  146. return -child.signalstatus
  147. if child.exitstatus > 128:
  148. return -(child.exitstatus - 128)
  149. return child.exitstatus
  150. # Make system() with a functional interface for outside use. Note that we use
  151. # getoutput() from the _common utils, which is built on top of popen(). Using
  152. # pexpect to get subprocess output produces difficult to parse output, since
  153. # programs think they are talking to a tty and produce highly formatted output
  154. # (ls is a good example) that makes them hard.
  155. system = ProcessHandler().system
  156. def check_pid(pid: int) -> bool:
  157. try:
  158. os.kill(pid, 0)
  159. except OSError as err:
  160. if err.errno == errno.ESRCH:
  161. return False
  162. elif err.errno == errno.EPERM:
  163. # Don't have permission to signal the process - probably means it exists
  164. return True
  165. raise
  166. else:
  167. return True