"Das U-Boot" Source Tree
1# SPDX-License-Identifier: GPL-2.0
2# Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved.
3
4"""
5Logic to spawn a sub-process and interact with its stdio.
6"""
7
8import io
9import os
10import re
11import pty
12import pytest
13import signal
14import select
15import sys
16import termios
17import time
18import traceback
19
20# Character to send (twice) to exit the terminal
21EXIT_CHAR = 0x1d # FS (Ctrl + ])
22
23class Timeout(Exception):
24 """An exception sub-class that indicates that a timeout occurred."""
25
26class BootFail(Exception):
27 """An exception sub-class that indicates that a boot failure occurred.
28
29 This is used when a bad pattern is seen when waiting for the boot prompt.
30 It is regarded as fatal, to avoid trying to boot the again and again to no
31 avail.
32 """
33
34class Unexpected(Exception):
35 """An exception sub-class that indicates that unexpected test was seen."""
36
37
38def handle_exception(ubconfig, console, log, err, name, fatal, output=''):
39 """Handle an exception from the console
40
41 Exceptions can occur when there is unexpected output or due to the board
42 crashing or hanging. Some exceptions are likely fatal, where retrying will
43 just chew up time to no available. In those cases it is best to cause
44 further tests be skipped.
45
46 Args:
47 ubconfig (ArbitraryAttributeContainer): ubconfig object
48 log (Logfile): Place to log errors
49 console (ConsoleBase): Console to clean up, if fatal
50 err (Exception): Exception which was thrown
51 name (str): Name of problem, to log
52 fatal (bool): True to abort all tests
53 output (str): Extra output to report on boot failure. This can show the
54 target's console output as it tried to boot
55 """
56 msg = f'{name}: '
57 if fatal:
58 msg += 'Marking connection bad - no other tests will run'
59 else:
60 msg += 'Assuming that lab is healthy'
61 print(msg)
62 log.error(msg)
63 log.error(f'Error: {err}')
64
65 if output:
66 msg += f'; output {output}'
67
68 if fatal:
69 ubconfig.connection_ok = False
70 console.cleanup_spawn()
71 pytest.exit(msg)
72
73
74class Spawn:
75 """Represents the stdio of a freshly created sub-process. Commands may be
76 sent to the process, and responses waited for.
77
78 Members:
79 output: accumulated output from expect()
80 """
81
82 def __init__(self, args, cwd=None, decode_signal=False):
83 """Spawn (fork/exec) the sub-process.
84
85 Args:
86 args: array of processs arguments. argv[0] is the command to
87 execute.
88 cwd: the directory to run the process in, or None for no change.
89 decode_signal (bool): True to indicate the exception number when
90 something goes wrong
91
92 Returns:
93 Nothing.
94 """
95 self.decode_signal = decode_signal
96 self.waited = False
97 self.exit_code = 0
98 self.exit_info = ''
99 self.buf = ''
100 self.output = ''
101 self.logfile_read = None
102 self.before = ''
103 self.after = ''
104 self.timeout = None
105 # http://stackoverflow.com/questions/7857352/python-regex-to-match-vt100-escape-sequences
106 self.re_vt100 = re.compile(r'(\x1b\[|\x9b)[^@-_]*[@-_]|\x1b[@-_]', re.I)
107
108 (self.pid, self.fd) = pty.fork()
109 if self.pid == 0:
110 try:
111 # For some reason, SIGHUP is set to SIG_IGN at this point when
112 # run under "go" (www.go.cd). Perhaps this happens under any
113 # background (non-interactive) system?
114 signal.signal(signal.SIGHUP, signal.SIG_DFL)
115 if cwd:
116 os.chdir(cwd)
117 os.execvp(args[0], args)
118 except:
119 print('CHILD EXECEPTION:')
120 traceback.print_exc()
121 finally:
122 os._exit(255)
123
124 old = None
125 try:
126 isatty = False
127 try:
128 isatty = os.isatty(sys.stdout.fileno())
129
130 # with --capture=tee-sys we cannot call fileno()
131 except io.UnsupportedOperation as exc:
132 pass
133 if isatty:
134 new = termios.tcgetattr(self.fd)
135 old = new
136 new[3] = new[3] & ~(termios.ICANON | termios.ISIG)
137 new[3] = new[3] & ~termios.ECHO
138 new[6][termios.VMIN] = 0
139 new[6][termios.VTIME] = 0
140 termios.tcsetattr(self.fd, termios.TCSANOW, new)
141
142 self.poll = select.poll()
143 self.poll.register(self.fd, select.POLLIN | select.POLLPRI | select.POLLERR |
144 select.POLLHUP | select.POLLNVAL)
145 except:
146 if old:
147 termios.tcsetattr(self.fd, termios.TCSANOW, old)
148 self.close()
149 raise
150
151 def kill(self, sig):
152 """Send unix signal "sig" to the child process.
153
154 Args:
155 sig: The signal number to send.
156
157 Returns:
158 Nothing.
159 """
160
161 os.kill(self.pid, sig)
162
163 def checkalive(self):
164 """Determine whether the child process is still running.
165
166 Returns:
167 tuple:
168 True if process is alive, else False
169 0 if process is alive, else exit code of process
170 string describing what happened ('' or 'status/signal n')
171 """
172
173 if self.waited:
174 return False, self.exit_code, self.exit_info
175
176 w = os.waitpid(self.pid, os.WNOHANG)
177 if w[0] == 0:
178 return True, 0, 'running'
179 status = w[1]
180
181 if os.WIFEXITED(status):
182 self.exit_code = os.WEXITSTATUS(status)
183 self.exit_info = 'status %d' % self.exit_code
184 elif os.WIFSIGNALED(status):
185 signum = os.WTERMSIG(status)
186 self.exit_code = -signum
187 self.exit_info = 'signal %d (%s)' % (signum, signal.Signals(signum).name)
188 self.waited = True
189 return False, self.exit_code, self.exit_info
190
191 def isalive(self):
192 """Determine whether the child process is still running.
193
194 Args:
195 None.
196
197 Returns:
198 Boolean indicating whether process is alive.
199 """
200 return self.checkalive()[0]
201
202 def send(self, data):
203 """Send data to the sub-process's stdin.
204
205 Args:
206 data: The data to send to the process.
207
208 Returns:
209 Nothing.
210 """
211
212 os.write(self.fd, data.encode(errors='replace'))
213
214 def receive(self, num_bytes):
215 """Receive data from the sub-process's stdin.
216
217 Args:
218 num_bytes (int): Maximum number of bytes to read
219
220 Returns:
221 str: The data received
222
223 Raises:
224 ValueError if U-Boot died
225 """
226 try:
227 c = os.read(self.fd, num_bytes).decode(errors='replace')
228 except OSError as err:
229 # With sandbox, try to detect when U-Boot exits when it
230 # shouldn't and explain why. This is much more friendly than
231 # just dying with an I/O error
232 if self.decode_signal and err.errno == 5: # I/O error
233 alive, _, info = self.checkalive()
234 if alive:
235 raise err
236 raise ValueError('U-Boot exited with %s' % info)
237 raise
238 return c
239
240 def expect(self, patterns):
241 """Wait for the sub-process to emit specific data.
242
243 This function waits for the process to emit one pattern from the
244 supplied list of patterns, or for a timeout to occur.
245
246 Args:
247 patterns: A list of strings or regex objects that we expect to
248 see in the sub-process' stdout.
249
250 Returns:
251 The index within the patterns array of the pattern the process
252 emitted.
253
254 Notable exceptions:
255 Timeout, if the process did not emit any of the patterns within
256 the expected time.
257 """
258
259 for pi in range(len(patterns)):
260 if type(patterns[pi]) == type(''):
261 patterns[pi] = re.compile(patterns[pi])
262
263 tstart_s = time.time()
264 try:
265 while True:
266 earliest_m = None
267 earliest_pi = None
268 for pi in range(len(patterns)):
269 pattern = patterns[pi]
270 m = pattern.search(self.buf)
271 if not m:
272 continue
273 if earliest_m and m.start() >= earliest_m.start():
274 continue
275 earliest_m = m
276 earliest_pi = pi
277 if earliest_m:
278 pos = earliest_m.start()
279 posafter = earliest_m.end()
280 self.before = self.buf[:pos]
281 self.after = self.buf[pos:posafter]
282 self.output += self.buf[:posafter]
283 self.buf = self.buf[posafter:]
284 return earliest_pi
285 tnow_s = time.time()
286 if self.timeout:
287 tdelta_ms = (tnow_s - tstart_s) * 1000
288 poll_maxwait = self.timeout - tdelta_ms
289 if tdelta_ms > self.timeout:
290 raise Timeout()
291 else:
292 poll_maxwait = None
293 events = self.poll.poll(poll_maxwait)
294 if not events:
295 raise Timeout()
296 c = self.receive(1024)
297 if self.logfile_read:
298 self.logfile_read.write(c)
299 self.buf += c
300 # count=0 is supposed to be the default, which indicates
301 # unlimited substitutions, but in practice the version of
302 # Python in Ubuntu 14.04 appears to default to count=2!
303 self.buf = self.re_vt100.sub('', self.buf, count=1000000)
304 finally:
305 if self.logfile_read:
306 self.logfile_read.flush()
307
308 def close(self):
309 """Close the stdio connection to the sub-process.
310
311 This also waits a reasonable time for the sub-process to stop running.
312
313 Args:
314 None.
315
316 Returns:
317 str: Type of closure completed
318 """
319 # For Labgrid-sjg, ask it is exit gracefully, so it can transition the
320 # board to the final state (like 'off') before exiting.
321 if os.environ.get('USE_LABGRID_SJG'):
322 self.send(chr(EXIT_CHAR) * 2)
323
324 # Wait about 10 seconds for Labgrid to close and power off the board
325 for _ in range(100):
326 if not self.isalive():
327 return 'normal'
328 time.sleep(0.1)
329
330 # That didn't work, so try closing the PTY
331 os.close(self.fd)
332 for _ in range(100):
333 if not self.isalive():
334 return 'break'
335 time.sleep(0.1)
336
337 return 'timeout'
338
339 def get_expect_output(self):
340 """Return the output read by expect()
341
342 Returns:
343 The output processed by expect(), as a string.
344 """
345 return self.output