u_boot_spawn.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. # SPDX-License-Identifier: GPL-2.0
  2. # Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved.
  3. """
  4. Logic to spawn a sub-process and interact with its stdio.
  5. """
  6. import os
  7. import re
  8. import pty
  9. import signal
  10. import select
  11. import time
  12. import traceback
  13. class Timeout(Exception):
  14. """An exception sub-class that indicates that a timeout occurred."""
  15. class Spawn:
  16. """Represents the stdio of a freshly created sub-process. Commands may be
  17. sent to the process, and responses waited for.
  18. Members:
  19. output: accumulated output from expect()
  20. """
  21. def __init__(self, args, cwd=None):
  22. """Spawn (fork/exec) the sub-process.
  23. Args:
  24. args: array of processs arguments. argv[0] is the command to
  25. execute.
  26. cwd: the directory to run the process in, or None for no change.
  27. Returns:
  28. Nothing.
  29. """
  30. self.waited = False
  31. self.exit_code = 0
  32. self.exit_info = ''
  33. self.buf = ''
  34. self.output = ''
  35. self.logfile_read = None
  36. self.before = ''
  37. self.after = ''
  38. self.timeout = None
  39. # http://stackoverflow.com/questions/7857352/python-regex-to-match-vt100-escape-sequences
  40. self.re_vt100 = re.compile(r'(\x1b\[|\x9b)[^@-_]*[@-_]|\x1b[@-_]', re.I)
  41. (self.pid, self.fd) = pty.fork()
  42. if self.pid == 0:
  43. try:
  44. # For some reason, SIGHUP is set to SIG_IGN at this point when
  45. # run under "go" (www.go.cd). Perhaps this happens under any
  46. # background (non-interactive) system?
  47. signal.signal(signal.SIGHUP, signal.SIG_DFL)
  48. if cwd:
  49. os.chdir(cwd)
  50. os.execvp(args[0], args)
  51. except:
  52. print('CHILD EXECEPTION:')
  53. traceback.print_exc()
  54. finally:
  55. os._exit(255)
  56. try:
  57. self.poll = select.poll()
  58. self.poll.register(self.fd, select.POLLIN | select.POLLPRI | select.POLLERR |
  59. select.POLLHUP | select.POLLNVAL)
  60. except:
  61. self.close()
  62. raise
  63. def kill(self, sig):
  64. """Send unix signal "sig" to the child process.
  65. Args:
  66. sig: The signal number to send.
  67. Returns:
  68. Nothing.
  69. """
  70. os.kill(self.pid, sig)
  71. def checkalive(self):
  72. """Determine whether the child process is still running.
  73. Returns:
  74. tuple:
  75. True if process is alive, else False
  76. 0 if process is alive, else exit code of process
  77. string describing what happened ('' or 'status/signal n')
  78. """
  79. if self.waited:
  80. return False, self.exit_code, self.exit_info
  81. w = os.waitpid(self.pid, os.WNOHANG)
  82. if w[0] == 0:
  83. return True, 0, 'running'
  84. status = w[1]
  85. if os.WIFEXITED(status):
  86. self.exit_code = os.WEXITSTATUS(status)
  87. self.exit_info = 'status %d' % self.exit_code
  88. elif os.WIFSIGNALED(status):
  89. signum = os.WTERMSIG(status)
  90. self.exit_code = -signum
  91. self.exit_info = 'signal %d (%s)' % (signum, signal.Signals(signum).name)
  92. self.waited = True
  93. return False, self.exit_code, self.exit_info
  94. def isalive(self):
  95. """Determine whether the child process is still running.
  96. Args:
  97. None.
  98. Returns:
  99. Boolean indicating whether process is alive.
  100. """
  101. return self.checkalive()[0]
  102. def send(self, data):
  103. """Send data to the sub-process's stdin.
  104. Args:
  105. data: The data to send to the process.
  106. Returns:
  107. Nothing.
  108. """
  109. os.write(self.fd, data.encode(errors='replace'))
  110. def expect(self, patterns):
  111. """Wait for the sub-process to emit specific data.
  112. This function waits for the process to emit one pattern from the
  113. supplied list of patterns, or for a timeout to occur.
  114. Args:
  115. patterns: A list of strings or regex objects that we expect to
  116. see in the sub-process' stdout.
  117. Returns:
  118. The index within the patterns array of the pattern the process
  119. emitted.
  120. Notable exceptions:
  121. Timeout, if the process did not emit any of the patterns within
  122. the expected time.
  123. """
  124. for pi in range(len(patterns)):
  125. if type(patterns[pi]) == type(''):
  126. patterns[pi] = re.compile(patterns[pi])
  127. tstart_s = time.time()
  128. try:
  129. while True:
  130. earliest_m = None
  131. earliest_pi = None
  132. for pi in range(len(patterns)):
  133. pattern = patterns[pi]
  134. m = pattern.search(self.buf)
  135. if not m:
  136. continue
  137. if earliest_m and m.start() >= earliest_m.start():
  138. continue
  139. earliest_m = m
  140. earliest_pi = pi
  141. if earliest_m:
  142. pos = earliest_m.start()
  143. posafter = earliest_m.end()
  144. self.before = self.buf[:pos]
  145. self.after = self.buf[pos:posafter]
  146. self.output += self.buf[:posafter]
  147. self.buf = self.buf[posafter:]
  148. return earliest_pi
  149. tnow_s = time.time()
  150. if self.timeout:
  151. tdelta_ms = (tnow_s - tstart_s) * 1000
  152. poll_maxwait = self.timeout - tdelta_ms
  153. if tdelta_ms > self.timeout:
  154. raise Timeout()
  155. else:
  156. poll_maxwait = None
  157. events = self.poll.poll(poll_maxwait)
  158. if not events:
  159. raise Timeout()
  160. try:
  161. c = os.read(self.fd, 1024).decode(errors='replace')
  162. except OSError as err:
  163. # With sandbox, try to detect when U-Boot exits when it
  164. # shouldn't and explain why. This is much more friendly than
  165. # just dying with an I/O error
  166. if err.errno == 5: # Input/output error
  167. alive, _, info = self.checkalive()
  168. if alive:
  169. raise err
  170. raise ValueError('U-Boot exited with %s' % info)
  171. raise err
  172. if self.logfile_read:
  173. self.logfile_read.write(c)
  174. self.buf += c
  175. # count=0 is supposed to be the default, which indicates
  176. # unlimited substitutions, but in practice the version of
  177. # Python in Ubuntu 14.04 appears to default to count=2!
  178. self.buf = self.re_vt100.sub('', self.buf, count=1000000)
  179. finally:
  180. if self.logfile_read:
  181. self.logfile_read.flush()
  182. def close(self):
  183. """Close the stdio connection to the sub-process.
  184. This also waits a reasonable time for the sub-process to stop running.
  185. Args:
  186. None.
  187. Returns:
  188. Nothing.
  189. """
  190. os.close(self.fd)
  191. for _ in range(100):
  192. if not self.isalive():
  193. break
  194. time.sleep(0.1)
  195. def get_expect_output(self):
  196. """Return the output read by expect()
  197. Returns:
  198. The output processed by expect(), as a string.
  199. """
  200. return self.output