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