blob: 2868619be90fc393b70b6fa870264fa67ec5c8ac [file] [log] [blame]
Miguel Osorio9a8e4b82019-09-25 07:58:24 -07001# Copyright lowRISC contributors.
2# Licensed under the Apache License, Version 2.0, see LICENSE for details.
3# SPDX-License-Identifier: Apache-2.0
4
5import difflib
6import filecmp
7import inspect
8import logging
9import os
10import re
Miguel Young de la Sota04c81522019-10-14 13:27:19 -050011import select
Miguel Osorio9a8e4b82019-09-25 07:58:24 -070012import shlex
13import signal
14import subprocess
15import time
16
17
18class Process:
19 """Utility class used to spawn an interact with processs.s"""
Miguel Osorio9a8e4b82019-09-25 07:58:24 -070020 def __init__(self,
21 cmd,
22 logdir,
23 cwd=None,
24 startup_done_expect=None,
25 startup_timeout=None):
26 """Creates Process instance.
27
28 Args:
29 cmd: Command line argument used to spawn process.
30 logdir: Directory used to store STDOUT and STDERR output.
31 cwd: Working directory used to spawn process.
32 startup_done_expect: Pattern used to check at process startup time.
33 startup_timeout: Timeout in seconds for |startup_done_expect| check.
34 """
35 if isinstance(cmd, str):
36 self.cmd = shlex.split(cmd)
37 else:
38 self.cmd = cmd
39 self.logdir = logdir
40 self.cwd = cwd
41 self.startup_done_expect = startup_done_expect
42 self.startup_timeout = startup_timeout
43 self.proc = None
44 self.logger = logging.getLogger(__name__)
45
46 self._f_stdout = None
47 self._f_stderr = None
48 self._f_stdout_r = None
49 self._f_stderr_r = None
50
51 def __del__(self):
52 try:
53 self.proc.kill()
54 self._f_stdout.close()
55 self._f_stderr.close()
56 self._f_stdout_r.close()
57 self._f_stderr_r.close()
58 except:
59 pass
60
61 def run(self):
62 """Start process with command line configured in constructor."""
63 cmd_name = os.path.basename(self.cmd[0])
64
65 # Enforce line-buffered STDOUT even when sending STDOUT/STDERR to file.
66 # If applications don't fflush() STDOUT manually, STDOUT goes through
67 # a 4kB buffer before we see any output, which prevents searching for
68 # the string indicating a successful startup.
69 # see discussion at http://www.pixelbeat.org/programming/stdio_buffering/
70 cmd = ['stdbuf', '-oL'] + self.cmd
71 self.logger.info("Running command " + ' '.join(cmd))
72
73 logfile_stdout = os.path.join(self.logdir,
74 "{}.stdout".format(cmd_name))
75 logfile_stderr = os.path.join(self.logdir,
76 "{}.stderr".format(cmd_name))
77 self.logger.debug("Capturing STDOUT at " + logfile_stdout)
78 self.logger.debug("Capturing STDERR at " + logfile_stderr)
79
80 self._f_stdout = open(logfile_stdout, 'w')
81 self._f_stderr = open(logfile_stderr, 'w')
Miguel Young de la Sota04c81522019-10-14 13:27:19 -050082 self.proc = subprocess.Popen(cmd,
83 cwd=self.cwd,
84 universal_newlines=True,
85 bufsize=1,
86 stdin=subprocess.PIPE,
87 stdout=self._f_stdout,
88 stderr=self._f_stderr)
Miguel Osorio9a8e4b82019-09-25 07:58:24 -070089
90 self._f_stdout_r = open(logfile_stdout, 'r')
91 self._f_stderr_r = open(logfile_stderr, 'r')
92
93 # no startup match pattern given => startup done!
94 if self.startup_done_expect == None:
95 return True
96
97 # check if the string indicating a successful startup appears in the
98 # the program output (STDOUT or STDERR)
Miguel Young de la Sota04c81522019-10-14 13:27:19 -050099 init_done = self.find_in_output(pattern=self.startup_done_expect,
100 timeout=self.startup_timeout)
Miguel Osorio9a8e4b82019-09-25 07:58:24 -0700101
102 if init_done == None:
103 raise subprocess.TimeoutExpired
104
105 self.logger.info("Startup sequence matched, startup done.")
106
107 return True
108
109 def terminate(self):
110 """Terminates process started by run call."""
111 if not self.proc:
112 return
113 self.proc.terminate()
114
115 def send_ctrl_c(self):
116 """Sends SIGINT to process started by run call."""
117 if not self.proc:
118 return
119 self.proc.send_signal(signal.SIGINT)
120
121 def expect(self, stdin_data=None, pattern=None, timeout=None):
122 """Write send to STDIN and check if the output is as expected.
123
124 Args:
125 stdin_data: Data to send to STDIN.
126 pattern: Pattern to search for after sending |stdin_data|.
127 timeout: Timeout in seconds for |pattern| check.
128 Returns:
129 True if |pattern| found, False otherwise.
130 """
131 # We don't get STDOUT/STDERR from subprocess.communicate() as it's
132 # redirected to file. We need to read the files instead.
133
134 # XXX: races (false positives) can happen here if output is generated
135 # before the input is sent to the process.
136 if pattern == None:
137 self._f_stdout_r.seek(0, 2)
138
139 self.proc.stdin.write(stdin_data)
140 self.proc.stdin.flush()
141
142 if pattern == None:
143 return True
144
Miguel Young de la Sota04c81522019-10-14 13:27:19 -0500145 return self.find_in_output(pattern, timeout) != None
Miguel Osorio9a8e4b82019-09-25 07:58:24 -0700146
Miguel Young de la Sota04c81522019-10-14 13:27:19 -0500147 def find_in_output(self, pattern, timeout):
Miguel Osorio9a8e4b82019-09-25 07:58:24 -0700148 """Read STDOUT and STDERR to find an expected pattern.
149
150 Both streams are reset to the start of the stream before searching.
151 pattern can be of two types:
152 1. A regular expression (a re object). In this case, the pattern is
153 matched against all lines and the result of re.match(pattern) is
154 returned. Multi-line matches are not supported.
155 2. A string. In this case pattern is compared to each line with
156 startswith(), and the full matching line is returned on a match.
157 If no match is found None is returned.
158 timeout is given in seconds.
159
160 Args:
161 pattern: Pattern to search for in STDOUT or STDERR.
162 timeout: Timeout in seconds for |pattern| check.
163 Returns:
164 String containing |pattern|, None otherwise.
165 """
166
167 if timeout != None:
168 t_end = time.time() + timeout
169
Miguel Osorio9a8e4b82019-09-25 07:58:24 -0700170 # reset streams
171 self._f_stdout_r.seek(0)
172 self._f_stderr_r.seek(0)
173
174 while True:
175 # check STDOUT as long as there is one
176 i = 0
177 for line in self._f_stdout_r:
178 i += 1
179 if hasattr(pattern, "match"):
180 m = pattern.match(line)
181 if m:
182 return m
183 else:
184 if line.startswith(pattern):
185 return line
186
187 # Check if we exceed the timeout while reading from STDOUT
188 # do so only every 100 lines to reduce the performance impact.
189 if timeout != None and i % 100 == 99 and time.time() >= t_end:
190 break
191
192 # check STDERR as long as there is one
193 i = 0
194 for line in self._f_stderr_r:
195 i += 1
196 if hasattr(pattern, "match"):
197 m = pattern.match(line)
198 if m:
199 return m
200 else:
201 if line.startswith(pattern):
202 return line
203
204 # Check if we exceed the timeout while reading from STDERR
205 # do so only every 100 lines to reduce the performance impact.
206 if timeout != None and i % 100 == 99 and time.time() >= t_end:
207 break
208
209 # wait for 200ms for new output
210 if timeout != None:
211 try:
212 self.proc.wait(timeout=0.2)
213 except subprocess.TimeoutExpired:
214 pass
215
216 return None
Miguel Young de la Sota04c81522019-10-14 13:27:19 -0500217
218
219def stream_fd_to_log(fd, logger, pattern, timeout=None):
220 """
221 Streams lines from the given fd to log until pattern matches.
222
223 Returns the match object derived from pattern.match(), or None if
224 the timeout expires.
225 """
226
227 deadline = None
228 if timeout != None:
229 deadline = time.monotonic() + timeout
230
231 os.set_blocking(fd, False)
232 line_of_output = b''
233 while True:
Philipp Wagner907d8032019-12-02 18:22:05 +0000234 curtime = time.monotonic()
235 if deadline != None and curtime > deadline:
Miguel Young de la Sota04c81522019-10-14 13:27:19 -0500236 return None
237
238 if line_of_output.endswith(b'\n'):
239 line_of_output = b''
240
241 # select() on the fd so that we don't waste time reading when
242 # we wouldn't get anything out of it.
243 if deadline != None:
244 rlist, _, _ = select.select([fd], [], [],
Philipp Wagner907d8032019-12-02 18:22:05 +0000245 deadline - curtime)
Miguel Young de la Sota04c81522019-10-14 13:27:19 -0500246 else:
247 rlist, _, _ = select.select([fd], [], [])
248
249 if len(rlist) == 0:
250 continue
251
252 raw_bytes = os.read(fd, 1024)
253 lines = raw_bytes.splitlines(True)
254
255 for line in lines:
256 line_of_output += line
257 if not line_of_output.endswith(b'\n'):
258 break
259
260 logger.debug('fd#%d: %s' % (fd, line_of_output))
261 match = pattern.match(line_of_output.decode('utf-8'))
262 if match != None:
263 return match
264
265 line_of_output = b''
Timothy Chen07269642019-11-05 15:05:11 -0800266
267# If logfile option was given, log all outputs to file.
268def setup_logfile(logger, logfile):
269 if logfile:
270 logger.debug("Logfile at %s" % (logfile))
271 logger.setLevel(logging.DEBUG)
272 fh = logging.FileHandler(filename=logfile, mode='w')
273 fh.setLevel(logging.DEBUG)
274 logger.addHandler(fh)