delegator.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import os
  2. import subprocess
  3. import shlex
  4. import signal
  5. from pexpect.popen_spawn import PopenSpawn
  6. # Include `unicode` in STR_TYPES for Python 2.X
  7. try:
  8. STR_TYPES = (str, unicode)
  9. except NameError:
  10. STR_TYPES = (str, )
  11. TIMEOUT = 30
  12. class Command(object):
  13. def __init__(self, cmd, timeout=TIMEOUT):
  14. super(Command, self).__init__()
  15. self.cmd = cmd
  16. self.timeout = timeout
  17. self.subprocess = None
  18. self.blocking = None
  19. self.was_run = False
  20. self.__out = None
  21. self.__err = None
  22. def __repr__(self):
  23. return '<Command {!r}>'.format(self.cmd)
  24. @property
  25. def _popen_args(self):
  26. return self.cmd
  27. @property
  28. def _default_popen_kwargs(self):
  29. return {
  30. 'env': os.environ.copy(),
  31. 'stdin': subprocess.PIPE,
  32. 'stdout': subprocess.PIPE,
  33. 'stderr': subprocess.PIPE,
  34. 'shell': True,
  35. 'universal_newlines': True,
  36. 'bufsize': 0,
  37. }
  38. @property
  39. def _default_pexpect_kwargs(self):
  40. return {
  41. 'env': os.environ.copy(),
  42. 'encoding': 'utf-8',
  43. 'timeout': self.timeout
  44. }
  45. @property
  46. def _uses_subprocess(self):
  47. return isinstance(self.subprocess, subprocess.Popen)
  48. @property
  49. def _uses_pexpect(self):
  50. return isinstance(self.subprocess, PopenSpawn)
  51. @property
  52. def std_out(self):
  53. return self.subprocess.stdout
  54. @property
  55. def _pexpect_out(self):
  56. if self.subprocess.encoding:
  57. result = ''
  58. else:
  59. result = b''
  60. if self.subprocess.before:
  61. result += self.subprocess.before
  62. if self.subprocess.after:
  63. result += self.subprocess.after
  64. result += self.subprocess.read()
  65. return result
  66. @property
  67. def out(self):
  68. """Std/out output (cached)"""
  69. if self.__out is not None:
  70. return self.__out
  71. if self._uses_subprocess:
  72. self.__out = self.std_out.read()
  73. else:
  74. self.__out = self._pexpect_out
  75. return self.__out
  76. @property
  77. def std_err(self):
  78. return self.subprocess.stderr
  79. @property
  80. def err(self):
  81. """Std/err output (cached)"""
  82. if self.__err is not None:
  83. return self.__err
  84. if self._uses_subprocess:
  85. self.__err = self.std_err.read()
  86. return self.__err
  87. else:
  88. return self._pexpect_out
  89. @property
  90. def pid(self):
  91. """The process' PID."""
  92. # Support for pexpect's functionality.
  93. if hasattr(self.subprocess, 'proc'):
  94. return self.subprocess.proc.pid
  95. # Standard subprocess method.
  96. return self.subprocess.pid
  97. @property
  98. def return_code(self):
  99. # Support for pexpect's functionality.
  100. if self._uses_pexpect:
  101. return self.subprocess.exitstatus
  102. # Standard subprocess method.
  103. return self.subprocess.returncode
  104. @property
  105. def std_in(self):
  106. return self.subprocess.stdin
  107. def run(self, block=True, binary=False):
  108. """Runs the given command, with or without pexpect functionality enabled."""
  109. self.blocking = block
  110. # Use subprocess.
  111. if self.blocking:
  112. popen_kwargs = self._default_popen_kwargs.copy()
  113. popen_kwargs['universal_newlines'] = not binary
  114. s = subprocess.Popen(self._popen_args, **popen_kwargs)
  115. # Otherwise, use pexpect.
  116. else:
  117. pexpect_kwargs = self._default_pexpect_kwargs.copy()
  118. if binary:
  119. pexpect_kwargs['encoding'] = None
  120. # Enable Python subprocesses to work with expect functionality.
  121. pexpect_kwargs['env']['PYTHONUNBUFFERED'] = '1'
  122. s = PopenSpawn(self._popen_args, **pexpect_kwargs)
  123. self.subprocess = s
  124. self.was_run = True
  125. def expect(self, pattern, timeout=-1):
  126. """Waits on the given pattern to appear in std_out"""
  127. if self.blocking:
  128. raise RuntimeError('expect can only be used on non-blocking commands.')
  129. self.subprocess.expect(pattern=pattern, timeout=timeout)
  130. def send(self, s, end=os.linesep, signal=False):
  131. """Sends the given string or signal to std_in."""
  132. if self.blocking:
  133. raise RuntimeError('send can only be used on non-blocking commands.')
  134. if not signal:
  135. if self._uses_subprocess:
  136. return self.subprocess.communicate(s + end)
  137. else:
  138. return self.subprocess.send(s + end)
  139. else:
  140. self.subprocess.send_signal(s)
  141. def terminate(self):
  142. self.subprocess.terminate()
  143. def kill(self):
  144. self.subprocess.kill(signal.SIGINT)
  145. def block(self):
  146. """Blocks until process is complete."""
  147. if self._uses_subprocess:
  148. # consume stdout and stderr
  149. stdout, stderr = self.subprocess.communicate()
  150. self.__out = stdout
  151. self.__err = stderr
  152. else:
  153. self.subprocess.wait()
  154. def pipe(self, command, timeout=None):
  155. """Runs the current command and passes its output to the next
  156. given process.
  157. """
  158. if not timeout:
  159. timeout = self.timeout
  160. if not self.was_run:
  161. self.run(block=False)
  162. data = self.out
  163. if timeout:
  164. c = Command(command, timeout)
  165. else:
  166. c = Command(command)
  167. c.run(block=False)
  168. if data:
  169. c.send(data)
  170. c.subprocess.sendeof()
  171. c.block()
  172. return c
  173. def _expand_args(command):
  174. """Parses command strings and returns a Popen-ready list."""
  175. # Prepare arguments.
  176. if isinstance(command, STR_TYPES):
  177. splitter = shlex.shlex(command.encode('utf-8'))
  178. splitter.whitespace = '|'
  179. splitter.whitespace_split = True
  180. command = []
  181. while True:
  182. token = splitter.get_token()
  183. if token:
  184. command.append(token)
  185. else:
  186. break
  187. command = list(map(shlex.split, command))
  188. return command
  189. def chain(command, timeout=TIMEOUT):
  190. commands = _expand_args(command)
  191. data = None
  192. for command in commands:
  193. c = run(command, block=False, timeout=timeout)
  194. if data:
  195. c.send(data)
  196. c.subprocess.sendeof()
  197. data = c.out
  198. return c
  199. def run(command, block=True, binary=False, timeout=TIMEOUT):
  200. c = Command(command, timeout=timeout)
  201. c.run(block=block, binary=binary)
  202. if block:
  203. c.block()
  204. return c