blob: 748055d3681ed6710c50d55a1fe8165a04bf118a [file] [log] [blame]
Kuang-che Wu6e4beca2018-06-27 17:45:02 +08001# -*- coding: utf-8 -*-
Kuang-che Wu88875db2017-07-20 10:47:53 +08002# Copyright 2017 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Utility functions and classes."""
6
7from __future__ import print_function
Kuang-che Wu11713052019-05-30 16:21:54 +08008import difflib
Kuang-che Wu88875db2017-07-20 10:47:53 +08009import logging
Kuang-che Wu88875db2017-07-20 10:47:53 +080010import subprocess
11import threading
12import time
13
14import psutil
Kuang-che Wu68f022d2019-11-29 14:38:48 +080015import six
Kuang-che Wuae6824b2019-08-27 22:20:01 +080016from six.moves import queue
Kuang-che Wu88875db2017-07-20 10:47:53 +080017
Kuang-che Wu44278142019-03-04 11:33:57 +080018from bisect_kit import errors
19
Kuang-che Wu88875db2017-07-20 10:47:53 +080020logger = logging.getLogger(__name__)
21
22
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +080023class TimeoutExpired(Exception):
24 """Timeout expired.
25
26 This may be raised by blocking calls like Popen.wait(), check_call(),
27 check_output(), etc.
28 """
29
30
Kuang-che Wu88875db2017-07-20 10:47:53 +080031class Popen(object):
32 """Wrapper of subprocess.Popen. Support output logging.
33
Kuang-che Wu68f022d2019-11-29 14:38:48 +080034 The default is text mode with utf8 encoding. This is different to
35 subprocess.Popen, which is default binary.
36
Kuang-che Wu88875db2017-07-20 10:47:53 +080037 Attributes:
38 duration: Wall time of program execution in seconds.
39 returncode: The child return code.
40 """
41
42 def __init__(self,
43 args,
44 stdout_callback=None,
45 stderr_callback=None,
Kuang-che Wubcafc552019-08-15 15:27:02 +080046 log_stdout=True,
Kuang-che Wu68f022d2019-11-29 14:38:48 +080047 binary=None,
Kuang-che Wu88875db2017-07-20 10:47:53 +080048 **kwargs):
49 """Initializes Popen.
50
51 Args:
52 args: Command line arguments.
53 stdout_callback: Callback function for stdout. Called once per line.
54 stderr_callback: Callback function for stderr. Called once per line.
Kuang-che Wu68f022d2019-11-29 14:38:48 +080055 binary: binary mode; default is False
Kuang-che Wubcafc552019-08-15 15:27:02 +080056 log_stdout: Whether write the stdout output of the child process to log.
Kuang-che Wu88875db2017-07-20 10:47:53 +080057 **kwargs: Additional arguments passing to subprocess.Popen.
58 """
59 if 'stdout' in kwargs:
60 raise ValueError('stdout argument not allowed, it will be overridden.')
61 if 'stderr' in kwargs:
62 raise ValueError('stderr argument not allowed, it will be overridden.')
63
Kuang-che Wu68f022d2019-11-29 14:38:48 +080064 if binary:
65 assert not kwargs.get('encoding')
66 self.binary_mode = True
67 self.encoding = None
68 self.log_stdout = False
69 else:
70 self.binary_mode = False
71 self.encoding = kwargs.get('encoding', 'utf8')
72 self.log_stdout = log_stdout
73
74 if six.PY2:
75 if 'encoding' in kwargs:
76 del kwargs['encoding']
77 else:
78 kwargs['encoding'] = self.encoding
79
Kuang-che Wu88875db2017-07-20 10:47:53 +080080 self.stdout_callback = stdout_callback
81 self.stderr_callback = stderr_callback
Kuang-che Wu88875db2017-07-20 10:47:53 +080082 self.stdout_lines = []
83 self.stderr_lines = []
84 self.duration = -1
85 self.start = time.time()
Kuang-che Wuae6824b2019-08-27 22:20:01 +080086 self.queue = queue.Queue(65536)
Kuang-che Wu88875db2017-07-20 10:47:53 +080087 if isinstance(args, str):
88 logger.debug('cwd=%s, run %r', kwargs.get('cwd'), args)
89 else:
Kuang-che Wu62dda1e2018-03-27 20:37:52 +080090 logger.debug('cwd=%s, run %r', kwargs.get('cwd'),
91 subprocess.list2cmdline(args))
Kuang-che Wu88875db2017-07-20 10:47:53 +080092 self.p = subprocess.Popen(
93 args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
94
95 self.stdout_thread = threading.Thread(
96 target=self._reader_thread, args=('stdout', self.p.stdout))
97 self.stdout_thread.setDaemon(True)
98 self.stdout_thread.start()
99
100 self.stderr_thread = threading.Thread(
101 target=self._reader_thread, args=('stderr', self.p.stderr))
102 self.stderr_thread.setDaemon(True)
103 self.stderr_thread.start()
104
105 @property
106 def returncode(self):
107 return self.p.returncode
108
109 def _reader_thread(self, where, child_file):
110 """Reader thread to help reading stdout and stderr.
111
112 Args:
113 where: 'stdout' or 'stderr'.
114 child_file: file object which producing output.
115 """
116 for line in iter(child_file.readline, ''):
117 self.queue.put((where, line))
118 self.queue.put((where, ''))
Kuang-che Wu74bcb642020-02-20 18:45:53 +0800119 child_file.close()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800120
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +0800121 def wait(self, timeout=None):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800122 """Waits child process.
123
124 Returns:
125 return code.
126 """
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +0800127 t0 = time.time()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800128 ended = 0
129 while ended < 2:
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +0800130 if timeout is not None:
131 try:
132 remaining_time = timeout - (time.time() - t0)
133 if remaining_time > 0:
134 where, line = self.queue.get(block=True, timeout=remaining_time)
135 else:
136 # We follow queue.get's behavior to raise queue.Empty, so it's
137 # always queue.Empty when time is up, no matter remaining_time is
138 # negative or positive.
139 raise queue.Empty
140 except queue.Empty:
141 logger.debug('child process time out (%.1f seconds), kill it',
142 timeout)
143 self.p.kill()
144 raise TimeoutExpired
145 else:
146 where, line = self.queue.get(block=True)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800147 # line includes '\n', will be '' if EOF.
148 if not line:
149 ended += 1
150 continue
Kuang-che Wu88875db2017-07-20 10:47:53 +0800151 if self.stdout_callback and where == 'stdout':
152 self.stdout_callback(line)
153 if self.stderr_callback and where == 'stderr':
154 self.stderr_callback(line)
Kuang-che Wu68f022d2019-11-29 14:38:48 +0800155 if self.log_stdout or where == 'stderr':
156 if self.binary_mode:
157 line = line.decode('utf8', errors='replace')
158 logger.debug('[%s] %s', where, line.rstrip('\n'))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800159 self.p.wait()
160 self.duration = time.time() - self.start
161 logger.debug('returncode %d', self.returncode)
162 return self.returncode
163
164 def terminate(self):
165 """Terminates child and descendant processes."""
166 # Need to ignore failures because sometimes they are expected.
167 # For example, the owner of child process is different to current and
168 # unable to be killed by current process. 'cros_sdk' is one of such case.
Kuang-che Wu455c7342017-11-28 13:21:32 +0800169 for proc in psutil.Process(self.p.pid).children(recursive=True):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800170 try:
171 proc.terminate()
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800172 except psutil.AccessDenied:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800173 logger.warning('Unable to terminate pid=%d; ignore', proc.pid)
174 try:
175 self.p.terminate()
176 except OSError:
177 logger.warning('Unable to terminate pid=%d; ignore', self.p.pid)
178 time.sleep(0.1)
179 try:
180 self.p.kill()
181 except OSError:
182 logger.warning('Unable to kill pid=%d; ignore', self.p.pid)
183
184
185def call(*args, **kwargs):
186 """Run command.
187
188 Modeled after subprocess.call.
189
190 Returns:
191 Exit code of sub-process.
192 """
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +0800193 timeout = kwargs.get('timeout')
194 # TODO(kcwu): let current function capture this optional parameter after
195 # migrated to python3
196 if 'timeout' in kwargs:
197 del kwargs['timeout']
Kuang-che Wu88875db2017-07-20 10:47:53 +0800198 p = Popen(args, **kwargs)
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +0800199 return p.wait(timeout=timeout)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800200
201
202def check_output(*args, **kwargs):
203 """Runs command and return output.
204
205 Modeled after subprocess.check_output.
206
207 Returns:
208 stdout string of execution.
209
210 Raises:
211 subprocess.CalledProcessError if the exit code is non-zero.
212 """
213 stdout_lines = []
214
215 def collect_stdout(line):
216 stdout_lines.append(line)
217
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +0800218 timeout = kwargs.get('timeout')
219 # TODO(kcwu): let current function capture this optional parameter after
220 # migrated to python3
221 if 'timeout' in kwargs:
222 del kwargs['timeout']
Kuang-che Wu88875db2017-07-20 10:47:53 +0800223 p = Popen(args, stdout_callback=collect_stdout, **kwargs)
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +0800224 p.wait(timeout=timeout)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800225 stdout = ''.join(stdout_lines)
226 if p.returncode != 0:
227 raise subprocess.CalledProcessError(p.returncode, args, stdout)
228
229 return stdout
230
231
232def check_call(*args, **kwargs):
233 """Runs command and ensures it succeeded.
234
235 Modeled after subprocess.check_call.
236
237 Raises:
238 subprocess.CalledProcessError if the exit code is non-zero.
239 """
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +0800240 timeout = kwargs.get('timeout')
241 # TODO(kcwu): let current function capture this optional parameter after
242 # migrated to python3
243 if 'timeout' in kwargs:
244 del kwargs['timeout']
Kuang-che Wu88875db2017-07-20 10:47:53 +0800245 p = Popen(args, **kwargs)
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +0800246 p.wait(timeout=timeout)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800247 if p.returncode != 0:
248 raise subprocess.CalledProcessError(p.returncode, args)
249
250
Kuang-che Wu44278142019-03-04 11:33:57 +0800251def ssh_cmd(host, *args, **kwargs):
252 """Runs remote command using ssh.
253
254 Args:
255 host: remote host address
256 args: command and args running on the remote host
257 kwargs:
258 connect_timeout: connection timeout in seconds (int)
259
260 Raises:
261 subprocess.CalledProcessError if the exit code is non-zero.
262 """
263 cmd = ['ssh']
264 if kwargs.get('connect_timeout'):
265 cmd += ['-oConnectTimeout=%d' % kwargs['connect_timeout']]
266 cmd.append(host)
267 cmd += list(args)
268 try:
269 return check_output(*cmd)
270 except subprocess.CalledProcessError as e:
271 # ssh's own error code is 255.
272 if e.returncode == 255:
273 raise errors.SshConnectionError('ssh connection to %r failed' % host)
274 raise
275
276
Kuang-che Wu88875db2017-07-20 10:47:53 +0800277def version_key_func(v):
278 """Splits version string into components.
279
280 Split version number by '.', and convert to `int` if possible. After this
281 conversion, version numbers can be compared ordering directly. Usually this is
282 used with sort function together.
283
284 Example,
285 >>> version_key_func('1.a.3')
286 [1, 'a', 3]
287
288 Args:
289 v: version string
290
291 Returns:
292 list of int or string
293 """
294 return [int(x) if x.isdigit() else x for x in v.split('.')]
295
296
297def is_version_lesseq(a, b):
298 """Compares whether version `a` is less or equal to version `b`.
299
300 Note this only compares the numeric values component-wise. That is, '1.1' is
301 less than '2.0', but '1.1' may or may not be older than '2.0' according to
302 chromium version semantic.
303
304 Args:
305 a: version string
306 b: version string
307
308 Returns:
309 bool: True if a <= b
310 """
311 return version_key_func(a) <= version_key_func(b)
312
313
314def is_direct_relative_version(a, b):
315 r"""Determines two versions are direct-relative.
316
317 "Direct-relative" means "one is ancestor of the other".
318
319 This follows chromium and chromiumos version semantic.
320 https://www.chromium.org/developers/version-numbers
321
322 That is, [Major+1].[Minor] is a descendant of [Major+1].1, which is branched
323 from [Major+1].0, which is a child of [Major].0. Thus, [Major+1].[Minor] is
324 not direct-relative to any [Major].[Minor>0].
325
326 For example, in this chart, 3.3 is not direct-relative to 2.2.
327
328 -> 2.0 ------------------> 3.0 -------------
329 \ \
330 -> 2.1 -> 2.2 .... -> 3.1 -> 3.2 -> 3.3 ....
331
332 Args:
333 a: version string
334 b: version string
335
336 Returns:
337 bool: True if `a` and `b` are direct-relative.
338 """
339 a = version_key_func(a)
340 b = version_key_func(b)
341 assert len(a) == len(b)
342 if a > b:
343 a, b = b, a
344
345 branched = False
346 for x, y in zip(a, b):
347 if branched:
348 if x != 0:
349 return False
350 elif x != y:
351 branched = True
352
353 return True
Kuang-che Wu11713052019-05-30 16:21:54 +0800354
355
356def show_similar_candidates(key, value, candidates):
357 logger.error('incorrect %s: %r; possible candidates:', key, value)
358 if not candidates:
359 logger.error('(no candidates at all)')
360 return
361 similar_candidates = difflib.get_close_matches(value, candidates)
362 if not similar_candidates:
363 logger.error('(no similar candidates)')
364 return
365 for candidate in similar_candidates:
366 logger.error(' %s', candidate)