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