blob: f80cd78e4a52e6ebb4abe5ab2fa35203191a9ce3 [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
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000665def GetPrimarySolutionPath():
666 """Returns the full path to the primary solution. (gclient_root + src)"""
zturner@chromium.org0db9a142014-08-13 23:15:25 +0000667
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000668 gclient_root = FindGclientRoot(os.getcwd())
669 if not gclient_root:
jochen@chromium.orgaaee92f2014-07-02 07:35:31 +0000670 # Some projects might not use .gclient. Try to see whether we're in a git
671 # checkout.
672 top_dir = [os.getcwd()]
673 def filter_fn(line):
674 top_dir[0] = os.path.normpath(line.rstrip('\n'))
675 try:
676 CheckCallAndFilter(["git", "rev-parse", "--show-toplevel"],
677 print_stdout=False, filter_fn=filter_fn)
678 except Exception:
679 pass
680 top_dir = top_dir[0]
681 if os.path.exists(os.path.join(top_dir, 'buildtools')):
682 return os.path.join(top_dir, 'buildtools')
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000683 return None
kjellander@chromium.orgf7facfa2014-09-05 12:40:28 +0000684
685 # Some projects' top directory is not named 'src'.
686 source_dir_name = GetGClientPrimarySolutionName(gclient_root) or 'src'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000687 return os.path.join(gclient_root, source_dir_name)
688
689
690def GetBuildtoolsPath():
691 """Returns the full path to the buildtools directory.
692 This is based on the root of the checkout containing the current directory."""
693
694 # Overriding the build tools path by environment is highly unsupported and may
695 # break without warning. Do not rely on this for anything important.
696 override = os.environ.get('CHROMIUM_BUILDTOOLS_PATH')
697 if override is not None:
698 return override
699
700 primary_solution = GetPrimarySolutionPath()
701 buildtools_path = os.path.join(primary_solution, 'buildtools')
ncbray@chromium.org43e91582014-11-12 22:38:51 +0000702 if not os.path.exists(buildtools_path):
703 # Buildtools may be in the gclient root.
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +0000704 gclient_root = FindGclientRoot(os.getcwd())
ncbray@chromium.org43e91582014-11-12 22:38:51 +0000705 buildtools_path = os.path.join(gclient_root, 'buildtools')
706 return buildtools_path
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000707
708
709def GetBuildtoolsPlatformBinaryPath():
710 """Returns the full path to the binary directory for the current platform."""
711 # Mac and Windows just have one directory, Linux has two according to whether
712 # it's 32 or 64 bits.
713 buildtools_path = GetBuildtoolsPath()
714 if not buildtools_path:
715 return None
716
717 if sys.platform.startswith(('cygwin', 'win')):
718 subdir = 'win'
719 elif sys.platform == 'darwin':
720 subdir = 'mac'
721 elif sys.platform.startswith('linux'):
722 if sys.maxsize > 2**32:
723 subdir = 'linux64'
724 else:
725 subdir = 'linux32'
726 else:
727 raise Error('Unknown platform: ' + sys.platform)
728 return os.path.join(buildtools_path, subdir)
729
730
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000731def GetExeSuffix():
732 """Returns '' or '.exe' depending on how executables work on this platform."""
733 if sys.platform.startswith(('cygwin', 'win')):
734 return '.exe'
735 return ''
736
737
kjellander@chromium.orgf7facfa2014-09-05 12:40:28 +0000738def GetGClientPrimarySolutionName(gclient_root_dir_path):
739 """Returns the name of the primary solution in the .gclient file specified."""
740 gclient_config_file = os.path.join(gclient_root_dir_path, '.gclient')
741 env = {}
742 execfile(gclient_config_file, env)
743 solutions = env.get('solutions', [])
744 if solutions:
745 return solutions[0].get('name')
746 return None
747
748
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000749def GetGClientRootAndEntries(path=None):
750 """Returns the gclient root and the dict of entries."""
751 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000752 root = FindFileUpwards(config_file, path)
753 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000754 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000755 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000756 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000757 env = {}
758 execfile(config_path, env)
759 config_dir = os.path.dirname(config_path)
760 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000761
762
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000763def lockedmethod(method):
764 """Method decorator that holds self.lock for the duration of the call."""
765 def inner(self, *args, **kwargs):
766 try:
767 try:
768 self.lock.acquire()
769 except KeyboardInterrupt:
770 print >> sys.stderr, 'Was deadlocked'
771 raise
772 return method(self, *args, **kwargs)
773 finally:
774 self.lock.release()
775 return inner
776
777
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000778class WorkItem(object):
779 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000780 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
781 # As a workaround, use a single lock. Yep you read it right. Single lock for
782 # all the 100 objects.
783 lock = threading.Lock()
784
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000785 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000786 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000787 self._name = name
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000788 self.outbuf = cStringIO.StringIO()
789 self.start = self.finish = None
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000790
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000791 def run(self, work_queue):
792 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000793 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000794 pass
795
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000796 @property
797 def name(self):
798 return self._name
799
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000800
801class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000802 """Runs a set of WorkItem that have interdependencies and were WorkItem are
803 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000804
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000805 In gclient's case, Dependencies sometime needs to be run out of order due to
806 From() keyword. This class manages that all the required dependencies are run
807 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000808
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000809 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000810 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000811 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000812 """jobs specifies the number of concurrent tasks to allow. progress is a
813 Progress instance."""
814 # Set when a thread is done or a new item is enqueued.
815 self.ready_cond = threading.Condition()
816 # Maximum number of concurrent tasks.
817 self.jobs = jobs
818 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000819 self.queued = []
820 # List of strings representing each Dependency.name that was run.
821 self.ran = []
822 # List of items currently running.
823 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000824 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000825 self.exceptions = Queue.Queue()
826 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000827 self.progress = progress
828 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000829 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000830
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000831 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000832 self.verbose = verbose
833 self.last_join = None
834 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000835
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000836 def enqueue(self, d):
837 """Enqueue one Dependency to be executed later once its requirements are
838 satisfied.
839 """
840 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000841 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000842 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000843 self.queued.append(d)
844 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000845 if self.jobs == 1:
846 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000847 logging.debug('enqueued(%s)' % d.name)
848 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000849 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000850 self.progress.update(0)
851 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000852 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000853 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000854
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000855 def out_cb(self, _):
856 self.last_subproc_output = datetime.datetime.now()
857 return True
858
859 @staticmethod
860 def format_task_output(task, comment=''):
861 if comment:
862 comment = ' (%s)' % comment
863 if task.start and task.finish:
864 elapsed = ' (Elapsed: %s)' % (
865 str(task.finish - task.start).partition('.')[0])
866 else:
867 elapsed = ''
868 return """
869%s%s%s
870----------------------------------------
871%s
872----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000873 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000874
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000875 def flush(self, *args, **kwargs):
876 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000877 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000878 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000879 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000880 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000881 while True:
882 # Check for task to run first, then wait.
883 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000884 if not self.exceptions.empty():
885 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000886 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000887 self._flush_terminated_threads()
888 if (not self.queued and not self.running or
889 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000890 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000891 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000892
893 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000894 for i in xrange(len(self.queued)):
895 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000896 if (self.ignore_requirements or
897 not (set(self.queued[i].requirements) - set(self.ran))):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000898 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000899 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000900 break
901 else:
902 # Couldn't find an item that could run. Break out the outher loop.
903 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000904
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000905 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000906 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000907 break
908 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000909 try:
910 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000911 # If we haven't printed to terminal for a while, but we have received
912 # spew from a suprocess, let the user know we're still progressing.
913 now = datetime.datetime.now()
914 if (now - self.last_join > datetime.timedelta(seconds=60) and
915 self.last_subproc_output > self.last_join):
916 if self.progress:
917 print >> sys.stdout, ''
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000918 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000919 elapsed = Elapsed()
920 print >> sys.stdout, '[%s] Still working on:' % elapsed
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000921 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000922 for task in self.running:
923 print >> sys.stdout, '[%s] %s' % (elapsed, task.item.name)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000924 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000925 except KeyboardInterrupt:
926 # Help debugging by printing some information:
927 print >> sys.stderr, (
928 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
929 'Running: %d') % (
930 self.jobs,
931 len(self.queued),
932 ', '.join(self.ran),
933 len(self.running)))
934 for i in self.queued:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000935 print >> sys.stderr, '%s (not started): %s' % (
936 i.name, ', '.join(i.requirements))
937 for i in self.running:
938 print >> sys.stderr, self.format_task_output(i.item, 'interrupted')
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000939 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000940 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000941 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000942 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000943
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000944 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000945 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000946 if self.progress:
947 print >> sys.stdout, ''
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000948 # To get back the stack location correctly, the raise a, b, c form must be
949 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000950 e, task = self.exceptions.get()
951 print >> sys.stderr, self.format_task_output(task.item, 'ERROR')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000952 raise e[0], e[1], e[2]
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000953 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000954 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000955
maruel@chromium.org3742c842010-09-09 19:27:14 +0000956 def _flush_terminated_threads(self):
957 """Flush threads that have terminated."""
958 running = self.running
959 self.running = []
960 for t in running:
961 if t.isAlive():
962 self.running.append(t)
963 else:
964 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000965 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000966 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000967 if self.verbose:
968 print >> sys.stdout, self.format_task_output(t.item)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000969 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000970 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000971 if t.item.name in self.ran:
972 raise Error(
973 'gclient is confused, "%s" is already in "%s"' % (
974 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000975 if not t.item.name in self.ran:
976 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000977
978 def _run_one_task(self, task_item, args, kwargs):
979 if self.jobs > 1:
980 # Start the thread.
981 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000982 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000983 self.running.append(new_thread)
984 new_thread.start()
985 else:
986 # Run the 'thread' inside the main thread. Don't try to catch any
987 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000988 try:
989 task_item.start = datetime.datetime.now()
990 print >> task_item.outbuf, '[%s] Started.' % Elapsed(task_item.start)
991 task_item.run(*args, **kwargs)
992 task_item.finish = datetime.datetime.now()
993 print >> task_item.outbuf, '[%s] Finished.' % Elapsed(task_item.finish)
994 self.ran.append(task_item.name)
995 if self.verbose:
996 if self.progress:
997 print >> sys.stdout, ''
998 print >> sys.stdout, self.format_task_output(task_item)
999 if self.progress:
1000 self.progress.update(1, ', '.join(t.item.name for t in self.running))
1001 except KeyboardInterrupt:
1002 print >> sys.stderr, self.format_task_output(task_item, 'interrupted')
1003 raise
1004 except Exception:
1005 print >> sys.stderr, self.format_task_output(task_item, 'ERROR')
1006 raise
1007
maruel@chromium.org3742c842010-09-09 19:27:14 +00001008
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001009 class _Worker(threading.Thread):
1010 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001011 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001012 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001013 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001014 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001015 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001016 self.args = args
1017 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001018 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001019
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001020 def run(self):
1021 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001022 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001023 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001024 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001025 self.item.start = datetime.datetime.now()
1026 print >> self.item.outbuf, '[%s] Started.' % Elapsed(self.item.start)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001027 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001028 self.item.finish = datetime.datetime.now()
1029 print >> self.item.outbuf, '[%s] Finished.' % Elapsed(self.item.finish)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001030 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001031 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001032 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001033 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001034 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001035 except Exception:
1036 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001037 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001038 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001039 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001040 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001041 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001042 work_queue.ready_cond.acquire()
1043 try:
1044 work_queue.ready_cond.notifyAll()
1045 finally:
1046 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001047
1048
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001049def GetEditor(git, git_editor=None):
1050 """Returns the most plausible editor to use.
1051
1052 In order of preference:
1053 - GIT_EDITOR/SVN_EDITOR environment variable
1054 - core.editor git configuration variable (if supplied by git-cl)
1055 - VISUAL environment variable
1056 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001057 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001058
1059 In the case of git-cl, this matches git's behaviour, except that it does not
1060 include dumb terminal detection.
1061
1062 In the case of gcl, this matches svn's behaviour, except that it does not
1063 accept a command-line flag or check the editor-cmd configuration variable.
1064 """
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001065 if git:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001066 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001067 else:
1068 editor = os.environ.get('SVN_EDITOR')
1069 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001070 editor = os.environ.get('VISUAL')
1071 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001072 editor = os.environ.get('EDITOR')
1073 if not editor:
1074 if sys.platform.startswith('win'):
1075 editor = 'notepad'
1076 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001077 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001078 return editor
1079
1080
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001081def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001082 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001083 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001084 # Make sure CRLF is handled properly by requiring none.
1085 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +00001086 print >> sys.stderr, (
1087 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001088 fileobj = os.fdopen(file_handle, 'w')
1089 # Still remove \r if present.
1090 fileobj.write(re.sub('\r?\n', '\n', content))
1091 fileobj.close()
1092
1093 try:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001094 editor = GetEditor(git, git_editor=git_editor)
1095 if not editor:
1096 return None
1097 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001098 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1099 # Msysgit requires the usage of 'env' to be present.
1100 cmd = 'env ' + cmd
1101 try:
1102 # shell=True to allow the shell to handle all forms of quotes in
1103 # $EDITOR.
1104 subprocess2.check_call(cmd, shell=True)
1105 except subprocess2.CalledProcessError:
1106 return None
1107 return FileRead(filename)
1108 finally:
1109 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001110
1111
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001112def UpgradeToHttps(url):
1113 """Upgrades random urls to https://.
1114
1115 Do not touch unknown urls like ssh:// or git://.
1116 Do not touch http:// urls with a port number,
1117 Fixes invalid GAE url.
1118 """
1119 if not url:
1120 return url
1121 if not re.match(r'[a-z\-]+\://.*', url):
1122 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1123 # relative url and will use http:///foo. Note that it defaults to http://
1124 # for compatibility with naked url like "localhost:8080".
1125 url = 'http://%s' % url
1126 parsed = list(urlparse.urlparse(url))
1127 # Do not automatically upgrade http to https if a port number is provided.
1128 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1129 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001130 return urlparse.urlunparse(parsed)
1131
1132
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001133def ParseCodereviewSettingsContent(content):
1134 """Process a codereview.settings file properly."""
1135 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1136 try:
1137 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1138 except ValueError:
1139 raise Error(
1140 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001141 def fix_url(key):
1142 if keyvals.get(key):
1143 keyvals[key] = UpgradeToHttps(keyvals[key])
1144 fix_url('CODE_REVIEW_SERVER')
1145 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001146 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001147
1148
1149def NumLocalCpus():
1150 """Returns the number of processors.
1151
dnj@chromium.org530523b2015-01-07 19:54:57 +00001152 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1153 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1154 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001155 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001156 # Surround the entire thing in try/except; no failure here should stop gclient
1157 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001158 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001159 # Use multiprocessing to get CPU count. This may raise
1160 # NotImplementedError.
1161 try:
1162 import multiprocessing
1163 return multiprocessing.cpu_count()
1164 except NotImplementedError: # pylint: disable=W0702
1165 # (UNIX) Query 'os.sysconf'.
1166 # pylint: disable=E1101
1167 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1168 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1169
1170 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1171 if 'NUMBER_OF_PROCESSORS' in os.environ:
1172 return int(os.environ['NUMBER_OF_PROCESSORS'])
1173 except Exception as e:
1174 logging.exception("Exception raised while probing CPU count: %s", e)
1175
1176 logging.debug('Failed to get CPU count. Defaulting to 1.')
1177 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001178
1179def DefaultDeltaBaseCacheLimit():
1180 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1181
1182 The primary constraint is the address space of virtual memory. The cache
1183 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1184 parameter is set too high.
1185 """
1186 if platform.architecture()[0].startswith('64'):
1187 return '2g'
1188 else:
1189 return '512m'
1190
szager@chromium.orgff113292014-03-25 06:02:08 +00001191def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001192 """Return reasonable default values for configuring git-index-pack.
1193
1194 Experiments suggest that higher values for pack.threads don't improve
1195 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001196 cache_limit = DefaultDeltaBaseCacheLimit()
1197 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1198 if url in THREADED_INDEX_PACK_BLACKLIST:
1199 result.extend(['-c', 'pack.threads=1'])
1200 return result