blob: 48c023fddf74a370ddbe922c907cd06bf8dce05d [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)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000228 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000229 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 Chenga0c5f082017-10-19 13:35:19 -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
Edward Lemur231f5ea2018-01-31 19:02:36 +0100328class WriteToStdout(Wrapper):
329 """Creates a file object clone to also print to sys.stdout."""
330 def __init__(self, wrapped):
331 super(WriteToStdout, self).__init__(wrapped)
332 if not hasattr(self, 'lock'):
333 self.lock = threading.Lock()
334
335 def write(self, out, *args, **kwargs):
336 self._wrapped.write(out, *args, **kwargs)
337 self.lock.acquire()
338 try:
339 sys.stdout.write(out, *args, **kwargs)
340 finally:
341 self.lock.release()
342
343
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000344class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000345 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000346 def __init__(self, wrapped, delay):
347 super(AutoFlush, self).__init__(wrapped)
348 if not hasattr(self, 'lock'):
349 self.lock = threading.Lock()
350 self.__last_flushed_at = time.time()
351 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000352
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000353 @property
354 def autoflush(self):
355 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000356
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000357 def write(self, out, *args, **kwargs):
358 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000359 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000360 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000361 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000362 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000363 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000364 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000365 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000366 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000367 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000368 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000369
370
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000371class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000372 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000373 threads with a NN> prefix.
374 """
375 def __init__(self, wrapped, include_zero=False):
376 super(Annotated, self).__init__(wrapped)
377 if not hasattr(self, 'lock'):
378 self.lock = threading.Lock()
379 self.__output_buffers = {}
380 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000381
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000382 @property
383 def annotated(self):
384 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000385
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000386 def write(self, out):
387 index = getattr(threading.currentThread(), 'index', 0)
388 if not index and not self.__include_zero:
389 # Unindexed threads aren't buffered.
390 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000391
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000392 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000393 try:
394 # Use a dummy array to hold the string so the code can be lockless.
395 # Strings are immutable, requiring to keep a lock for the whole dictionary
396 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000397 if not index in self.__output_buffers:
398 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000399 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000400 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000401 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000402 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000403
404 # Continue lockless.
405 obj[0] += out
406 while '\n' in obj[0]:
407 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000408 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000409 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000410 obj[0] = remaining
411
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000412 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000413 """Flush buffered output."""
414 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000415 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000416 try:
417 # Detect threads no longer existing.
418 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000419 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000420 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000421 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000422 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000423 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000424 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000425 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000426 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000427
428 # Don't keep the lock while writting. Will append \n when it shouldn't.
429 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000430 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000431 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
432 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000433
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000434
435def MakeFileAutoFlush(fileobj, delay=10):
436 autoflush = getattr(fileobj, 'autoflush', None)
437 if autoflush:
438 autoflush.delay = delay
439 return fileobj
440 return AutoFlush(fileobj, delay)
441
442
443def MakeFileAnnotated(fileobj, include_zero=False):
444 if getattr(fileobj, 'annotated', None):
445 return fileobj
446 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000447
448
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000449GCLIENT_CHILDREN = []
450GCLIENT_CHILDREN_LOCK = threading.Lock()
451
452
453class GClientChildren(object):
454 @staticmethod
455 def add(popen_obj):
456 with GCLIENT_CHILDREN_LOCK:
457 GCLIENT_CHILDREN.append(popen_obj)
458
459 @staticmethod
460 def remove(popen_obj):
461 with GCLIENT_CHILDREN_LOCK:
462 GCLIENT_CHILDREN.remove(popen_obj)
463
464 @staticmethod
465 def _attemptToKillChildren():
466 global GCLIENT_CHILDREN
467 with GCLIENT_CHILDREN_LOCK:
468 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
469
470 for zombie in zombies:
471 try:
472 zombie.kill()
473 except OSError:
474 pass
475
476 with GCLIENT_CHILDREN_LOCK:
477 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
478
479 @staticmethod
480 def _areZombies():
481 with GCLIENT_CHILDREN_LOCK:
482 return bool(GCLIENT_CHILDREN)
483
484 @staticmethod
485 def KillAllRemainingChildren():
486 GClientChildren._attemptToKillChildren()
487
488 if GClientChildren._areZombies():
489 time.sleep(0.5)
490 GClientChildren._attemptToKillChildren()
491
492 with GCLIENT_CHILDREN_LOCK:
493 if GCLIENT_CHILDREN:
494 print >> sys.stderr, 'Could not kill the following subprocesses:'
495 for zombie in GCLIENT_CHILDREN:
496 print >> sys.stderr, ' ', zombie.pid
497
498
maruel@chromium.org17d01792010-09-01 18:07:10 +0000499def CheckCallAndFilter(args, stdout=None, filter_fn=None,
500 print_stdout=None, call_filter_on_first_line=False,
tandrii64103db2016-10-11 05:30:05 -0700501 retry=False, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000502 """Runs a command and calls back a filter function if needed.
503
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000504 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000505 print_stdout: If True, the command's stdout is forwarded to stdout.
506 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000507 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000508 character trimmed.
509 stdout: Can be any bufferable output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000510 retry: If the process exits non-zero, sleep for a brief interval and try
511 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000512
513 stderr is always redirected to stdout.
514 """
515 assert print_stdout or filter_fn
516 stdout = stdout or sys.stdout
hinoka@google.com267f33e2014-02-28 22:02:32 +0000517 output = cStringIO.StringIO()
maruel@chromium.org17d01792010-09-01 18:07:10 +0000518 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000519
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000520 sleep_interval = RETRY_INITIAL_SLEEP
521 run_cwd = kwargs.get('cwd', os.getcwd())
522 for _ in xrange(RETRY_MAX + 1):
523 kid = subprocess2.Popen(
524 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
525 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000526
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000527 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000528
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000529 # Do a flush of stdout before we begin reading from the subprocess2's stdout
530 stdout.flush()
531
532 # Also, we need to forward stdout to prevent weird re-ordering of output.
533 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700534 # normally buffering is done for each line, but if the process requests
535 # input, no end-of-line character is output after the prompt and it would
536 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000537 try:
538 in_byte = kid.stdout.read(1)
539 if in_byte:
540 if call_filter_on_first_line:
541 filter_fn(None)
542 in_line = ''
543 while in_byte:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000544 output.write(in_byte)
545 if print_stdout:
546 stdout.write(in_byte)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000547 if in_byte not in ['\r', '\n']:
548 in_line += in_byte
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000549 else:
550 filter_fn(in_line)
551 in_line = ''
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000552 in_byte = kid.stdout.read(1)
553 # Flush the rest of buffered output. This is only an issue with
554 # stdout/stderr not ending with a \n.
555 if len(in_line):
szager@google.com85d3e3a2011-10-07 17:12:00 +0000556 filter_fn(in_line)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000557 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000558
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000559 # Don't put this in a 'finally,' since the child may still run if we get
560 # an exception.
561 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000562
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000563 except KeyboardInterrupt:
564 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
565 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000566
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000567 if rv == 0:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000568 return output.getvalue()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000569 if not retry:
570 break
tandrii30d95622016-10-11 05:20:26 -0700571 print ("WARNING: subprocess '%s' in %s failed; will retry after a short "
572 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000573 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000574 sleep_interval *= 2
575 raise subprocess2.CalledProcessError(
576 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000577
578
agable@chromium.org5a306a22014-02-24 22:13:59 +0000579class GitFilter(object):
580 """A filter_fn implementation for quieting down git output messages.
581
582 Allows a custom function to skip certain lines (predicate), and will throttle
583 the output of percentage completed lines to only output every X seconds.
584 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000585 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000586
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000587 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000588 """
589 Args:
590 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
591 XX% complete messages) to only be printed at least |time_throttle|
592 seconds apart.
593 predicate (f(line)): An optional function which is invoked for every line.
594 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000595 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000596 """
597 self.last_time = 0
598 self.time_throttle = time_throttle
599 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000600 self.out_fh = out_fh or sys.stdout
601 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000602
603 def __call__(self, line):
604 # git uses an escape sequence to clear the line; elide it.
605 esc = line.find(unichr(033))
606 if esc > -1:
607 line = line[:esc]
608 if self.predicate and not self.predicate(line):
609 return
610 now = time.time()
611 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000612 if match:
613 if match.group(1) != self.progress_prefix:
614 self.progress_prefix = match.group(1)
615 elif now - self.last_time < self.time_throttle:
616 return
617 self.last_time = now
618 self.out_fh.write('[%s] ' % Elapsed())
619 print >> self.out_fh, line
agable@chromium.org5a306a22014-02-24 22:13:59 +0000620
621
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000622def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000623 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000624
rcui@google.com13595ff2011-10-13 01:25:07 +0000625 Returns nearest upper-level directory with the passed in file.
626 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000627 if not path:
628 path = os.getcwd()
629 path = os.path.realpath(path)
630 while True:
631 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000632 if os.path.exists(file_path):
633 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000634 (new_path, _) = os.path.split(path)
635 if new_path == path:
636 return None
637 path = new_path
638
639
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000640def GetMacWinOrLinux():
641 """Returns 'mac', 'win', or 'linux', matching the current platform."""
642 if sys.platform.startswith(('cygwin', 'win')):
643 return 'win'
644 elif sys.platform.startswith('linux'):
645 return 'linux'
646 elif sys.platform == 'darwin':
647 return 'mac'
648 raise Error('Unknown platform: ' + sys.platform)
649
650
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000651def GetGClientRootAndEntries(path=None):
652 """Returns the gclient root and the dict of entries."""
653 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000654 root = FindFileUpwards(config_file, path)
655 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000656 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000657 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000658 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000659 env = {}
660 execfile(config_path, env)
661 config_dir = os.path.dirname(config_path)
662 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000663
664
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000665def lockedmethod(method):
666 """Method decorator that holds self.lock for the duration of the call."""
667 def inner(self, *args, **kwargs):
668 try:
669 try:
670 self.lock.acquire()
671 except KeyboardInterrupt:
672 print >> sys.stderr, 'Was deadlocked'
673 raise
674 return method(self, *args, **kwargs)
675 finally:
676 self.lock.release()
677 return inner
678
679
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000680class WorkItem(object):
681 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000682 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
683 # As a workaround, use a single lock. Yep you read it right. Single lock for
684 # all the 100 objects.
685 lock = threading.Lock()
686
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000687 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000688 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000689 self._name = name
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000690 self.outbuf = cStringIO.StringIO()
691 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700692 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000693
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000694 def run(self, work_queue):
695 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000696 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000697 pass
698
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000699 @property
700 def name(self):
701 return self._name
702
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000703
704class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000705 """Runs a set of WorkItem that have interdependencies and were WorkItem are
706 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000707
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200708 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000709 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000710
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000711 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000712 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000713 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000714 """jobs specifies the number of concurrent tasks to allow. progress is a
715 Progress instance."""
716 # Set when a thread is done or a new item is enqueued.
717 self.ready_cond = threading.Condition()
718 # Maximum number of concurrent tasks.
719 self.jobs = jobs
720 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000721 self.queued = []
722 # List of strings representing each Dependency.name that was run.
723 self.ran = []
724 # List of items currently running.
725 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000726 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000727 self.exceptions = Queue.Queue()
728 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000729 self.progress = progress
730 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000731 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000732
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000733 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000734 self.verbose = verbose
735 self.last_join = None
736 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000737
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000738 def enqueue(self, d):
739 """Enqueue one Dependency to be executed later once its requirements are
740 satisfied.
741 """
742 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000743 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000744 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000745 self.queued.append(d)
746 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000747 if self.jobs == 1:
748 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000749 logging.debug('enqueued(%s)' % d.name)
750 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000751 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000752 self.progress.update(0)
753 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000754 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000755 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000756
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000757 def out_cb(self, _):
758 self.last_subproc_output = datetime.datetime.now()
759 return True
760
761 @staticmethod
762 def format_task_output(task, comment=''):
763 if comment:
764 comment = ' (%s)' % comment
765 if task.start and task.finish:
766 elapsed = ' (Elapsed: %s)' % (
767 str(task.finish - task.start).partition('.')[0])
768 else:
769 elapsed = ''
770 return """
771%s%s%s
772----------------------------------------
773%s
774----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000775 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000776
hinoka885e5b12016-06-08 14:40:09 -0700777 def _is_conflict(self, job):
778 """Checks to see if a job will conflict with another running job."""
779 for running_job in self.running:
780 for used_resource in running_job.item.resources:
781 logging.debug('Checking resource %s' % used_resource)
782 if used_resource in job.resources:
783 return True
784 return False
785
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000786 def flush(self, *args, **kwargs):
787 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000788 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000789 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000790 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000791 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000792 while True:
793 # Check for task to run first, then wait.
794 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000795 if not self.exceptions.empty():
796 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000797 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000798 self._flush_terminated_threads()
799 if (not self.queued and not self.running or
800 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000801 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000802 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000803
804 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000805 for i in xrange(len(self.queued)):
806 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000807 if (self.ignore_requirements or
808 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700809 if not self._is_conflict(self.queued[i]):
810 # Start one work item: all its requirements are satisfied.
811 self._run_one_task(self.queued.pop(i), args, kwargs)
812 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000813 else:
814 # Couldn't find an item that could run. Break out the outher loop.
815 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000816
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000817 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000818 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000819 break
820 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000821 try:
822 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000823 # If we haven't printed to terminal for a while, but we have received
824 # spew from a suprocess, let the user know we're still progressing.
825 now = datetime.datetime.now()
826 if (now - self.last_join > datetime.timedelta(seconds=60) and
827 self.last_subproc_output > self.last_join):
828 if self.progress:
829 print >> sys.stdout, ''
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000830 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000831 elapsed = Elapsed()
832 print >> sys.stdout, '[%s] Still working on:' % elapsed
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000833 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000834 for task in self.running:
835 print >> sys.stdout, '[%s] %s' % (elapsed, task.item.name)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000836 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000837 except KeyboardInterrupt:
838 # Help debugging by printing some information:
839 print >> sys.stderr, (
840 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
841 'Running: %d') % (
842 self.jobs,
843 len(self.queued),
844 ', '.join(self.ran),
845 len(self.running)))
846 for i in self.queued:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000847 print >> sys.stderr, '%s (not started): %s' % (
848 i.name, ', '.join(i.requirements))
849 for i in self.running:
850 print >> sys.stderr, self.format_task_output(i.item, 'interrupted')
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000851 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000852 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000853 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000854 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000855
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000856 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000857 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000858 if self.progress:
859 print >> sys.stdout, ''
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000860 # To get back the stack location correctly, the raise a, b, c form must be
861 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000862 e, task = self.exceptions.get()
863 print >> sys.stderr, self.format_task_output(task.item, 'ERROR')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000864 raise e[0], e[1], e[2]
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000865 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000866 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000867
maruel@chromium.org3742c842010-09-09 19:27:14 +0000868 def _flush_terminated_threads(self):
869 """Flush threads that have terminated."""
870 running = self.running
871 self.running = []
872 for t in running:
873 if t.isAlive():
874 self.running.append(t)
875 else:
876 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000877 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000878 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000879 if self.verbose:
880 print >> sys.stdout, self.format_task_output(t.item)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000881 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000882 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000883 if t.item.name in self.ran:
884 raise Error(
885 'gclient is confused, "%s" is already in "%s"' % (
886 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000887 if not t.item.name in self.ran:
888 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000889
890 def _run_one_task(self, task_item, args, kwargs):
891 if self.jobs > 1:
892 # Start the thread.
893 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000894 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000895 self.running.append(new_thread)
896 new_thread.start()
897 else:
898 # Run the 'thread' inside the main thread. Don't try to catch any
899 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000900 try:
901 task_item.start = datetime.datetime.now()
902 print >> task_item.outbuf, '[%s] Started.' % Elapsed(task_item.start)
903 task_item.run(*args, **kwargs)
904 task_item.finish = datetime.datetime.now()
905 print >> task_item.outbuf, '[%s] Finished.' % Elapsed(task_item.finish)
906 self.ran.append(task_item.name)
907 if self.verbose:
908 if self.progress:
909 print >> sys.stdout, ''
910 print >> sys.stdout, self.format_task_output(task_item)
911 if self.progress:
912 self.progress.update(1, ', '.join(t.item.name for t in self.running))
913 except KeyboardInterrupt:
914 print >> sys.stderr, self.format_task_output(task_item, 'interrupted')
915 raise
916 except Exception:
917 print >> sys.stderr, self.format_task_output(task_item, 'ERROR')
918 raise
919
maruel@chromium.org3742c842010-09-09 19:27:14 +0000920
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000921 class _Worker(threading.Thread):
922 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000923 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000924 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000925 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000926 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000927 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000928 self.args = args
929 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000930 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000931
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000932 def run(self):
933 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000934 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000935 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000936 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000937 self.item.start = datetime.datetime.now()
938 print >> self.item.outbuf, '[%s] Started.' % Elapsed(self.item.start)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000939 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000940 self.item.finish = datetime.datetime.now()
941 print >> self.item.outbuf, '[%s] Finished.' % Elapsed(self.item.finish)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000942 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000943 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000944 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000945 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000946 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000947 except Exception:
948 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000949 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000950 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000951 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000952 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000953 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000954 work_queue.ready_cond.acquire()
955 try:
956 work_queue.ready_cond.notifyAll()
957 finally:
958 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000959
960
agable92bec4f2016-08-24 09:27:27 -0700961def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000962 """Returns the most plausible editor to use.
963
964 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -0700965 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000966 - core.editor git configuration variable (if supplied by git-cl)
967 - VISUAL environment variable
968 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +0000969 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000970
971 In the case of git-cl, this matches git's behaviour, except that it does not
972 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000973 """
agable92bec4f2016-08-24 09:27:27 -0700974 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000975 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000976 editor = os.environ.get('VISUAL')
977 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000978 editor = os.environ.get('EDITOR')
979 if not editor:
980 if sys.platform.startswith('win'):
981 editor = 'notepad'
982 else:
bratell@opera.com65621c72013-12-09 15:05:32 +0000983 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000984 return editor
985
986
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000987def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000988 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +0000989 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000990 # Make sure CRLF is handled properly by requiring none.
991 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000992 print >> sys.stderr, (
993 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000994 fileobj = os.fdopen(file_handle, 'w')
995 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +0000996 content = re.sub('\r?\n', '\n', content)
997 # Some editors complain when the file doesn't end in \n.
998 if not content.endswith('\n'):
999 content += '\n'
1000 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001001 fileobj.close()
1002
1003 try:
agable92bec4f2016-08-24 09:27:27 -07001004 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001005 if not editor:
1006 return None
1007 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001008 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1009 # Msysgit requires the usage of 'env' to be present.
1010 cmd = 'env ' + cmd
1011 try:
1012 # shell=True to allow the shell to handle all forms of quotes in
1013 # $EDITOR.
1014 subprocess2.check_call(cmd, shell=True)
1015 except subprocess2.CalledProcessError:
1016 return None
1017 return FileRead(filename)
1018 finally:
1019 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001020
1021
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001022def UpgradeToHttps(url):
1023 """Upgrades random urls to https://.
1024
1025 Do not touch unknown urls like ssh:// or git://.
1026 Do not touch http:// urls with a port number,
1027 Fixes invalid GAE url.
1028 """
1029 if not url:
1030 return url
1031 if not re.match(r'[a-z\-]+\://.*', url):
1032 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1033 # relative url and will use http:///foo. Note that it defaults to http://
1034 # for compatibility with naked url like "localhost:8080".
1035 url = 'http://%s' % url
1036 parsed = list(urlparse.urlparse(url))
1037 # Do not automatically upgrade http to https if a port number is provided.
1038 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1039 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001040 return urlparse.urlunparse(parsed)
1041
1042
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001043def ParseCodereviewSettingsContent(content):
1044 """Process a codereview.settings file properly."""
1045 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1046 try:
1047 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1048 except ValueError:
1049 raise Error(
1050 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001051 def fix_url(key):
1052 if keyvals.get(key):
1053 keyvals[key] = UpgradeToHttps(keyvals[key])
1054 fix_url('CODE_REVIEW_SERVER')
1055 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001056 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001057
1058
1059def NumLocalCpus():
1060 """Returns the number of processors.
1061
dnj@chromium.org530523b2015-01-07 19:54:57 +00001062 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1063 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1064 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001065 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001066 # Surround the entire thing in try/except; no failure here should stop gclient
1067 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001068 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001069 # Use multiprocessing to get CPU count. This may raise
1070 # NotImplementedError.
1071 try:
1072 import multiprocessing
1073 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001074 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001075 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001076 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001077 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1078 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1079
1080 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1081 if 'NUMBER_OF_PROCESSORS' in os.environ:
1082 return int(os.environ['NUMBER_OF_PROCESSORS'])
1083 except Exception as e:
1084 logging.exception("Exception raised while probing CPU count: %s", e)
1085
1086 logging.debug('Failed to get CPU count. Defaulting to 1.')
1087 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001088
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001089
szager@chromium.orgfc616382014-03-18 20:32:04 +00001090def DefaultDeltaBaseCacheLimit():
1091 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1092
1093 The primary constraint is the address space of virtual memory. The cache
1094 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1095 parameter is set too high.
1096 """
1097 if platform.architecture()[0].startswith('64'):
1098 return '2g'
1099 else:
1100 return '512m'
1101
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001102
szager@chromium.orgff113292014-03-25 06:02:08 +00001103def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001104 """Return reasonable default values for configuring git-index-pack.
1105
1106 Experiments suggest that higher values for pack.threads don't improve
1107 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001108 cache_limit = DefaultDeltaBaseCacheLimit()
1109 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1110 if url in THREADED_INDEX_PACK_BLACKLIST:
1111 result.extend(['-c', 'pack.threads=1'])
1112 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001113
1114
1115def FindExecutable(executable):
1116 """This mimics the "which" utility."""
1117 path_folders = os.environ.get('PATH').split(os.pathsep)
1118
1119 for path_folder in path_folders:
1120 target = os.path.join(path_folder, executable)
1121 # Just incase we have some ~/blah paths.
1122 target = os.path.abspath(os.path.expanduser(target))
1123 if os.path.isfile(target) and os.access(target, os.X_OK):
1124 return target
1125 if sys.platform.startswith('win'):
1126 for suffix in ('.bat', '.cmd', '.exe'):
1127 alt_target = target + suffix
1128 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1129 return alt_target
1130 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001131
1132
1133def freeze(obj):
1134 """Takes a generic object ``obj``, and returns an immutable version of it.
1135
1136 Supported types:
1137 * dict / OrderedDict -> FrozenDict
1138 * list -> tuple
1139 * set -> frozenset
1140 * any object with a working __hash__ implementation (assumes that hashable
1141 means immutable)
1142
1143 Will raise TypeError if you pass an object which is not hashable.
1144 """
Edward Lesmes6f64a052018-03-20 17:35:49 -04001145 if isinstance(obj, collections.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001146 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.iteritems())
1147 elif isinstance(obj, (list, tuple)):
1148 return tuple(freeze(i) for i in obj)
1149 elif isinstance(obj, set):
1150 return frozenset(freeze(i) for i in obj)
1151 else:
1152 hash(obj)
1153 return obj
1154
1155
1156class FrozenDict(collections.Mapping):
1157 """An immutable OrderedDict.
1158
1159 Modified From: http://stackoverflow.com/a/2704866
1160 """
1161 def __init__(self, *args, **kwargs):
1162 self._d = collections.OrderedDict(*args, **kwargs)
1163
1164 # Calculate the hash immediately so that we know all the items are
1165 # hashable too.
1166 self._hash = reduce(operator.xor,
1167 (hash(i) for i in enumerate(self._d.iteritems())), 0)
1168
1169 def __eq__(self, other):
1170 if not isinstance(other, collections.Mapping):
1171 return NotImplemented
1172 if self is other:
1173 return True
1174 if len(self) != len(other):
1175 return False
1176 for k, v in self.iteritems():
1177 if k not in other or other[k] != v:
1178 return False
1179 return True
1180
1181 def __iter__(self):
1182 return iter(self._d)
1183
1184 def __len__(self):
1185 return len(self._d)
1186
1187 def __getitem__(self, key):
1188 return self._d[key]
1189
1190 def __hash__(self):
1191 return self._hash
1192
1193 def __repr__(self):
1194 return 'FrozenDict(%r)' % (self._d.items(),)