blob: 0090cb5a4a7135b945e3e9ab5ecd45a9e4f6bdf6 [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 Queue
11import subprocess
12import threading
13import time
14
15import psutil
16
Kuang-che Wu44278142019-03-04 11:33:57 +080017from bisect_kit import errors
18
Kuang-che Wu88875db2017-07-20 10:47:53 +080019logger = logging.getLogger(__name__)
20
21
Kuang-che Wu88875db2017-07-20 10:47:53 +080022class Popen(object):
23 """Wrapper of subprocess.Popen. Support output logging.
24
25 Attributes:
26 duration: Wall time of program execution in seconds.
27 returncode: The child return code.
28 """
29
30 def __init__(self,
31 args,
32 stdout_callback=None,
33 stderr_callback=None,
Kuang-che Wubcafc552019-08-15 15:27:02 +080034 log_stdout=True,
Kuang-che Wu88875db2017-07-20 10:47:53 +080035 **kwargs):
36 """Initializes Popen.
37
38 Args:
39 args: Command line arguments.
40 stdout_callback: Callback function for stdout. Called once per line.
41 stderr_callback: Callback function for stderr. Called once per line.
Kuang-che Wubcafc552019-08-15 15:27:02 +080042 log_stdout: Whether write the stdout output of the child process to log.
Kuang-che Wu88875db2017-07-20 10:47:53 +080043 **kwargs: Additional arguments passing to subprocess.Popen.
44 """
45 if 'stdout' in kwargs:
46 raise ValueError('stdout argument not allowed, it will be overridden.')
47 if 'stderr' in kwargs:
48 raise ValueError('stderr argument not allowed, it will be overridden.')
49
50 self.stdout_callback = stdout_callback
51 self.stderr_callback = stderr_callback
Kuang-che Wubcafc552019-08-15 15:27:02 +080052 self.log_stdout = log_stdout
Kuang-che Wu88875db2017-07-20 10:47:53 +080053 self.stdout_lines = []
54 self.stderr_lines = []
55 self.duration = -1
56 self.start = time.time()
57 self.queue = Queue.Queue(65536)
58 if isinstance(args, str):
59 logger.debug('cwd=%s, run %r', kwargs.get('cwd'), args)
60 else:
Kuang-che Wu62dda1e2018-03-27 20:37:52 +080061 logger.debug('cwd=%s, run %r', kwargs.get('cwd'),
62 subprocess.list2cmdline(args))
Kuang-che Wu88875db2017-07-20 10:47:53 +080063 self.p = subprocess.Popen(
64 args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
65
66 self.stdout_thread = threading.Thread(
67 target=self._reader_thread, args=('stdout', self.p.stdout))
68 self.stdout_thread.setDaemon(True)
69 self.stdout_thread.start()
70
71 self.stderr_thread = threading.Thread(
72 target=self._reader_thread, args=('stderr', self.p.stderr))
73 self.stderr_thread.setDaemon(True)
74 self.stderr_thread.start()
75
76 @property
77 def returncode(self):
78 return self.p.returncode
79
80 def _reader_thread(self, where, child_file):
81 """Reader thread to help reading stdout and stderr.
82
83 Args:
84 where: 'stdout' or 'stderr'.
85 child_file: file object which producing output.
86 """
87 for line in iter(child_file.readline, ''):
88 self.queue.put((where, line))
89 self.queue.put((where, ''))
90
91 def wait(self):
92 """Waits child process.
93
94 Returns:
95 return code.
96 """
97 ended = 0
98 while ended < 2:
99 where, line = self.queue.get()
100 # line includes '\n', will be '' if EOF.
101 if not line:
102 ended += 1
103 continue
Kuang-che Wubcafc552019-08-15 15:27:02 +0800104 if self.log_stdout or where == 'stderr':
Kuang-che Wu88875db2017-07-20 10:47:53 +0800105 logger.debug('[%s] %s', where, line.rstrip('\n'))
106 if self.stdout_callback and where == 'stdout':
107 self.stdout_callback(line)
108 if self.stderr_callback and where == 'stderr':
109 self.stderr_callback(line)
110 self.p.wait()
111 self.duration = time.time() - self.start
112 logger.debug('returncode %d', self.returncode)
113 return self.returncode
114
115 def terminate(self):
116 """Terminates child and descendant processes."""
117 # Need to ignore failures because sometimes they are expected.
118 # For example, the owner of child process is different to current and
119 # unable to be killed by current process. 'cros_sdk' is one of such case.
Kuang-che Wu455c7342017-11-28 13:21:32 +0800120 for proc in psutil.Process(self.p.pid).children(recursive=True):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800121 try:
122 proc.terminate()
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800123 except psutil.AccessDenied:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800124 logger.warning('Unable to terminate pid=%d; ignore', proc.pid)
125 try:
126 self.p.terminate()
127 except OSError:
128 logger.warning('Unable to terminate pid=%d; ignore', self.p.pid)
129 time.sleep(0.1)
130 try:
131 self.p.kill()
132 except OSError:
133 logger.warning('Unable to kill pid=%d; ignore', self.p.pid)
134
135
136def call(*args, **kwargs):
137 """Run command.
138
139 Modeled after subprocess.call.
140
141 Returns:
142 Exit code of sub-process.
143 """
144 p = Popen(args, **kwargs)
145 return p.wait()
146
147
148def check_output(*args, **kwargs):
149 """Runs command and return output.
150
151 Modeled after subprocess.check_output.
152
153 Returns:
154 stdout string of execution.
155
156 Raises:
157 subprocess.CalledProcessError if the exit code is non-zero.
158 """
159 stdout_lines = []
160
161 def collect_stdout(line):
162 stdout_lines.append(line)
163
164 p = Popen(args, stdout_callback=collect_stdout, **kwargs)
165 p.wait()
166 stdout = ''.join(stdout_lines)
167 if p.returncode != 0:
168 raise subprocess.CalledProcessError(p.returncode, args, stdout)
169
170 return stdout
171
172
173def check_call(*args, **kwargs):
174 """Runs command and ensures it succeeded.
175
176 Modeled after subprocess.check_call.
177
178 Raises:
179 subprocess.CalledProcessError if the exit code is non-zero.
180 """
181 p = Popen(args, **kwargs)
182 p.wait()
183 if p.returncode != 0:
184 raise subprocess.CalledProcessError(p.returncode, args)
185
186
Kuang-che Wu44278142019-03-04 11:33:57 +0800187def ssh_cmd(host, *args, **kwargs):
188 """Runs remote command using ssh.
189
190 Args:
191 host: remote host address
192 args: command and args running on the remote host
193 kwargs:
194 connect_timeout: connection timeout in seconds (int)
195
196 Raises:
197 subprocess.CalledProcessError if the exit code is non-zero.
198 """
199 cmd = ['ssh']
200 if kwargs.get('connect_timeout'):
201 cmd += ['-oConnectTimeout=%d' % kwargs['connect_timeout']]
202 cmd.append(host)
203 cmd += list(args)
204 try:
205 return check_output(*cmd)
206 except subprocess.CalledProcessError as e:
207 # ssh's own error code is 255.
208 if e.returncode == 255:
209 raise errors.SshConnectionError('ssh connection to %r failed' % host)
210 raise
211
212
Kuang-che Wu88875db2017-07-20 10:47:53 +0800213def version_key_func(v):
214 """Splits version string into components.
215
216 Split version number by '.', and convert to `int` if possible. After this
217 conversion, version numbers can be compared ordering directly. Usually this is
218 used with sort function together.
219
220 Example,
221 >>> version_key_func('1.a.3')
222 [1, 'a', 3]
223
224 Args:
225 v: version string
226
227 Returns:
228 list of int or string
229 """
230 return [int(x) if x.isdigit() else x for x in v.split('.')]
231
232
233def is_version_lesseq(a, b):
234 """Compares whether version `a` is less or equal to version `b`.
235
236 Note this only compares the numeric values component-wise. That is, '1.1' is
237 less than '2.0', but '1.1' may or may not be older than '2.0' according to
238 chromium version semantic.
239
240 Args:
241 a: version string
242 b: version string
243
244 Returns:
245 bool: True if a <= b
246 """
247 return version_key_func(a) <= version_key_func(b)
248
249
250def is_direct_relative_version(a, b):
251 r"""Determines two versions are direct-relative.
252
253 "Direct-relative" means "one is ancestor of the other".
254
255 This follows chromium and chromiumos version semantic.
256 https://www.chromium.org/developers/version-numbers
257
258 That is, [Major+1].[Minor] is a descendant of [Major+1].1, which is branched
259 from [Major+1].0, which is a child of [Major].0. Thus, [Major+1].[Minor] is
260 not direct-relative to any [Major].[Minor>0].
261
262 For example, in this chart, 3.3 is not direct-relative to 2.2.
263
264 -> 2.0 ------------------> 3.0 -------------
265 \ \
266 -> 2.1 -> 2.2 .... -> 3.1 -> 3.2 -> 3.3 ....
267
268 Args:
269 a: version string
270 b: version string
271
272 Returns:
273 bool: True if `a` and `b` are direct-relative.
274 """
275 a = version_key_func(a)
276 b = version_key_func(b)
277 assert len(a) == len(b)
278 if a > b:
279 a, b = b, a
280
281 branched = False
282 for x, y in zip(a, b):
283 if branched:
284 if x != 0:
285 return False
286 elif x != y:
287 branched = True
288
289 return True
Kuang-che Wu11713052019-05-30 16:21:54 +0800290
291
292def show_similar_candidates(key, value, candidates):
293 logger.error('incorrect %s: %r; possible candidates:', key, value)
294 if not candidates:
295 logger.error('(no candidates at all)')
296 return
297 similar_candidates = difflib.get_close_matches(value, candidates)
298 if not similar_candidates:
299 logger.error('(no similar candidates)')
300 return
301 for candidate in similar_candidates:
302 logger.error(' %s', candidate)