blob: fbcbc262b2b1abe490026d054e4216055892f254 [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')):
jiangj@opera.comd6d15b82015-04-20 06:43:48 +0000682 return top_dir
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."""
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000711 buildtools_path = GetBuildtoolsPath()
712 if not buildtools_path:
713 return None
714
715 if sys.platform.startswith(('cygwin', 'win')):
716 subdir = 'win'
717 elif sys.platform == 'darwin':
718 subdir = 'mac'
719 elif sys.platform.startswith('linux'):
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000720 subdir = 'linux64'
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000721 else:
722 raise Error('Unknown platform: ' + sys.platform)
723 return os.path.join(buildtools_path, subdir)
724
725
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000726def GetExeSuffix():
727 """Returns '' or '.exe' depending on how executables work on this platform."""
728 if sys.platform.startswith(('cygwin', 'win')):
729 return '.exe'
730 return ''
731
732
kjellander@chromium.orgf7facfa2014-09-05 12:40:28 +0000733def GetGClientPrimarySolutionName(gclient_root_dir_path):
734 """Returns the name of the primary solution in the .gclient file specified."""
735 gclient_config_file = os.path.join(gclient_root_dir_path, '.gclient')
736 env = {}
737 execfile(gclient_config_file, env)
738 solutions = env.get('solutions', [])
739 if solutions:
740 return solutions[0].get('name')
741 return None
742
743
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000744def GetGClientRootAndEntries(path=None):
745 """Returns the gclient root and the dict of entries."""
746 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000747 root = FindFileUpwards(config_file, path)
748 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000749 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000750 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000751 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000752 env = {}
753 execfile(config_path, env)
754 config_dir = os.path.dirname(config_path)
755 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000756
757
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000758def lockedmethod(method):
759 """Method decorator that holds self.lock for the duration of the call."""
760 def inner(self, *args, **kwargs):
761 try:
762 try:
763 self.lock.acquire()
764 except KeyboardInterrupt:
765 print >> sys.stderr, 'Was deadlocked'
766 raise
767 return method(self, *args, **kwargs)
768 finally:
769 self.lock.release()
770 return inner
771
772
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000773class WorkItem(object):
774 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000775 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
776 # As a workaround, use a single lock. Yep you read it right. Single lock for
777 # all the 100 objects.
778 lock = threading.Lock()
779
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000780 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000781 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000782 self._name = name
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000783 self.outbuf = cStringIO.StringIO()
784 self.start = self.finish = None
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000785
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000786 def run(self, work_queue):
787 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000788 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000789 pass
790
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000791 @property
792 def name(self):
793 return self._name
794
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000795
796class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000797 """Runs a set of WorkItem that have interdependencies and were WorkItem are
798 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000799
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000800 In gclient's case, Dependencies sometime needs to be run out of order due to
801 From() keyword. This class manages that all the required dependencies are run
802 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000803
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000804 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000805 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000806 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000807 """jobs specifies the number of concurrent tasks to allow. progress is a
808 Progress instance."""
809 # Set when a thread is done or a new item is enqueued.
810 self.ready_cond = threading.Condition()
811 # Maximum number of concurrent tasks.
812 self.jobs = jobs
813 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000814 self.queued = []
815 # List of strings representing each Dependency.name that was run.
816 self.ran = []
817 # List of items currently running.
818 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000819 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000820 self.exceptions = Queue.Queue()
821 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000822 self.progress = progress
823 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000824 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000825
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000826 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000827 self.verbose = verbose
828 self.last_join = None
829 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000830
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000831 def enqueue(self, d):
832 """Enqueue one Dependency to be executed later once its requirements are
833 satisfied.
834 """
835 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000836 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000837 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000838 self.queued.append(d)
839 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000840 if self.jobs == 1:
841 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000842 logging.debug('enqueued(%s)' % d.name)
843 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000844 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000845 self.progress.update(0)
846 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000847 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000848 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000849
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000850 def out_cb(self, _):
851 self.last_subproc_output = datetime.datetime.now()
852 return True
853
854 @staticmethod
855 def format_task_output(task, comment=''):
856 if comment:
857 comment = ' (%s)' % comment
858 if task.start and task.finish:
859 elapsed = ' (Elapsed: %s)' % (
860 str(task.finish - task.start).partition('.')[0])
861 else:
862 elapsed = ''
863 return """
864%s%s%s
865----------------------------------------
866%s
867----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000868 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000869
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000870 def flush(self, *args, **kwargs):
871 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000872 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000873 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000874 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000875 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000876 while True:
877 # Check for task to run first, then wait.
878 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000879 if not self.exceptions.empty():
880 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000881 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000882 self._flush_terminated_threads()
883 if (not self.queued and not self.running or
884 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000885 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000886 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000887
888 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000889 for i in xrange(len(self.queued)):
890 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000891 if (self.ignore_requirements or
892 not (set(self.queued[i].requirements) - set(self.ran))):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000893 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000894 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000895 break
896 else:
897 # Couldn't find an item that could run. Break out the outher loop.
898 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000899
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000900 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000901 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000902 break
903 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000904 try:
905 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000906 # If we haven't printed to terminal for a while, but we have received
907 # spew from a suprocess, let the user know we're still progressing.
908 now = datetime.datetime.now()
909 if (now - self.last_join > datetime.timedelta(seconds=60) and
910 self.last_subproc_output > self.last_join):
911 if self.progress:
912 print >> sys.stdout, ''
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000913 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000914 elapsed = Elapsed()
915 print >> sys.stdout, '[%s] Still working on:' % elapsed
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000916 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000917 for task in self.running:
918 print >> sys.stdout, '[%s] %s' % (elapsed, task.item.name)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000919 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000920 except KeyboardInterrupt:
921 # Help debugging by printing some information:
922 print >> sys.stderr, (
923 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
924 'Running: %d') % (
925 self.jobs,
926 len(self.queued),
927 ', '.join(self.ran),
928 len(self.running)))
929 for i in self.queued:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000930 print >> sys.stderr, '%s (not started): %s' % (
931 i.name, ', '.join(i.requirements))
932 for i in self.running:
933 print >> sys.stderr, self.format_task_output(i.item, 'interrupted')
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000934 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000935 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000936 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000937 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000938
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000939 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000940 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000941 if self.progress:
942 print >> sys.stdout, ''
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000943 # To get back the stack location correctly, the raise a, b, c form must be
944 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000945 e, task = self.exceptions.get()
946 print >> sys.stderr, self.format_task_output(task.item, 'ERROR')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000947 raise e[0], e[1], e[2]
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000948 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000949 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000950
maruel@chromium.org3742c842010-09-09 19:27:14 +0000951 def _flush_terminated_threads(self):
952 """Flush threads that have terminated."""
953 running = self.running
954 self.running = []
955 for t in running:
956 if t.isAlive():
957 self.running.append(t)
958 else:
959 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000960 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000961 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000962 if self.verbose:
963 print >> sys.stdout, self.format_task_output(t.item)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000964 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000965 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000966 if t.item.name in self.ran:
967 raise Error(
968 'gclient is confused, "%s" is already in "%s"' % (
969 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000970 if not t.item.name in self.ran:
971 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000972
973 def _run_one_task(self, task_item, args, kwargs):
974 if self.jobs > 1:
975 # Start the thread.
976 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000977 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000978 self.running.append(new_thread)
979 new_thread.start()
980 else:
981 # Run the 'thread' inside the main thread. Don't try to catch any
982 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000983 try:
984 task_item.start = datetime.datetime.now()
985 print >> task_item.outbuf, '[%s] Started.' % Elapsed(task_item.start)
986 task_item.run(*args, **kwargs)
987 task_item.finish = datetime.datetime.now()
988 print >> task_item.outbuf, '[%s] Finished.' % Elapsed(task_item.finish)
989 self.ran.append(task_item.name)
990 if self.verbose:
991 if self.progress:
992 print >> sys.stdout, ''
993 print >> sys.stdout, self.format_task_output(task_item)
994 if self.progress:
995 self.progress.update(1, ', '.join(t.item.name for t in self.running))
996 except KeyboardInterrupt:
997 print >> sys.stderr, self.format_task_output(task_item, 'interrupted')
998 raise
999 except Exception:
1000 print >> sys.stderr, self.format_task_output(task_item, 'ERROR')
1001 raise
1002
maruel@chromium.org3742c842010-09-09 19:27:14 +00001003
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001004 class _Worker(threading.Thread):
1005 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001006 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001007 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001008 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001009 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001010 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001011 self.args = args
1012 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001013 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001014
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001015 def run(self):
1016 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001017 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001018 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001019 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001020 self.item.start = datetime.datetime.now()
1021 print >> self.item.outbuf, '[%s] Started.' % Elapsed(self.item.start)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001022 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001023 self.item.finish = datetime.datetime.now()
1024 print >> self.item.outbuf, '[%s] Finished.' % Elapsed(self.item.finish)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001025 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001026 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001027 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001028 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001029 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001030 except Exception:
1031 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001032 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001033 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001034 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001035 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001036 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001037 work_queue.ready_cond.acquire()
1038 try:
1039 work_queue.ready_cond.notifyAll()
1040 finally:
1041 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001042
1043
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001044def GetEditor(git, git_editor=None):
1045 """Returns the most plausible editor to use.
1046
1047 In order of preference:
1048 - GIT_EDITOR/SVN_EDITOR environment variable
1049 - core.editor git configuration variable (if supplied by git-cl)
1050 - VISUAL environment variable
1051 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001052 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001053
1054 In the case of git-cl, this matches git's behaviour, except that it does not
1055 include dumb terminal detection.
1056
1057 In the case of gcl, this matches svn's behaviour, except that it does not
1058 accept a command-line flag or check the editor-cmd configuration variable.
1059 """
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001060 if git:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001061 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001062 else:
1063 editor = os.environ.get('SVN_EDITOR')
1064 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001065 editor = os.environ.get('VISUAL')
1066 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001067 editor = os.environ.get('EDITOR')
1068 if not editor:
1069 if sys.platform.startswith('win'):
1070 editor = 'notepad'
1071 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001072 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001073 return editor
1074
1075
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001076def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001077 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001078 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001079 # Make sure CRLF is handled properly by requiring none.
1080 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +00001081 print >> sys.stderr, (
1082 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001083 fileobj = os.fdopen(file_handle, 'w')
1084 # Still remove \r if present.
1085 fileobj.write(re.sub('\r?\n', '\n', content))
1086 fileobj.close()
1087
1088 try:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001089 editor = GetEditor(git, git_editor=git_editor)
1090 if not editor:
1091 return None
1092 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001093 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1094 # Msysgit requires the usage of 'env' to be present.
1095 cmd = 'env ' + cmd
1096 try:
1097 # shell=True to allow the shell to handle all forms of quotes in
1098 # $EDITOR.
1099 subprocess2.check_call(cmd, shell=True)
1100 except subprocess2.CalledProcessError:
1101 return None
1102 return FileRead(filename)
1103 finally:
1104 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001105
1106
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001107def UpgradeToHttps(url):
1108 """Upgrades random urls to https://.
1109
1110 Do not touch unknown urls like ssh:// or git://.
1111 Do not touch http:// urls with a port number,
1112 Fixes invalid GAE url.
1113 """
1114 if not url:
1115 return url
1116 if not re.match(r'[a-z\-]+\://.*', url):
1117 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1118 # relative url and will use http:///foo. Note that it defaults to http://
1119 # for compatibility with naked url like "localhost:8080".
1120 url = 'http://%s' % url
1121 parsed = list(urlparse.urlparse(url))
1122 # Do not automatically upgrade http to https if a port number is provided.
1123 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1124 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001125 return urlparse.urlunparse(parsed)
1126
1127
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001128def ParseCodereviewSettingsContent(content):
1129 """Process a codereview.settings file properly."""
1130 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1131 try:
1132 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1133 except ValueError:
1134 raise Error(
1135 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001136 def fix_url(key):
1137 if keyvals.get(key):
1138 keyvals[key] = UpgradeToHttps(keyvals[key])
1139 fix_url('CODE_REVIEW_SERVER')
1140 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001141 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001142
1143
1144def NumLocalCpus():
1145 """Returns the number of processors.
1146
dnj@chromium.org530523b2015-01-07 19:54:57 +00001147 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1148 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1149 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001150 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001151 # Surround the entire thing in try/except; no failure here should stop gclient
1152 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001153 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001154 # Use multiprocessing to get CPU count. This may raise
1155 # NotImplementedError.
1156 try:
1157 import multiprocessing
1158 return multiprocessing.cpu_count()
1159 except NotImplementedError: # pylint: disable=W0702
1160 # (UNIX) Query 'os.sysconf'.
1161 # pylint: disable=E1101
1162 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1163 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1164
1165 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1166 if 'NUMBER_OF_PROCESSORS' in os.environ:
1167 return int(os.environ['NUMBER_OF_PROCESSORS'])
1168 except Exception as e:
1169 logging.exception("Exception raised while probing CPU count: %s", e)
1170
1171 logging.debug('Failed to get CPU count. Defaulting to 1.')
1172 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001173
1174def DefaultDeltaBaseCacheLimit():
1175 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1176
1177 The primary constraint is the address space of virtual memory. The cache
1178 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1179 parameter is set too high.
1180 """
1181 if platform.architecture()[0].startswith('64'):
1182 return '2g'
1183 else:
1184 return '512m'
1185
szager@chromium.orgff113292014-03-25 06:02:08 +00001186def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001187 """Return reasonable default values for configuring git-index-pack.
1188
1189 Experiments suggest that higher values for pack.threads don't improve
1190 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001191 cache_limit = DefaultDeltaBaseCacheLimit()
1192 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1193 if url in THREADED_INDEX_PACK_BLACKLIST:
1194 result.extend(['-c', 'pack.threads=1'])
1195 return result