blob: 96989a99e3dd54b78080728793a959368cda63d4 [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
hinoka@google.com267f33e2014-02-28 22:02:32 +00008import cStringIO
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00009import datetime
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000010import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000011import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000012import pipes
szager@chromium.orgfc616382014-03-18 20:32:04 +000013import platform
maruel@chromium.org3742c842010-09-09 19:27:14 +000014import Queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000015import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000016import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000017import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000018import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000019import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000020import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000021import time
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000022import urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000023
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000024import subprocess2
25
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000026
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000027RETRY_MAX = 3
28RETRY_INITIAL_SLEEP = 0.5
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000029START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000030
31
borenet@google.com6a9b1682014-03-24 18:35:23 +000032_WARNINGS = []
33
34
szager@chromium.orgff113292014-03-25 06:02:08 +000035# These repos are known to cause OOM errors on 32-bit platforms, due the the
36# very large objects they contain. It is not safe to use threaded index-pack
37# when cloning/fetching them.
38THREADED_INDEX_PACK_BLACKLIST = [
39 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
40]
41
42
maruel@chromium.org66c83e62010-09-07 14:18:45 +000043class Error(Exception):
44 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000045 def __init__(self, msg, *args, **kwargs):
46 index = getattr(threading.currentThread(), 'index', 0)
47 if index:
48 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
49 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000050
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000051
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000052def Elapsed(until=None):
53 if until is None:
54 until = datetime.datetime.now()
55 return str(until - START).partition('.')[0]
56
57
borenet@google.com6a9b1682014-03-24 18:35:23 +000058def PrintWarnings():
59 """Prints any accumulated warnings."""
60 if _WARNINGS:
61 print >> sys.stderr, '\n\nWarnings:'
62 for warning in _WARNINGS:
63 print >> sys.stderr, warning
64
65
66def AddWarning(msg):
67 """Adds the given warning message to the list of accumulated warnings."""
68 _WARNINGS.append(msg)
69
70
msb@chromium.orgac915bb2009-11-13 17:03:01 +000071def SplitUrlRevision(url):
72 """Splits url and returns a two-tuple: url, rev"""
73 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000074 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +000075 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000076 components = re.search(regex, url).groups()
77 else:
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +000078 components = url.rsplit('@', 1)
79 if re.match(r'^\w+\@', url) and '@' not in components[0]:
80 components = [url]
81
msb@chromium.orgac915bb2009-11-13 17:03:01 +000082 if len(components) == 1:
83 components += [None]
84 return tuple(components)
85
86
primiano@chromium.org5439ea52014-08-06 17:18:18 +000087def IsGitSha(revision):
88 """Returns true if the given string is a valid hex-encoded sha"""
89 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
90
91
floitsch@google.comeaab7842011-04-28 09:07:58 +000092def IsDateRevision(revision):
93 """Returns true if the given revision is of the form "{ ... }"."""
94 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
95
96
97def MakeDateRevision(date):
98 """Returns a revision representing the latest revision before the given
99 date."""
100 return "{" + date + "}"
101
102
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000103def SyntaxErrorToError(filename, e):
104 """Raises a gclient_utils.Error exception with the human readable message"""
105 try:
106 # Try to construct a human readable error message
107 if filename:
108 error_message = 'There is a syntax error in %s\n' % filename
109 else:
110 error_message = 'There is a syntax error\n'
111 error_message += 'Line #%s, character %s: "%s"' % (
112 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
113 except:
114 # Something went wrong, re-raise the original exception
115 raise e
116 else:
117 raise Error(error_message)
118
119
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000120class PrintableObject(object):
121 def __str__(self):
122 output = ''
123 for i in dir(self):
124 if i.startswith('__'):
125 continue
126 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
127 return output
128
129
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000130def FileRead(filename, mode='rU'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000131 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +0000132 # codecs.open() has different behavior than open() on python 2.6 so use
133 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000134 s = f.read()
135 try:
136 return s.decode('utf-8')
137 except UnicodeDecodeError:
138 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000139
140
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000141def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000142 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000143 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000144
145
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000146def safe_rename(old, new):
147 """Renames a file reliably.
148
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000149 Sometimes os.rename does not work because a dying git process keeps a handle
150 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000151 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000152 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000153 """
154 # roughly 10s
155 retries = 100
156 for i in range(retries):
157 try:
158 os.rename(old, new)
159 break
160 except OSError:
161 if i == (retries - 1):
162 # Give up.
163 raise
164 # retry
165 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
166 time.sleep(0.1)
167
168
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000169def rm_file_or_tree(path):
170 if os.path.isfile(path):
171 os.remove(path)
172 else:
173 rmtree(path)
174
175
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000176def rmtree(path):
177 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000178
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000179 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000180
181 shutil.rmtree() doesn't work on Windows if any of the files or directories
182 are read-only, which svn repositories and some .svn files are. We need to
183 be able to force the files to be writable (i.e., deletable) as we traverse
184 the tree.
185
186 Even with all this, Windows still sometimes fails to delete a file, citing
187 a permission error (maybe something to do with antivirus scans or disk
188 indexing). The best suggestion any of the user forums had was to wait a
189 bit and try again, so we do that too. It's hand-waving, but sometimes it
190 works. :/
191
192 On POSIX systems, things are a little bit simpler. The modes of the files
193 to be deleted doesn't matter, only the modes of the directories containing
194 them are significant. As the directory tree is traversed, each directory
195 has its mode set appropriately before descending into it. This should
196 result in the entire tree being removed, with the possible exception of
197 *path itself, because nothing attempts to change the mode of its parent.
198 Doing so would be hazardous, as it's not a directory slated for removal.
199 In the ordinary case, this is not a problem: for our purposes, the user
200 will never lack write permission on *path's parent.
201 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000202 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000203 return
204
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000205 if os.path.islink(path) or not os.path.isdir(path):
206 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000207
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000208 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000209 # Give up and use cmd.exe's rd command.
210 path = os.path.normcase(path)
211 for _ in xrange(3):
212 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
213 if exitcode == 0:
214 return
215 else:
216 print >> sys.stderr, 'rd exited with code %d' % exitcode
217 time.sleep(3)
218 raise Exception('Failed to remove path %s' % path)
219
220 # On POSIX systems, we need the x-bit set on the directory to access it,
221 # the r-bit to see its contents, and the w-bit to remove files from it.
222 # The actual modes of the files within the directory is irrelevant.
223 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000224
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000225 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000226 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000227
228 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000229 # If fullpath is a symbolic link that points to a directory, isdir will
230 # be True, but we don't want to descend into that as a directory, we just
231 # want to remove the link. Check islink and treat links as ordinary files
232 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000233 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000234 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000235 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000236 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000237 # Recurse.
238 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000239
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000240 remove(os.rmdir, path)
241
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000242
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000243def safe_makedirs(tree):
244 """Creates the directory in a safe manner.
245
246 Because multiple threads can create these directories concurently, trap the
247 exception and pass on.
248 """
249 count = 0
250 while not os.path.exists(tree):
251 count += 1
252 try:
253 os.makedirs(tree)
254 except OSError, e:
255 # 17 POSIX, 183 Windows
256 if e.errno not in (17, 183):
257 raise
258 if count > 40:
259 # Give up.
260 raise
261
262
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000263def CommandToStr(args):
264 """Converts an arg list into a shell escaped string."""
265 return ' '.join(pipes.quote(arg) for arg in args)
266
267
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000268def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000269 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000270
maruel@chromium.org17d01792010-09-01 18:07:10 +0000271 If |always| is True, a message indicating what is being done
272 is printed to stdout all the time even if not output is generated. Otherwise
273 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000274 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000275 stdout = kwargs.setdefault('stdout', sys.stdout)
276 if header is None:
277 header = "\n________ running '%s' in '%s'\n" % (
ilevy@chromium.org4aad1852013-07-12 21:32:51 +0000278 ' '.join(args), kwargs.get('cwd', '.'))
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000279
maruel@chromium.org17d01792010-09-01 18:07:10 +0000280 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000281 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000282 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000283 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000284 def filter_msg(line):
285 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000286 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000287 elif filter_fn:
288 filter_fn(line)
289 kwargs['filter_fn'] = filter_msg
290 kwargs['call_filter_on_first_line'] = True
291 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000292 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000293 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000294
maruel@chromium.org17d01792010-09-01 18:07:10 +0000295
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000296class Wrapper(object):
297 """Wraps an object, acting as a transparent proxy for all properties by
298 default.
299 """
300 def __init__(self, wrapped):
301 self._wrapped = wrapped
302
303 def __getattr__(self, name):
304 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000305
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000306
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000307class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000308 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000309 def __init__(self, wrapped, delay):
310 super(AutoFlush, self).__init__(wrapped)
311 if not hasattr(self, 'lock'):
312 self.lock = threading.Lock()
313 self.__last_flushed_at = time.time()
314 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000315
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000316 @property
317 def autoflush(self):
318 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000319
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000320 def write(self, out, *args, **kwargs):
321 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000322 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000323 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000324 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000325 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000326 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000327 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000328 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000329 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000330 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000331 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000332
333
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000334class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000335 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000336 threads with a NN> prefix.
337 """
338 def __init__(self, wrapped, include_zero=False):
339 super(Annotated, self).__init__(wrapped)
340 if not hasattr(self, 'lock'):
341 self.lock = threading.Lock()
342 self.__output_buffers = {}
343 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000344
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000345 @property
346 def annotated(self):
347 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000348
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000349 def write(self, out):
350 index = getattr(threading.currentThread(), 'index', 0)
351 if not index and not self.__include_zero:
352 # Unindexed threads aren't buffered.
353 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000354
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000355 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000356 try:
357 # Use a dummy array to hold the string so the code can be lockless.
358 # Strings are immutable, requiring to keep a lock for the whole dictionary
359 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000360 if not index in self.__output_buffers:
361 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000362 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000363 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000364 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000365 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000366
367 # Continue lockless.
368 obj[0] += out
369 while '\n' in obj[0]:
370 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000371 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000372 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000373 obj[0] = remaining
374
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000375 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000376 """Flush buffered output."""
377 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000378 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000379 try:
380 # Detect threads no longer existing.
381 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000382 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000383 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000384 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000385 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000386 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000387 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000388 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000389 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000390
391 # Don't keep the lock while writting. Will append \n when it shouldn't.
392 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000393 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000394 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
395 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000396
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000397
398def MakeFileAutoFlush(fileobj, delay=10):
399 autoflush = getattr(fileobj, 'autoflush', None)
400 if autoflush:
401 autoflush.delay = delay
402 return fileobj
403 return AutoFlush(fileobj, delay)
404
405
406def MakeFileAnnotated(fileobj, include_zero=False):
407 if getattr(fileobj, 'annotated', None):
408 return fileobj
409 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000410
411
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000412GCLIENT_CHILDREN = []
413GCLIENT_CHILDREN_LOCK = threading.Lock()
414
415
416class GClientChildren(object):
417 @staticmethod
418 def add(popen_obj):
419 with GCLIENT_CHILDREN_LOCK:
420 GCLIENT_CHILDREN.append(popen_obj)
421
422 @staticmethod
423 def remove(popen_obj):
424 with GCLIENT_CHILDREN_LOCK:
425 GCLIENT_CHILDREN.remove(popen_obj)
426
427 @staticmethod
428 def _attemptToKillChildren():
429 global GCLIENT_CHILDREN
430 with GCLIENT_CHILDREN_LOCK:
431 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
432
433 for zombie in zombies:
434 try:
435 zombie.kill()
436 except OSError:
437 pass
438
439 with GCLIENT_CHILDREN_LOCK:
440 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
441
442 @staticmethod
443 def _areZombies():
444 with GCLIENT_CHILDREN_LOCK:
445 return bool(GCLIENT_CHILDREN)
446
447 @staticmethod
448 def KillAllRemainingChildren():
449 GClientChildren._attemptToKillChildren()
450
451 if GClientChildren._areZombies():
452 time.sleep(0.5)
453 GClientChildren._attemptToKillChildren()
454
455 with GCLIENT_CHILDREN_LOCK:
456 if GCLIENT_CHILDREN:
457 print >> sys.stderr, 'Could not kill the following subprocesses:'
458 for zombie in GCLIENT_CHILDREN:
459 print >> sys.stderr, ' ', zombie.pid
460
461
maruel@chromium.org17d01792010-09-01 18:07:10 +0000462def CheckCallAndFilter(args, stdout=None, filter_fn=None,
463 print_stdout=None, call_filter_on_first_line=False,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000464 retry=False, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000465 """Runs a command and calls back a filter function if needed.
466
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000467 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000468 print_stdout: If True, the command's stdout is forwarded to stdout.
469 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000470 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000471 character trimmed.
472 stdout: Can be any bufferable output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000473 retry: If the process exits non-zero, sleep for a brief interval and try
474 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000475
476 stderr is always redirected to stdout.
477 """
478 assert print_stdout or filter_fn
479 stdout = stdout or sys.stdout
hinoka@google.com267f33e2014-02-28 22:02:32 +0000480 output = cStringIO.StringIO()
maruel@chromium.org17d01792010-09-01 18:07:10 +0000481 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000482
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000483 sleep_interval = RETRY_INITIAL_SLEEP
484 run_cwd = kwargs.get('cwd', os.getcwd())
485 for _ in xrange(RETRY_MAX + 1):
486 kid = subprocess2.Popen(
487 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
488 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000489
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000490 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000491
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000492 # Do a flush of stdout before we begin reading from the subprocess2's stdout
493 stdout.flush()
494
495 # Also, we need to forward stdout to prevent weird re-ordering of output.
496 # This has to be done on a per byte basis to make sure it is not buffered:
497 # normally buffering is done for each line, but if svn requests input, no
498 # end-of-line character is output after the prompt and it would not show up.
499 try:
500 in_byte = kid.stdout.read(1)
501 if in_byte:
502 if call_filter_on_first_line:
503 filter_fn(None)
504 in_line = ''
505 while in_byte:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000506 output.write(in_byte)
507 if print_stdout:
508 stdout.write(in_byte)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000509 if in_byte not in ['\r', '\n']:
510 in_line += in_byte
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000511 else:
512 filter_fn(in_line)
513 in_line = ''
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000514 in_byte = kid.stdout.read(1)
515 # Flush the rest of buffered output. This is only an issue with
516 # stdout/stderr not ending with a \n.
517 if len(in_line):
szager@google.com85d3e3a2011-10-07 17:12:00 +0000518 filter_fn(in_line)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000519 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000520
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000521 # Don't put this in a 'finally,' since the child may still run if we get
522 # an exception.
523 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000524
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000525 except KeyboardInterrupt:
526 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
527 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000528
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000529 if rv == 0:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000530 return output.getvalue()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000531 if not retry:
532 break
533 print ("WARNING: subprocess '%s' in %s failed; will retry after a short "
534 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000535 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000536 sleep_interval *= 2
537 raise subprocess2.CalledProcessError(
538 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000539
540
agable@chromium.org5a306a22014-02-24 22:13:59 +0000541class GitFilter(object):
542 """A filter_fn implementation for quieting down git output messages.
543
544 Allows a custom function to skip certain lines (predicate), and will throttle
545 the output of percentage completed lines to only output every X seconds.
546 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000547 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000548
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000549 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000550 """
551 Args:
552 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
553 XX% complete messages) to only be printed at least |time_throttle|
554 seconds apart.
555 predicate (f(line)): An optional function which is invoked for every line.
556 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000557 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000558 """
559 self.last_time = 0
560 self.time_throttle = time_throttle
561 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000562 self.out_fh = out_fh or sys.stdout
563 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000564
565 def __call__(self, line):
566 # git uses an escape sequence to clear the line; elide it.
567 esc = line.find(unichr(033))
568 if esc > -1:
569 line = line[:esc]
570 if self.predicate and not self.predicate(line):
571 return
572 now = time.time()
573 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000574 if match:
575 if match.group(1) != self.progress_prefix:
576 self.progress_prefix = match.group(1)
577 elif now - self.last_time < self.time_throttle:
578 return
579 self.last_time = now
580 self.out_fh.write('[%s] ' % Elapsed())
581 print >> self.out_fh, line
agable@chromium.org5a306a22014-02-24 22:13:59 +0000582
583
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000584def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000585 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000586 real_from_dir = os.path.realpath(from_dir)
587 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000588 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000589 split_path = os.path.split(path)
590 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000591 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000592 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000593
594 # If we did not find the file in the current directory, make sure we are in a
595 # sub directory that is controlled by this configuration.
596 if path != real_from_dir:
597 entries_filename = os.path.join(path, filename + '_entries')
598 if not os.path.exists(entries_filename):
599 # If .gclient_entries does not exist, a previous call to gclient sync
600 # might have failed. In that case, we cannot verify that the .gclient
601 # is the one we want to use. In order to not to cause too much trouble,
602 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000603 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000604 "file you want to use" % (filename, path))
605 return path
606 scope = {}
607 try:
608 exec(FileRead(entries_filename), scope)
609 except SyntaxError, e:
610 SyntaxErrorToError(filename, e)
611 all_directories = scope['entries'].keys()
612 path_to_check = real_from_dir[len(path)+1:]
613 while path_to_check:
614 if path_to_check in all_directories:
615 return path
616 path_to_check = os.path.dirname(path_to_check)
617 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000618
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000619 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000620 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000621
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000622
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000623def PathDifference(root, subpath):
624 """Returns the difference subpath minus root."""
625 root = os.path.realpath(root)
626 subpath = os.path.realpath(subpath)
627 if not subpath.startswith(root):
628 return None
629 # If the root does not have a trailing \ or /, we add it so the returned
630 # path starts immediately after the seperator regardless of whether it is
631 # provided.
632 root = os.path.join(root, '')
633 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000634
635
636def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000637 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000638
rcui@google.com13595ff2011-10-13 01:25:07 +0000639 Returns nearest upper-level directory with the passed in file.
640 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000641 if not path:
642 path = os.getcwd()
643 path = os.path.realpath(path)
644 while True:
645 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000646 if os.path.exists(file_path):
647 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000648 (new_path, _) = os.path.split(path)
649 if new_path == path:
650 return None
651 path = new_path
652
653
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000654def GetMacWinOrLinux():
655 """Returns 'mac', 'win', or 'linux', matching the current platform."""
656 if sys.platform.startswith(('cygwin', 'win')):
657 return 'win'
658 elif sys.platform.startswith('linux'):
659 return 'linux'
660 elif sys.platform == 'darwin':
661 return 'mac'
662 raise Error('Unknown platform: ' + sys.platform)
663
664
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000665def GetBuildtoolsPath():
666 """Returns the full path to the buildtools directory.
667 This is based on the root of the checkout containing the current directory."""
zturner@chromium.org0db9a142014-08-13 23:15:25 +0000668
669 # Overriding the build tools path by environment is highly unsupported and may
670 # break without warning. Do not rely on this for anything important.
671 override = os.environ.get('CHROMIUM_BUILDTOOLS_PATH')
672 if override is not None:
673 return override
674
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000675 gclient_root = FindGclientRoot(os.getcwd())
676 if not gclient_root:
jochen@chromium.orgaaee92f2014-07-02 07:35:31 +0000677 # Some projects might not use .gclient. Try to see whether we're in a git
678 # checkout.
679 top_dir = [os.getcwd()]
680 def filter_fn(line):
681 top_dir[0] = os.path.normpath(line.rstrip('\n'))
682 try:
683 CheckCallAndFilter(["git", "rev-parse", "--show-toplevel"],
684 print_stdout=False, filter_fn=filter_fn)
685 except Exception:
686 pass
687 top_dir = top_dir[0]
688 if os.path.exists(os.path.join(top_dir, 'buildtools')):
689 return os.path.join(top_dir, 'buildtools')
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000690 return None
kjellander@chromium.orgf7facfa2014-09-05 12:40:28 +0000691
692 # Some projects' top directory is not named 'src'.
693 source_dir_name = GetGClientPrimarySolutionName(gclient_root) or 'src'
ncbray@chromium.org43e91582014-11-12 22:38:51 +0000694 buildtools_path = os.path.join(gclient_root, source_dir_name, 'buildtools')
695 if not os.path.exists(buildtools_path):
696 # Buildtools may be in the gclient root.
697 buildtools_path = os.path.join(gclient_root, 'buildtools')
698 return buildtools_path
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000699
700
701def GetBuildtoolsPlatformBinaryPath():
702 """Returns the full path to the binary directory for the current platform."""
703 # Mac and Windows just have one directory, Linux has two according to whether
704 # it's 32 or 64 bits.
705 buildtools_path = GetBuildtoolsPath()
706 if not buildtools_path:
707 return None
708
709 if sys.platform.startswith(('cygwin', 'win')):
710 subdir = 'win'
711 elif sys.platform == 'darwin':
712 subdir = 'mac'
713 elif sys.platform.startswith('linux'):
714 if sys.maxsize > 2**32:
715 subdir = 'linux64'
716 else:
717 subdir = 'linux32'
718 else:
719 raise Error('Unknown platform: ' + sys.platform)
720 return os.path.join(buildtools_path, subdir)
721
722
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000723def GetExeSuffix():
724 """Returns '' or '.exe' depending on how executables work on this platform."""
725 if sys.platform.startswith(('cygwin', 'win')):
726 return '.exe'
727 return ''
728
729
kjellander@chromium.orgf7facfa2014-09-05 12:40:28 +0000730def GetGClientPrimarySolutionName(gclient_root_dir_path):
731 """Returns the name of the primary solution in the .gclient file specified."""
732 gclient_config_file = os.path.join(gclient_root_dir_path, '.gclient')
733 env = {}
734 execfile(gclient_config_file, env)
735 solutions = env.get('solutions', [])
736 if solutions:
737 return solutions[0].get('name')
738 return None
739
740
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000741def GetGClientRootAndEntries(path=None):
742 """Returns the gclient root and the dict of entries."""
743 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000744 root = FindFileUpwards(config_file, path)
745 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000746 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000747 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000748 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000749 env = {}
750 execfile(config_path, env)
751 config_dir = os.path.dirname(config_path)
752 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000753
754
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000755def lockedmethod(method):
756 """Method decorator that holds self.lock for the duration of the call."""
757 def inner(self, *args, **kwargs):
758 try:
759 try:
760 self.lock.acquire()
761 except KeyboardInterrupt:
762 print >> sys.stderr, 'Was deadlocked'
763 raise
764 return method(self, *args, **kwargs)
765 finally:
766 self.lock.release()
767 return inner
768
769
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000770class WorkItem(object):
771 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000772 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
773 # As a workaround, use a single lock. Yep you read it right. Single lock for
774 # all the 100 objects.
775 lock = threading.Lock()
776
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000777 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000778 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000779 self._name = name
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000780 self.outbuf = cStringIO.StringIO()
781 self.start = self.finish = None
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000782
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000783 def run(self, work_queue):
784 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000785 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000786 pass
787
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000788 @property
789 def name(self):
790 return self._name
791
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000792
793class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000794 """Runs a set of WorkItem that have interdependencies and were WorkItem are
795 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000796
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000797 In gclient's case, Dependencies sometime needs to be run out of order due to
798 From() keyword. This class manages that all the required dependencies are run
799 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000800
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000801 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000802 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000803 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000804 """jobs specifies the number of concurrent tasks to allow. progress is a
805 Progress instance."""
806 # Set when a thread is done or a new item is enqueued.
807 self.ready_cond = threading.Condition()
808 # Maximum number of concurrent tasks.
809 self.jobs = jobs
810 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000811 self.queued = []
812 # List of strings representing each Dependency.name that was run.
813 self.ran = []
814 # List of items currently running.
815 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000816 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000817 self.exceptions = Queue.Queue()
818 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000819 self.progress = progress
820 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000821 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000822
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000823 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000824 self.verbose = verbose
825 self.last_join = None
826 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000827
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000828 def enqueue(self, d):
829 """Enqueue one Dependency to be executed later once its requirements are
830 satisfied.
831 """
832 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000833 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000834 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000835 self.queued.append(d)
836 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000837 if self.jobs == 1:
838 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000839 logging.debug('enqueued(%s)' % d.name)
840 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000841 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000842 self.progress.update(0)
843 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000844 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000845 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000846
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000847 def out_cb(self, _):
848 self.last_subproc_output = datetime.datetime.now()
849 return True
850
851 @staticmethod
852 def format_task_output(task, comment=''):
853 if comment:
854 comment = ' (%s)' % comment
855 if task.start and task.finish:
856 elapsed = ' (Elapsed: %s)' % (
857 str(task.finish - task.start).partition('.')[0])
858 else:
859 elapsed = ''
860 return """
861%s%s%s
862----------------------------------------
863%s
864----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000865 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000866
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000867 def flush(self, *args, **kwargs):
868 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000869 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000870 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000871 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000872 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000873 while True:
874 # Check for task to run first, then wait.
875 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000876 if not self.exceptions.empty():
877 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000878 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000879 self._flush_terminated_threads()
880 if (not self.queued and not self.running or
881 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000882 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000883 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000884
885 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000886 for i in xrange(len(self.queued)):
887 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000888 if (self.ignore_requirements or
889 not (set(self.queued[i].requirements) - set(self.ran))):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000890 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000891 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000892 break
893 else:
894 # Couldn't find an item that could run. Break out the outher loop.
895 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000896
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000897 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000898 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000899 break
900 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000901 try:
902 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000903 # If we haven't printed to terminal for a while, but we have received
904 # spew from a suprocess, let the user know we're still progressing.
905 now = datetime.datetime.now()
906 if (now - self.last_join > datetime.timedelta(seconds=60) and
907 self.last_subproc_output > self.last_join):
908 if self.progress:
909 print >> sys.stdout, ''
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000910 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000911 elapsed = Elapsed()
912 print >> sys.stdout, '[%s] Still working on:' % elapsed
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000913 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000914 for task in self.running:
915 print >> sys.stdout, '[%s] %s' % (elapsed, task.item.name)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000916 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000917 except KeyboardInterrupt:
918 # Help debugging by printing some information:
919 print >> sys.stderr, (
920 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
921 'Running: %d') % (
922 self.jobs,
923 len(self.queued),
924 ', '.join(self.ran),
925 len(self.running)))
926 for i in self.queued:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000927 print >> sys.stderr, '%s (not started): %s' % (
928 i.name, ', '.join(i.requirements))
929 for i in self.running:
930 print >> sys.stderr, self.format_task_output(i.item, 'interrupted')
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000931 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000932 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000933 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000934 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000935
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000936 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000937 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000938 if self.progress:
939 print >> sys.stdout, ''
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000940 # To get back the stack location correctly, the raise a, b, c form must be
941 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000942 e, task = self.exceptions.get()
943 print >> sys.stderr, self.format_task_output(task.item, 'ERROR')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000944 raise e[0], e[1], e[2]
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000945 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000946 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000947
maruel@chromium.org3742c842010-09-09 19:27:14 +0000948 def _flush_terminated_threads(self):
949 """Flush threads that have terminated."""
950 running = self.running
951 self.running = []
952 for t in running:
953 if t.isAlive():
954 self.running.append(t)
955 else:
956 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000957 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000958 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000959 if self.verbose:
960 print >> sys.stdout, self.format_task_output(t.item)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000961 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000962 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000963 if t.item.name in self.ran:
964 raise Error(
965 'gclient is confused, "%s" is already in "%s"' % (
966 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000967 if not t.item.name in self.ran:
968 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000969
970 def _run_one_task(self, task_item, args, kwargs):
971 if self.jobs > 1:
972 # Start the thread.
973 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000974 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000975 self.running.append(new_thread)
976 new_thread.start()
977 else:
978 # Run the 'thread' inside the main thread. Don't try to catch any
979 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000980 try:
981 task_item.start = datetime.datetime.now()
982 print >> task_item.outbuf, '[%s] Started.' % Elapsed(task_item.start)
983 task_item.run(*args, **kwargs)
984 task_item.finish = datetime.datetime.now()
985 print >> task_item.outbuf, '[%s] Finished.' % Elapsed(task_item.finish)
986 self.ran.append(task_item.name)
987 if self.verbose:
988 if self.progress:
989 print >> sys.stdout, ''
990 print >> sys.stdout, self.format_task_output(task_item)
991 if self.progress:
992 self.progress.update(1, ', '.join(t.item.name for t in self.running))
993 except KeyboardInterrupt:
994 print >> sys.stderr, self.format_task_output(task_item, 'interrupted')
995 raise
996 except Exception:
997 print >> sys.stderr, self.format_task_output(task_item, 'ERROR')
998 raise
999
maruel@chromium.org3742c842010-09-09 19:27:14 +00001000
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001001 class _Worker(threading.Thread):
1002 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001003 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001004 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001005 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001006 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001007 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001008 self.args = args
1009 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001010 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001011
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001012 def run(self):
1013 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001014 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001015 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001016 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001017 self.item.start = datetime.datetime.now()
1018 print >> self.item.outbuf, '[%s] Started.' % Elapsed(self.item.start)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001019 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001020 self.item.finish = datetime.datetime.now()
1021 print >> self.item.outbuf, '[%s] Finished.' % Elapsed(self.item.finish)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001022 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001023 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001024 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001025 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001026 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001027 except Exception:
1028 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001029 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001030 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001031 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001032 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001033 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001034 work_queue.ready_cond.acquire()
1035 try:
1036 work_queue.ready_cond.notifyAll()
1037 finally:
1038 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001039
1040
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001041def GetEditor(git, git_editor=None):
1042 """Returns the most plausible editor to use.
1043
1044 In order of preference:
1045 - GIT_EDITOR/SVN_EDITOR environment variable
1046 - core.editor git configuration variable (if supplied by git-cl)
1047 - VISUAL environment variable
1048 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001049 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001050
1051 In the case of git-cl, this matches git's behaviour, except that it does not
1052 include dumb terminal detection.
1053
1054 In the case of gcl, this matches svn's behaviour, except that it does not
1055 accept a command-line flag or check the editor-cmd configuration variable.
1056 """
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001057 if git:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001058 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001059 else:
1060 editor = os.environ.get('SVN_EDITOR')
1061 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001062 editor = os.environ.get('VISUAL')
1063 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001064 editor = os.environ.get('EDITOR')
1065 if not editor:
1066 if sys.platform.startswith('win'):
1067 editor = 'notepad'
1068 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001069 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001070 return editor
1071
1072
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001073def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001074 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001075 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001076 # Make sure CRLF is handled properly by requiring none.
1077 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +00001078 print >> sys.stderr, (
1079 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001080 fileobj = os.fdopen(file_handle, 'w')
1081 # Still remove \r if present.
1082 fileobj.write(re.sub('\r?\n', '\n', content))
1083 fileobj.close()
1084
1085 try:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001086 editor = GetEditor(git, git_editor=git_editor)
1087 if not editor:
1088 return None
1089 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001090 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1091 # Msysgit requires the usage of 'env' to be present.
1092 cmd = 'env ' + cmd
1093 try:
1094 # shell=True to allow the shell to handle all forms of quotes in
1095 # $EDITOR.
1096 subprocess2.check_call(cmd, shell=True)
1097 except subprocess2.CalledProcessError:
1098 return None
1099 return FileRead(filename)
1100 finally:
1101 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001102
1103
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001104def UpgradeToHttps(url):
1105 """Upgrades random urls to https://.
1106
1107 Do not touch unknown urls like ssh:// or git://.
1108 Do not touch http:// urls with a port number,
1109 Fixes invalid GAE url.
1110 """
1111 if not url:
1112 return url
1113 if not re.match(r'[a-z\-]+\://.*', url):
1114 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1115 # relative url and will use http:///foo. Note that it defaults to http://
1116 # for compatibility with naked url like "localhost:8080".
1117 url = 'http://%s' % url
1118 parsed = list(urlparse.urlparse(url))
1119 # Do not automatically upgrade http to https if a port number is provided.
1120 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1121 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001122 return urlparse.urlunparse(parsed)
1123
1124
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001125def ParseCodereviewSettingsContent(content):
1126 """Process a codereview.settings file properly."""
1127 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1128 try:
1129 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1130 except ValueError:
1131 raise Error(
1132 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001133 def fix_url(key):
1134 if keyvals.get(key):
1135 keyvals[key] = UpgradeToHttps(keyvals[key])
1136 fix_url('CODE_REVIEW_SERVER')
1137 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001138 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001139
1140
1141def NumLocalCpus():
1142 """Returns the number of processors.
1143
dnj@chromium.org530523b2015-01-07 19:54:57 +00001144 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1145 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1146 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001147 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001148 # Surround the entire thing in try/except; no failure here should stop gclient
1149 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001150 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001151 # Use multiprocessing to get CPU count. This may raise
1152 # NotImplementedError.
1153 try:
1154 import multiprocessing
1155 return multiprocessing.cpu_count()
1156 except NotImplementedError: # pylint: disable=W0702
1157 # (UNIX) Query 'os.sysconf'.
1158 # pylint: disable=E1101
1159 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1160 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1161
1162 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1163 if 'NUMBER_OF_PROCESSORS' in os.environ:
1164 return int(os.environ['NUMBER_OF_PROCESSORS'])
1165 except Exception as e:
1166 logging.exception("Exception raised while probing CPU count: %s", e)
1167
1168 logging.debug('Failed to get CPU count. Defaulting to 1.')
1169 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001170
1171def DefaultDeltaBaseCacheLimit():
1172 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1173
1174 The primary constraint is the address space of virtual memory. The cache
1175 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1176 parameter is set too high.
1177 """
1178 if platform.architecture()[0].startswith('64'):
1179 return '2g'
1180 else:
1181 return '512m'
1182
szager@chromium.orgff113292014-03-25 06:02:08 +00001183def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001184 """Return reasonable default values for configuring git-index-pack.
1185
1186 Experiments suggest that higher values for pack.threads don't improve
1187 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001188 cache_limit = DefaultDeltaBaseCacheLimit()
1189 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1190 if url in THREADED_INDEX_PACK_BLACKLIST:
1191 result.extend(['-c', 'pack.threads=1'])
1192 return result