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