blob: 23b9dd7d66c6a1798c3400580744649a35c254b7 [file] [log] [blame]
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org06617272010-11-04 13:50:50 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00004
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00005"""Generic utils."""
6
maruel@chromium.orgdae209f2012-07-03 16:08:15 +00007import codecs
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02008import collections
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +00009import contextlib
hinoka@google.com267f33e2014-02-28 22:02:32 +000010import cStringIO
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000011import datetime
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000012import logging
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020013import operator
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000014import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000015import pipes
szager@chromium.orgfc616382014-03-18 20:32:04 +000016import platform
maruel@chromium.org3742c842010-09-09 19:27:14 +000017import Queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000018import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000019import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000020import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000021import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000022import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000023import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000024import time
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000025import urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000026
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000027import subprocess2
28
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000029
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000030RETRY_MAX = 3
31RETRY_INITIAL_SLEEP = 0.5
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000032START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000033
34
borenet@google.com6a9b1682014-03-24 18:35:23 +000035_WARNINGS = []
36
37
szager@chromium.orgff113292014-03-25 06:02:08 +000038# These repos are known to cause OOM errors on 32-bit platforms, due the the
39# very large objects they contain. It is not safe to use threaded index-pack
40# when cloning/fetching them.
41THREADED_INDEX_PACK_BLACKLIST = [
42 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
43]
44
45
maruel@chromium.org66c83e62010-09-07 14:18:45 +000046class Error(Exception):
47 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000048 def __init__(self, msg, *args, **kwargs):
49 index = getattr(threading.currentThread(), 'index', 0)
50 if index:
51 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
52 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000053
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000054
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000055def Elapsed(until=None):
56 if until is None:
57 until = datetime.datetime.now()
58 return str(until - START).partition('.')[0]
59
60
borenet@google.com6a9b1682014-03-24 18:35:23 +000061def PrintWarnings():
62 """Prints any accumulated warnings."""
63 if _WARNINGS:
64 print >> sys.stderr, '\n\nWarnings:'
65 for warning in _WARNINGS:
66 print >> sys.stderr, warning
67
68
69def AddWarning(msg):
70 """Adds the given warning message to the list of accumulated warnings."""
71 _WARNINGS.append(msg)
72
73
msb@chromium.orgac915bb2009-11-13 17:03:01 +000074def SplitUrlRevision(url):
75 """Splits url and returns a two-tuple: url, rev"""
76 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000077 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +000078 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000079 components = re.search(regex, url).groups()
80 else:
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +000081 components = url.rsplit('@', 1)
82 if re.match(r'^\w+\@', url) and '@' not in components[0]:
83 components = [url]
84
msb@chromium.orgac915bb2009-11-13 17:03:01 +000085 if len(components) == 1:
86 components += [None]
87 return tuple(components)
88
89
primiano@chromium.org5439ea52014-08-06 17:18:18 +000090def IsGitSha(revision):
91 """Returns true if the given string is a valid hex-encoded sha"""
92 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
93
94
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +020095def IsFullGitSha(revision):
96 """Returns true if the given string is a valid hex-encoded full sha"""
97 return re.match('^[a-fA-F0-9]{40}$', revision) is not None
98
99
floitsch@google.comeaab7842011-04-28 09:07:58 +0000100def IsDateRevision(revision):
101 """Returns true if the given revision is of the form "{ ... }"."""
102 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
103
104
105def MakeDateRevision(date):
106 """Returns a revision representing the latest revision before the given
107 date."""
108 return "{" + date + "}"
109
110
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000111def SyntaxErrorToError(filename, e):
112 """Raises a gclient_utils.Error exception with the human readable message"""
113 try:
114 # Try to construct a human readable error message
115 if filename:
116 error_message = 'There is a syntax error in %s\n' % filename
117 else:
118 error_message = 'There is a syntax error\n'
119 error_message += 'Line #%s, character %s: "%s"' % (
120 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
121 except:
122 # Something went wrong, re-raise the original exception
123 raise e
124 else:
125 raise Error(error_message)
126
127
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000128class PrintableObject(object):
129 def __str__(self):
130 output = ''
131 for i in dir(self):
132 if i.startswith('__'):
133 continue
134 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
135 return output
136
137
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000138def FileRead(filename, mode='rU'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000139 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +0000140 # codecs.open() has different behavior than open() on python 2.6 so use
141 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000142 s = f.read()
143 try:
144 return s.decode('utf-8')
145 except UnicodeDecodeError:
146 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000147
148
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000149def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000150 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000151 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000152
153
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000154@contextlib.contextmanager
155def temporary_directory(**kwargs):
156 tdir = tempfile.mkdtemp(**kwargs)
157 try:
158 yield tdir
159 finally:
160 if tdir:
161 rmtree(tdir)
162
163
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000164def safe_rename(old, new):
165 """Renames a file reliably.
166
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000167 Sometimes os.rename does not work because a dying git process keeps a handle
168 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000169 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000170 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000171 """
172 # roughly 10s
173 retries = 100
174 for i in range(retries):
175 try:
176 os.rename(old, new)
177 break
178 except OSError:
179 if i == (retries - 1):
180 # Give up.
181 raise
182 # retry
183 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
184 time.sleep(0.1)
185
186
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000187def rm_file_or_tree(path):
188 if os.path.isfile(path):
189 os.remove(path)
190 else:
191 rmtree(path)
192
193
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000194def rmtree(path):
195 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000196
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000197 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000198
199 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700200 are read-only. We need to be able to force the files to be writable (i.e.,
201 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000202
203 Even with all this, Windows still sometimes fails to delete a file, citing
204 a permission error (maybe something to do with antivirus scans or disk
205 indexing). The best suggestion any of the user forums had was to wait a
206 bit and try again, so we do that too. It's hand-waving, but sometimes it
207 works. :/
208
209 On POSIX systems, things are a little bit simpler. The modes of the files
210 to be deleted doesn't matter, only the modes of the directories containing
211 them are significant. As the directory tree is traversed, each directory
212 has its mode set appropriately before descending into it. This should
213 result in the entire tree being removed, with the possible exception of
214 *path itself, because nothing attempts to change the mode of its parent.
215 Doing so would be hazardous, as it's not a directory slated for removal.
216 In the ordinary case, this is not a problem: for our purposes, the user
217 will never lack write permission on *path's parent.
218 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000219 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000220 return
221
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000222 if os.path.islink(path) or not os.path.isdir(path):
223 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000224
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000225 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000226 # Give up and use cmd.exe's rd command.
227 path = os.path.normcase(path)
228 for _ in xrange(3):
229 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
230 if exitcode == 0:
231 return
232 else:
233 print >> sys.stderr, 'rd exited with code %d' % exitcode
234 time.sleep(3)
235 raise Exception('Failed to remove path %s' % path)
236
237 # On POSIX systems, we need the x-bit set on the directory to access it,
238 # the r-bit to see its contents, and the w-bit to remove files from it.
239 # The actual modes of the files within the directory is irrelevant.
240 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000241
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000242 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000243 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000244
245 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000246 # If fullpath is a symbolic link that points to a directory, isdir will
247 # be True, but we don't want to descend into that as a directory, we just
248 # want to remove the link. Check islink and treat links as ordinary files
249 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000250 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000251 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000252 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000253 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000254 # Recurse.
255 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000256
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000257 remove(os.rmdir, path)
258
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000259
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000260def safe_makedirs(tree):
261 """Creates the directory in a safe manner.
262
263 Because multiple threads can create these directories concurently, trap the
264 exception and pass on.
265 """
266 count = 0
267 while not os.path.exists(tree):
268 count += 1
269 try:
270 os.makedirs(tree)
271 except OSError, e:
272 # 17 POSIX, 183 Windows
273 if e.errno not in (17, 183):
274 raise
275 if count > 40:
276 # Give up.
277 raise
278
279
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000280def CommandToStr(args):
281 """Converts an arg list into a shell escaped string."""
282 return ' '.join(pipes.quote(arg) for arg in args)
283
284
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000285def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000286 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000287
maruel@chromium.org17d01792010-09-01 18:07:10 +0000288 If |always| is True, a message indicating what is being done
289 is printed to stdout all the time even if not output is generated. Otherwise
290 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000291 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000292 stdout = kwargs.setdefault('stdout', sys.stdout)
293 if header is None:
Daniel Cheng5fd1def2017-10-19 11:27:27 -0700294 # The automatically generated header only prepends newline if always is
295 # false: always is usually set to false if there's an external progress
296 # display, and it's better not to clobber it in that case.
297 header = "%s________ running '%s' in '%s'\n" % (
298 '' if always else '\n',
ilevy@chromium.org4aad1852013-07-12 21:32:51 +0000299 ' '.join(args), kwargs.get('cwd', '.'))
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000300
maruel@chromium.org17d01792010-09-01 18:07:10 +0000301 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000302 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000303 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000304 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000305 def filter_msg(line):
306 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000307 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000308 elif filter_fn:
309 filter_fn(line)
310 kwargs['filter_fn'] = filter_msg
311 kwargs['call_filter_on_first_line'] = True
312 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000313 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000314 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000315
maruel@chromium.org17d01792010-09-01 18:07:10 +0000316
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000317class Wrapper(object):
318 """Wraps an object, acting as a transparent proxy for all properties by
319 default.
320 """
321 def __init__(self, wrapped):
322 self._wrapped = wrapped
323
324 def __getattr__(self, name):
325 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000326
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000327
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000328class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000329 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000330 def __init__(self, wrapped, delay):
331 super(AutoFlush, self).__init__(wrapped)
332 if not hasattr(self, 'lock'):
333 self.lock = threading.Lock()
334 self.__last_flushed_at = time.time()
335 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000336
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000337 @property
338 def autoflush(self):
339 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000340
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000341 def write(self, out, *args, **kwargs):
342 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000343 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000344 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000345 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000346 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000347 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000348 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000349 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000350 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000351 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000352 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000353
354
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000355class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000356 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000357 threads with a NN> prefix.
358 """
359 def __init__(self, wrapped, include_zero=False):
360 super(Annotated, self).__init__(wrapped)
361 if not hasattr(self, 'lock'):
362 self.lock = threading.Lock()
363 self.__output_buffers = {}
364 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000365
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000366 @property
367 def annotated(self):
368 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000369
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000370 def write(self, out):
371 index = getattr(threading.currentThread(), 'index', 0)
372 if not index and not self.__include_zero:
373 # Unindexed threads aren't buffered.
374 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000375
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000376 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000377 try:
378 # Use a dummy array to hold the string so the code can be lockless.
379 # Strings are immutable, requiring to keep a lock for the whole dictionary
380 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000381 if not index in self.__output_buffers:
382 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000383 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000384 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000385 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000386 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000387
388 # Continue lockless.
389 obj[0] += out
390 while '\n' in obj[0]:
391 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000392 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000393 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000394 obj[0] = remaining
395
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000396 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000397 """Flush buffered output."""
398 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000399 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000400 try:
401 # Detect threads no longer existing.
402 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000403 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000404 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000405 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000406 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000407 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000408 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000409 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000410 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000411
412 # Don't keep the lock while writting. Will append \n when it shouldn't.
413 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000414 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000415 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
416 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000417
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000418
419def MakeFileAutoFlush(fileobj, delay=10):
420 autoflush = getattr(fileobj, 'autoflush', None)
421 if autoflush:
422 autoflush.delay = delay
423 return fileobj
424 return AutoFlush(fileobj, delay)
425
426
427def MakeFileAnnotated(fileobj, include_zero=False):
428 if getattr(fileobj, 'annotated', None):
429 return fileobj
430 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000431
432
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000433GCLIENT_CHILDREN = []
434GCLIENT_CHILDREN_LOCK = threading.Lock()
435
436
437class GClientChildren(object):
438 @staticmethod
439 def add(popen_obj):
440 with GCLIENT_CHILDREN_LOCK:
441 GCLIENT_CHILDREN.append(popen_obj)
442
443 @staticmethod
444 def remove(popen_obj):
445 with GCLIENT_CHILDREN_LOCK:
446 GCLIENT_CHILDREN.remove(popen_obj)
447
448 @staticmethod
449 def _attemptToKillChildren():
450 global GCLIENT_CHILDREN
451 with GCLIENT_CHILDREN_LOCK:
452 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
453
454 for zombie in zombies:
455 try:
456 zombie.kill()
457 except OSError:
458 pass
459
460 with GCLIENT_CHILDREN_LOCK:
461 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
462
463 @staticmethod
464 def _areZombies():
465 with GCLIENT_CHILDREN_LOCK:
466 return bool(GCLIENT_CHILDREN)
467
468 @staticmethod
469 def KillAllRemainingChildren():
470 GClientChildren._attemptToKillChildren()
471
472 if GClientChildren._areZombies():
473 time.sleep(0.5)
474 GClientChildren._attemptToKillChildren()
475
476 with GCLIENT_CHILDREN_LOCK:
477 if GCLIENT_CHILDREN:
478 print >> sys.stderr, 'Could not kill the following subprocesses:'
479 for zombie in GCLIENT_CHILDREN:
480 print >> sys.stderr, ' ', zombie.pid
481
482
maruel@chromium.org17d01792010-09-01 18:07:10 +0000483def CheckCallAndFilter(args, stdout=None, filter_fn=None,
484 print_stdout=None, call_filter_on_first_line=False,
tandrii64103db2016-10-11 05:30:05 -0700485 retry=False, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000486 """Runs a command and calls back a filter function if needed.
487
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000488 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000489 print_stdout: If True, the command's stdout is forwarded to stdout.
490 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000491 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000492 character trimmed.
493 stdout: Can be any bufferable output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000494 retry: If the process exits non-zero, sleep for a brief interval and try
495 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000496
497 stderr is always redirected to stdout.
498 """
499 assert print_stdout or filter_fn
500 stdout = stdout or sys.stdout
hinoka@google.com267f33e2014-02-28 22:02:32 +0000501 output = cStringIO.StringIO()
maruel@chromium.org17d01792010-09-01 18:07:10 +0000502 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000503
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000504 sleep_interval = RETRY_INITIAL_SLEEP
505 run_cwd = kwargs.get('cwd', os.getcwd())
506 for _ in xrange(RETRY_MAX + 1):
507 kid = subprocess2.Popen(
508 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
509 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000510
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000511 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000512
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000513 # Do a flush of stdout before we begin reading from the subprocess2's stdout
514 stdout.flush()
515
516 # Also, we need to forward stdout to prevent weird re-ordering of output.
517 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700518 # normally buffering is done for each line, but if the process requests
519 # input, no end-of-line character is output after the prompt and it would
520 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000521 try:
522 in_byte = kid.stdout.read(1)
523 if in_byte:
524 if call_filter_on_first_line:
525 filter_fn(None)
526 in_line = ''
527 while in_byte:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000528 output.write(in_byte)
529 if print_stdout:
530 stdout.write(in_byte)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000531 if in_byte not in ['\r', '\n']:
532 in_line += in_byte
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000533 else:
534 filter_fn(in_line)
535 in_line = ''
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000536 in_byte = kid.stdout.read(1)
537 # Flush the rest of buffered output. This is only an issue with
538 # stdout/stderr not ending with a \n.
539 if len(in_line):
szager@google.com85d3e3a2011-10-07 17:12:00 +0000540 filter_fn(in_line)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000541 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000542
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000543 # Don't put this in a 'finally,' since the child may still run if we get
544 # an exception.
545 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000546
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000547 except KeyboardInterrupt:
548 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
549 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000550
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000551 if rv == 0:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000552 return output.getvalue()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000553 if not retry:
554 break
tandrii30d95622016-10-11 05:20:26 -0700555 print ("WARNING: subprocess '%s' in %s failed; will retry after a short "
556 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000557 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000558 sleep_interval *= 2
559 raise subprocess2.CalledProcessError(
560 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000561
562
agable@chromium.org5a306a22014-02-24 22:13:59 +0000563class GitFilter(object):
564 """A filter_fn implementation for quieting down git output messages.
565
566 Allows a custom function to skip certain lines (predicate), and will throttle
567 the output of percentage completed lines to only output every X seconds.
568 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000569 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000570
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000571 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000572 """
573 Args:
574 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
575 XX% complete messages) to only be printed at least |time_throttle|
576 seconds apart.
577 predicate (f(line)): An optional function which is invoked for every line.
578 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000579 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000580 """
581 self.last_time = 0
582 self.time_throttle = time_throttle
583 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000584 self.out_fh = out_fh or sys.stdout
585 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000586
587 def __call__(self, line):
588 # git uses an escape sequence to clear the line; elide it.
589 esc = line.find(unichr(033))
590 if esc > -1:
591 line = line[:esc]
592 if self.predicate and not self.predicate(line):
593 return
594 now = time.time()
595 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000596 if match:
597 if match.group(1) != self.progress_prefix:
598 self.progress_prefix = match.group(1)
599 elif now - self.last_time < self.time_throttle:
600 return
601 self.last_time = now
602 self.out_fh.write('[%s] ' % Elapsed())
603 print >> self.out_fh, line
agable@chromium.org5a306a22014-02-24 22:13:59 +0000604
605
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000606def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000607 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000608 real_from_dir = os.path.realpath(from_dir)
609 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000610 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000611 split_path = os.path.split(path)
612 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000613 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000614 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000615
616 # If we did not find the file in the current directory, make sure we are in a
617 # sub directory that is controlled by this configuration.
618 if path != real_from_dir:
619 entries_filename = os.path.join(path, filename + '_entries')
620 if not os.path.exists(entries_filename):
621 # If .gclient_entries does not exist, a previous call to gclient sync
622 # might have failed. In that case, we cannot verify that the .gclient
623 # is the one we want to use. In order to not to cause too much trouble,
624 # just issue a warning and return the path anyway.
Bruce Dawson1c5c1182017-06-13 10:34:06 -0700625 print >> sys.stderr, ("%s missing, %s file in parent directory %s might "
626 "not be the file you want to use." %
627 (entries_filename, filename, path))
jochen@chromium.org20760a52010-09-08 08:47:28 +0000628 return path
629 scope = {}
630 try:
631 exec(FileRead(entries_filename), scope)
632 except SyntaxError, e:
633 SyntaxErrorToError(filename, e)
634 all_directories = scope['entries'].keys()
635 path_to_check = real_from_dir[len(path)+1:]
636 while path_to_check:
637 if path_to_check in all_directories:
638 return path
639 path_to_check = os.path.dirname(path_to_check)
640 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000641
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000642 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000643 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000644
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000645
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000646def PathDifference(root, subpath):
647 """Returns the difference subpath minus root."""
648 root = os.path.realpath(root)
649 subpath = os.path.realpath(subpath)
650 if not subpath.startswith(root):
651 return None
652 # If the root does not have a trailing \ or /, we add it so the returned
653 # path starts immediately after the seperator regardless of whether it is
654 # provided.
655 root = os.path.join(root, '')
656 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000657
658
659def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000660 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000661
rcui@google.com13595ff2011-10-13 01:25:07 +0000662 Returns nearest upper-level directory with the passed in file.
663 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000664 if not path:
665 path = os.getcwd()
666 path = os.path.realpath(path)
667 while True:
668 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000669 if os.path.exists(file_path):
670 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000671 (new_path, _) = os.path.split(path)
672 if new_path == path:
673 return None
674 path = new_path
675
676
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000677def GetMacWinOrLinux():
678 """Returns 'mac', 'win', or 'linux', matching the current platform."""
679 if sys.platform.startswith(('cygwin', 'win')):
680 return 'win'
681 elif sys.platform.startswith('linux'):
682 return 'linux'
683 elif sys.platform == 'darwin':
684 return 'mac'
685 raise Error('Unknown platform: ' + sys.platform)
686
687
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000688def GetPrimarySolutionPath():
689 """Returns the full path to the primary solution. (gclient_root + src)"""
zturner@chromium.org0db9a142014-08-13 23:15:25 +0000690
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000691 gclient_root = FindGclientRoot(os.getcwd())
692 if not gclient_root:
jochen@chromium.orgaaee92f2014-07-02 07:35:31 +0000693 # Some projects might not use .gclient. Try to see whether we're in a git
694 # checkout.
695 top_dir = [os.getcwd()]
696 def filter_fn(line):
hanpfei13f9c372016-08-08 22:05:56 -0700697 repo_root_path = os.path.normpath(line.rstrip('\n'))
698 if os.path.exists(repo_root_path):
699 top_dir[0] = repo_root_path
jochen@chromium.orgaaee92f2014-07-02 07:35:31 +0000700 try:
701 CheckCallAndFilter(["git", "rev-parse", "--show-toplevel"],
702 print_stdout=False, filter_fn=filter_fn)
703 except Exception:
704 pass
705 top_dir = top_dir[0]
706 if os.path.exists(os.path.join(top_dir, 'buildtools')):
jiangj@opera.comd6d15b82015-04-20 06:43:48 +0000707 return top_dir
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000708 return None
kjellander@chromium.orgf7facfa2014-09-05 12:40:28 +0000709
710 # Some projects' top directory is not named 'src'.
711 source_dir_name = GetGClientPrimarySolutionName(gclient_root) or 'src'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000712 return os.path.join(gclient_root, source_dir_name)
713
714
715def GetBuildtoolsPath():
716 """Returns the full path to the buildtools directory.
717 This is based on the root of the checkout containing the current directory."""
718
719 # Overriding the build tools path by environment is highly unsupported and may
720 # break without warning. Do not rely on this for anything important.
721 override = os.environ.get('CHROMIUM_BUILDTOOLS_PATH')
722 if override is not None:
723 return override
724
725 primary_solution = GetPrimarySolutionPath()
sbc@chromium.org9d0644d2015-06-05 23:16:54 +0000726 if not primary_solution:
727 return None
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000728 buildtools_path = os.path.join(primary_solution, 'buildtools')
ncbray@chromium.org43e91582014-11-12 22:38:51 +0000729 if not os.path.exists(buildtools_path):
730 # Buildtools may be in the gclient root.
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000731 gclient_root = FindGclientRoot(os.getcwd())
ncbray@chromium.org43e91582014-11-12 22:38:51 +0000732 buildtools_path = os.path.join(gclient_root, 'buildtools')
733 return buildtools_path
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000734
735
736def GetBuildtoolsPlatformBinaryPath():
737 """Returns the full path to the binary directory for the current platform."""
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000738 buildtools_path = GetBuildtoolsPath()
739 if not buildtools_path:
740 return None
741
742 if sys.platform.startswith(('cygwin', 'win')):
743 subdir = 'win'
744 elif sys.platform == 'darwin':
745 subdir = 'mac'
746 elif sys.platform.startswith('linux'):
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000747 subdir = 'linux64'
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000748 else:
749 raise Error('Unknown platform: ' + sys.platform)
750 return os.path.join(buildtools_path, subdir)
751
752
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000753def GetExeSuffix():
754 """Returns '' or '.exe' depending on how executables work on this platform."""
755 if sys.platform.startswith(('cygwin', 'win')):
756 return '.exe'
757 return ''
758
759
kjellander@chromium.orgf7facfa2014-09-05 12:40:28 +0000760def GetGClientPrimarySolutionName(gclient_root_dir_path):
761 """Returns the name of the primary solution in the .gclient file specified."""
762 gclient_config_file = os.path.join(gclient_root_dir_path, '.gclient')
763 env = {}
764 execfile(gclient_config_file, env)
765 solutions = env.get('solutions', [])
766 if solutions:
767 return solutions[0].get('name')
768 return None
769
770
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000771def GetGClientRootAndEntries(path=None):
772 """Returns the gclient root and the dict of entries."""
773 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000774 root = FindFileUpwards(config_file, path)
775 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000776 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000777 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000778 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000779 env = {}
780 execfile(config_path, env)
781 config_dir = os.path.dirname(config_path)
782 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000783
784
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000785def lockedmethod(method):
786 """Method decorator that holds self.lock for the duration of the call."""
787 def inner(self, *args, **kwargs):
788 try:
789 try:
790 self.lock.acquire()
791 except KeyboardInterrupt:
792 print >> sys.stderr, 'Was deadlocked'
793 raise
794 return method(self, *args, **kwargs)
795 finally:
796 self.lock.release()
797 return inner
798
799
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000800class WorkItem(object):
801 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000802 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
803 # As a workaround, use a single lock. Yep you read it right. Single lock for
804 # all the 100 objects.
805 lock = threading.Lock()
806
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000807 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000808 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000809 self._name = name
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000810 self.outbuf = cStringIO.StringIO()
811 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700812 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000813
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000814 def run(self, work_queue):
815 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000816 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000817 pass
818
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000819 @property
820 def name(self):
821 return self._name
822
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000823
824class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000825 """Runs a set of WorkItem that have interdependencies and were WorkItem are
826 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000827
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200828 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000829 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000830
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000831 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000832 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000833 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000834 """jobs specifies the number of concurrent tasks to allow. progress is a
835 Progress instance."""
836 # Set when a thread is done or a new item is enqueued.
837 self.ready_cond = threading.Condition()
838 # Maximum number of concurrent tasks.
839 self.jobs = jobs
840 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000841 self.queued = []
842 # List of strings representing each Dependency.name that was run.
843 self.ran = []
844 # List of items currently running.
845 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000846 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000847 self.exceptions = Queue.Queue()
848 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000849 self.progress = progress
850 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000851 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000852
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000853 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000854 self.verbose = verbose
855 self.last_join = None
856 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000857
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000858 def enqueue(self, d):
859 """Enqueue one Dependency to be executed later once its requirements are
860 satisfied.
861 """
862 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000863 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000864 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000865 self.queued.append(d)
866 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000867 if self.jobs == 1:
868 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000869 logging.debug('enqueued(%s)' % d.name)
870 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000871 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000872 self.progress.update(0)
873 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000874 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000875 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000876
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000877 def out_cb(self, _):
878 self.last_subproc_output = datetime.datetime.now()
879 return True
880
881 @staticmethod
882 def format_task_output(task, comment=''):
883 if comment:
884 comment = ' (%s)' % comment
885 if task.start and task.finish:
886 elapsed = ' (Elapsed: %s)' % (
887 str(task.finish - task.start).partition('.')[0])
888 else:
889 elapsed = ''
890 return """
891%s%s%s
892----------------------------------------
893%s
894----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000895 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000896
hinoka885e5b12016-06-08 14:40:09 -0700897 def _is_conflict(self, job):
898 """Checks to see if a job will conflict with another running job."""
899 for running_job in self.running:
900 for used_resource in running_job.item.resources:
901 logging.debug('Checking resource %s' % used_resource)
902 if used_resource in job.resources:
903 return True
904 return False
905
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000906 def flush(self, *args, **kwargs):
907 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000908 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000909 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000910 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000911 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000912 while True:
913 # Check for task to run first, then wait.
914 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000915 if not self.exceptions.empty():
916 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000917 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000918 self._flush_terminated_threads()
919 if (not self.queued and not self.running or
920 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000921 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000922 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000923
924 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000925 for i in xrange(len(self.queued)):
926 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000927 if (self.ignore_requirements or
928 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700929 if not self._is_conflict(self.queued[i]):
930 # Start one work item: all its requirements are satisfied.
931 self._run_one_task(self.queued.pop(i), args, kwargs)
932 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000933 else:
934 # Couldn't find an item that could run. Break out the outher loop.
935 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000936
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000937 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000938 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000939 break
940 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000941 try:
942 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000943 # If we haven't printed to terminal for a while, but we have received
944 # spew from a suprocess, let the user know we're still progressing.
945 now = datetime.datetime.now()
946 if (now - self.last_join > datetime.timedelta(seconds=60) and
947 self.last_subproc_output > self.last_join):
948 if self.progress:
949 print >> sys.stdout, ''
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000950 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000951 elapsed = Elapsed()
952 print >> sys.stdout, '[%s] Still working on:' % elapsed
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000953 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000954 for task in self.running:
955 print >> sys.stdout, '[%s] %s' % (elapsed, task.item.name)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000956 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000957 except KeyboardInterrupt:
958 # Help debugging by printing some information:
959 print >> sys.stderr, (
960 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
961 'Running: %d') % (
962 self.jobs,
963 len(self.queued),
964 ', '.join(self.ran),
965 len(self.running)))
966 for i in self.queued:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000967 print >> sys.stderr, '%s (not started): %s' % (
968 i.name, ', '.join(i.requirements))
969 for i in self.running:
970 print >> sys.stderr, self.format_task_output(i.item, 'interrupted')
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000971 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000972 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000973 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000974 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000975
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000976 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000977 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000978 if self.progress:
979 print >> sys.stdout, ''
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000980 # To get back the stack location correctly, the raise a, b, c form must be
981 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000982 e, task = self.exceptions.get()
983 print >> sys.stderr, self.format_task_output(task.item, 'ERROR')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000984 raise e[0], e[1], e[2]
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000985 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000986 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000987
maruel@chromium.org3742c842010-09-09 19:27:14 +0000988 def _flush_terminated_threads(self):
989 """Flush threads that have terminated."""
990 running = self.running
991 self.running = []
992 for t in running:
993 if t.isAlive():
994 self.running.append(t)
995 else:
996 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000997 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000998 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000999 if self.verbose:
1000 print >> sys.stdout, self.format_task_output(t.item)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001001 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +00001002 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +00001003 if t.item.name in self.ran:
1004 raise Error(
1005 'gclient is confused, "%s" is already in "%s"' % (
1006 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +00001007 if not t.item.name in self.ran:
1008 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001009
1010 def _run_one_task(self, task_item, args, kwargs):
1011 if self.jobs > 1:
1012 # Start the thread.
1013 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +00001014 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001015 self.running.append(new_thread)
1016 new_thread.start()
1017 else:
1018 # Run the 'thread' inside the main thread. Don't try to catch any
1019 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001020 try:
1021 task_item.start = datetime.datetime.now()
1022 print >> task_item.outbuf, '[%s] Started.' % Elapsed(task_item.start)
1023 task_item.run(*args, **kwargs)
1024 task_item.finish = datetime.datetime.now()
1025 print >> task_item.outbuf, '[%s] Finished.' % Elapsed(task_item.finish)
1026 self.ran.append(task_item.name)
1027 if self.verbose:
1028 if self.progress:
1029 print >> sys.stdout, ''
1030 print >> sys.stdout, self.format_task_output(task_item)
1031 if self.progress:
1032 self.progress.update(1, ', '.join(t.item.name for t in self.running))
1033 except KeyboardInterrupt:
1034 print >> sys.stderr, self.format_task_output(task_item, 'interrupted')
1035 raise
1036 except Exception:
1037 print >> sys.stderr, self.format_task_output(task_item, 'ERROR')
1038 raise
1039
maruel@chromium.org3742c842010-09-09 19:27:14 +00001040
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001041 class _Worker(threading.Thread):
1042 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001043 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001044 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001045 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001046 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001047 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001048 self.args = args
1049 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001050 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001051
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001052 def run(self):
1053 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001054 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001055 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001056 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001057 self.item.start = datetime.datetime.now()
1058 print >> self.item.outbuf, '[%s] Started.' % Elapsed(self.item.start)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001059 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001060 self.item.finish = datetime.datetime.now()
1061 print >> self.item.outbuf, '[%s] Finished.' % Elapsed(self.item.finish)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001062 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001063 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001064 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001065 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001066 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001067 except Exception:
1068 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001069 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001070 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001071 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001072 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001073 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001074 work_queue.ready_cond.acquire()
1075 try:
1076 work_queue.ready_cond.notifyAll()
1077 finally:
1078 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001079
1080
agable92bec4f2016-08-24 09:27:27 -07001081def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001082 """Returns the most plausible editor to use.
1083
1084 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001085 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001086 - core.editor git configuration variable (if supplied by git-cl)
1087 - VISUAL environment variable
1088 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001089 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001090
1091 In the case of git-cl, this matches git's behaviour, except that it does not
1092 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001093 """
agable92bec4f2016-08-24 09:27:27 -07001094 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001095 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001096 editor = os.environ.get('VISUAL')
1097 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001098 editor = os.environ.get('EDITOR')
1099 if not editor:
1100 if sys.platform.startswith('win'):
1101 editor = 'notepad'
1102 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001103 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001104 return editor
1105
1106
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001107def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001108 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001109 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001110 # Make sure CRLF is handled properly by requiring none.
1111 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +00001112 print >> sys.stderr, (
1113 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001114 fileobj = os.fdopen(file_handle, 'w')
1115 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001116 content = re.sub('\r?\n', '\n', content)
1117 # Some editors complain when the file doesn't end in \n.
1118 if not content.endswith('\n'):
1119 content += '\n'
1120 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001121 fileobj.close()
1122
1123 try:
agable92bec4f2016-08-24 09:27:27 -07001124 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001125 if not editor:
1126 return None
1127 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001128 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1129 # Msysgit requires the usage of 'env' to be present.
1130 cmd = 'env ' + cmd
1131 try:
1132 # shell=True to allow the shell to handle all forms of quotes in
1133 # $EDITOR.
1134 subprocess2.check_call(cmd, shell=True)
1135 except subprocess2.CalledProcessError:
1136 return None
1137 return FileRead(filename)
1138 finally:
1139 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001140
1141
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001142def UpgradeToHttps(url):
1143 """Upgrades random urls to https://.
1144
1145 Do not touch unknown urls like ssh:// or git://.
1146 Do not touch http:// urls with a port number,
1147 Fixes invalid GAE url.
1148 """
1149 if not url:
1150 return url
1151 if not re.match(r'[a-z\-]+\://.*', url):
1152 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1153 # relative url and will use http:///foo. Note that it defaults to http://
1154 # for compatibility with naked url like "localhost:8080".
1155 url = 'http://%s' % url
1156 parsed = list(urlparse.urlparse(url))
1157 # Do not automatically upgrade http to https if a port number is provided.
1158 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1159 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001160 return urlparse.urlunparse(parsed)
1161
1162
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001163def ParseCodereviewSettingsContent(content):
1164 """Process a codereview.settings file properly."""
1165 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1166 try:
1167 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1168 except ValueError:
1169 raise Error(
1170 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001171 def fix_url(key):
1172 if keyvals.get(key):
1173 keyvals[key] = UpgradeToHttps(keyvals[key])
1174 fix_url('CODE_REVIEW_SERVER')
1175 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001176 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001177
1178
1179def NumLocalCpus():
1180 """Returns the number of processors.
1181
dnj@chromium.org530523b2015-01-07 19:54:57 +00001182 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1183 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1184 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001185 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001186 # Surround the entire thing in try/except; no failure here should stop gclient
1187 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001188 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001189 # Use multiprocessing to get CPU count. This may raise
1190 # NotImplementedError.
1191 try:
1192 import multiprocessing
1193 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001194 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001195 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001196 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001197 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1198 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1199
1200 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1201 if 'NUMBER_OF_PROCESSORS' in os.environ:
1202 return int(os.environ['NUMBER_OF_PROCESSORS'])
1203 except Exception as e:
1204 logging.exception("Exception raised while probing CPU count: %s", e)
1205
1206 logging.debug('Failed to get CPU count. Defaulting to 1.')
1207 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001208
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001209
szager@chromium.orgfc616382014-03-18 20:32:04 +00001210def DefaultDeltaBaseCacheLimit():
1211 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1212
1213 The primary constraint is the address space of virtual memory. The cache
1214 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1215 parameter is set too high.
1216 """
1217 if platform.architecture()[0].startswith('64'):
1218 return '2g'
1219 else:
1220 return '512m'
1221
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001222
szager@chromium.orgff113292014-03-25 06:02:08 +00001223def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001224 """Return reasonable default values for configuring git-index-pack.
1225
1226 Experiments suggest that higher values for pack.threads don't improve
1227 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001228 cache_limit = DefaultDeltaBaseCacheLimit()
1229 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1230 if url in THREADED_INDEX_PACK_BLACKLIST:
1231 result.extend(['-c', 'pack.threads=1'])
1232 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001233
1234
1235def FindExecutable(executable):
1236 """This mimics the "which" utility."""
1237 path_folders = os.environ.get('PATH').split(os.pathsep)
1238
1239 for path_folder in path_folders:
1240 target = os.path.join(path_folder, executable)
1241 # Just incase we have some ~/blah paths.
1242 target = os.path.abspath(os.path.expanduser(target))
1243 if os.path.isfile(target) and os.access(target, os.X_OK):
1244 return target
1245 if sys.platform.startswith('win'):
1246 for suffix in ('.bat', '.cmd', '.exe'):
1247 alt_target = target + suffix
1248 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1249 return alt_target
1250 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001251
1252
1253def freeze(obj):
1254 """Takes a generic object ``obj``, and returns an immutable version of it.
1255
1256 Supported types:
1257 * dict / OrderedDict -> FrozenDict
1258 * list -> tuple
1259 * set -> frozenset
1260 * any object with a working __hash__ implementation (assumes that hashable
1261 means immutable)
1262
1263 Will raise TypeError if you pass an object which is not hashable.
1264 """
1265 if isinstance(obj, dict):
1266 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.iteritems())
1267 elif isinstance(obj, (list, tuple)):
1268 return tuple(freeze(i) for i in obj)
1269 elif isinstance(obj, set):
1270 return frozenset(freeze(i) for i in obj)
1271 else:
1272 hash(obj)
1273 return obj
1274
1275
1276class FrozenDict(collections.Mapping):
1277 """An immutable OrderedDict.
1278
1279 Modified From: http://stackoverflow.com/a/2704866
1280 """
1281 def __init__(self, *args, **kwargs):
1282 self._d = collections.OrderedDict(*args, **kwargs)
1283
1284 # Calculate the hash immediately so that we know all the items are
1285 # hashable too.
1286 self._hash = reduce(operator.xor,
1287 (hash(i) for i in enumerate(self._d.iteritems())), 0)
1288
1289 def __eq__(self, other):
1290 if not isinstance(other, collections.Mapping):
1291 return NotImplemented
1292 if self is other:
1293 return True
1294 if len(self) != len(other):
1295 return False
1296 for k, v in self.iteritems():
1297 if k not in other or other[k] != v:
1298 return False
1299 return True
1300
1301 def __iter__(self):
1302 return iter(self._d)
1303
1304 def __len__(self):
1305 return len(self._d)
1306
1307 def __getitem__(self, key):
1308 return self._d[key]
1309
1310 def __hash__(self):
1311 return self._hash
1312
1313 def __repr__(self):
1314 return 'FrozenDict(%r)' % (self._d.items(),)