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