blob: 9c62a2a5b53af41b5aedae595bda06ecc61b1b77 [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
floitsch@google.comeaab7842011-04-28 09:07:58 +000087def IsDateRevision(revision):
88 """Returns true if the given revision is of the form "{ ... }"."""
89 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
90
91
92def MakeDateRevision(date):
93 """Returns a revision representing the latest revision before the given
94 date."""
95 return "{" + date + "}"
96
97
maruel@chromium.org5990f9d2010-07-07 18:02:58 +000098def SyntaxErrorToError(filename, e):
99 """Raises a gclient_utils.Error exception with the human readable message"""
100 try:
101 # Try to construct a human readable error message
102 if filename:
103 error_message = 'There is a syntax error in %s\n' % filename
104 else:
105 error_message = 'There is a syntax error\n'
106 error_message += 'Line #%s, character %s: "%s"' % (
107 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
108 except:
109 # Something went wrong, re-raise the original exception
110 raise e
111 else:
112 raise Error(error_message)
113
114
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000115class PrintableObject(object):
116 def __str__(self):
117 output = ''
118 for i in dir(self):
119 if i.startswith('__'):
120 continue
121 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
122 return output
123
124
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000125def FileRead(filename, mode='rU'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000126 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +0000127 # codecs.open() has different behavior than open() on python 2.6 so use
128 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000129 s = f.read()
130 try:
131 return s.decode('utf-8')
132 except UnicodeDecodeError:
133 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000134
135
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000136def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000137 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000138 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000139
140
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000141def safe_rename(old, new):
142 """Renames a file reliably.
143
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000144 Sometimes os.rename does not work because a dying git process keeps a handle
145 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000146 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000147 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000148 """
149 # roughly 10s
150 retries = 100
151 for i in range(retries):
152 try:
153 os.rename(old, new)
154 break
155 except OSError:
156 if i == (retries - 1):
157 # Give up.
158 raise
159 # retry
160 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
161 time.sleep(0.1)
162
163
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000164def rmtree(path):
165 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000166
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000167 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000168
169 shutil.rmtree() doesn't work on Windows if any of the files or directories
170 are read-only, which svn repositories and some .svn files are. We need to
171 be able to force the files to be writable (i.e., deletable) as we traverse
172 the tree.
173
174 Even with all this, Windows still sometimes fails to delete a file, citing
175 a permission error (maybe something to do with antivirus scans or disk
176 indexing). The best suggestion any of the user forums had was to wait a
177 bit and try again, so we do that too. It's hand-waving, but sometimes it
178 works. :/
179
180 On POSIX systems, things are a little bit simpler. The modes of the files
181 to be deleted doesn't matter, only the modes of the directories containing
182 them are significant. As the directory tree is traversed, each directory
183 has its mode set appropriately before descending into it. This should
184 result in the entire tree being removed, with the possible exception of
185 *path itself, because nothing attempts to change the mode of its parent.
186 Doing so would be hazardous, as it's not a directory slated for removal.
187 In the ordinary case, this is not a problem: for our purposes, the user
188 will never lack write permission on *path's parent.
189 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000190 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000191 return
192
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000193 if os.path.islink(path) or not os.path.isdir(path):
194 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000195
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000196 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000197 # Give up and use cmd.exe's rd command.
198 path = os.path.normcase(path)
199 for _ in xrange(3):
200 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
201 if exitcode == 0:
202 return
203 else:
204 print >> sys.stderr, 'rd exited with code %d' % exitcode
205 time.sleep(3)
206 raise Exception('Failed to remove path %s' % path)
207
208 # On POSIX systems, we need the x-bit set on the directory to access it,
209 # the r-bit to see its contents, and the w-bit to remove files from it.
210 # The actual modes of the files within the directory is irrelevant.
211 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000212
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000213 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000214 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000215
216 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000217 # If fullpath is a symbolic link that points to a directory, isdir will
218 # be True, but we don't want to descend into that as a directory, we just
219 # want to remove the link. Check islink and treat links as ordinary files
220 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000221 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000222 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000223 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000224 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000225 # Recurse.
226 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000227
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000228 remove(os.rmdir, path)
229
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000230
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000231def safe_makedirs(tree):
232 """Creates the directory in a safe manner.
233
234 Because multiple threads can create these directories concurently, trap the
235 exception and pass on.
236 """
237 count = 0
238 while not os.path.exists(tree):
239 count += 1
240 try:
241 os.makedirs(tree)
242 except OSError, e:
243 # 17 POSIX, 183 Windows
244 if e.errno not in (17, 183):
245 raise
246 if count > 40:
247 # Give up.
248 raise
249
250
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000251def CommandToStr(args):
252 """Converts an arg list into a shell escaped string."""
253 return ' '.join(pipes.quote(arg) for arg in args)
254
255
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000256def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000257 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000258
maruel@chromium.org17d01792010-09-01 18:07:10 +0000259 If |always| is True, a message indicating what is being done
260 is printed to stdout all the time even if not output is generated. Otherwise
261 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000262 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000263 stdout = kwargs.setdefault('stdout', sys.stdout)
264 if header is None:
265 header = "\n________ running '%s' in '%s'\n" % (
ilevy@chromium.org4aad1852013-07-12 21:32:51 +0000266 ' '.join(args), kwargs.get('cwd', '.'))
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000267
maruel@chromium.org17d01792010-09-01 18:07:10 +0000268 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000269 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000270 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000271 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000272 def filter_msg(line):
273 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000274 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000275 elif filter_fn:
276 filter_fn(line)
277 kwargs['filter_fn'] = filter_msg
278 kwargs['call_filter_on_first_line'] = True
279 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000280 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000281 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000282
maruel@chromium.org17d01792010-09-01 18:07:10 +0000283
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000284class Wrapper(object):
285 """Wraps an object, acting as a transparent proxy for all properties by
286 default.
287 """
288 def __init__(self, wrapped):
289 self._wrapped = wrapped
290
291 def __getattr__(self, name):
292 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000293
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000294
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000295class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000296 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000297 def __init__(self, wrapped, delay):
298 super(AutoFlush, self).__init__(wrapped)
299 if not hasattr(self, 'lock'):
300 self.lock = threading.Lock()
301 self.__last_flushed_at = time.time()
302 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000303
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000304 @property
305 def autoflush(self):
306 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000307
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000308 def write(self, out, *args, **kwargs):
309 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000310 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000311 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000312 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000313 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000314 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000315 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000316 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000317 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000318 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000319 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000320
321
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000322class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000323 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000324 threads with a NN> prefix.
325 """
326 def __init__(self, wrapped, include_zero=False):
327 super(Annotated, self).__init__(wrapped)
328 if not hasattr(self, 'lock'):
329 self.lock = threading.Lock()
330 self.__output_buffers = {}
331 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000332
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000333 @property
334 def annotated(self):
335 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000336
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000337 def write(self, out):
338 index = getattr(threading.currentThread(), 'index', 0)
339 if not index and not self.__include_zero:
340 # Unindexed threads aren't buffered.
341 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000342
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000343 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000344 try:
345 # Use a dummy array to hold the string so the code can be lockless.
346 # Strings are immutable, requiring to keep a lock for the whole dictionary
347 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000348 if not index in self.__output_buffers:
349 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000350 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000351 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000352 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000353 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000354
355 # Continue lockless.
356 obj[0] += out
357 while '\n' in obj[0]:
358 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000359 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000360 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000361 obj[0] = remaining
362
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000363 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000364 """Flush buffered output."""
365 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000366 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000367 try:
368 # Detect threads no longer existing.
369 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000370 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000371 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000372 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000373 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000374 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000375 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000376 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000377 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000378
379 # Don't keep the lock while writting. Will append \n when it shouldn't.
380 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000381 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000382 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
383 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000384
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000385
386def MakeFileAutoFlush(fileobj, delay=10):
387 autoflush = getattr(fileobj, 'autoflush', None)
388 if autoflush:
389 autoflush.delay = delay
390 return fileobj
391 return AutoFlush(fileobj, delay)
392
393
394def MakeFileAnnotated(fileobj, include_zero=False):
395 if getattr(fileobj, 'annotated', None):
396 return fileobj
397 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000398
399
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000400GCLIENT_CHILDREN = []
401GCLIENT_CHILDREN_LOCK = threading.Lock()
402
403
404class GClientChildren(object):
405 @staticmethod
406 def add(popen_obj):
407 with GCLIENT_CHILDREN_LOCK:
408 GCLIENT_CHILDREN.append(popen_obj)
409
410 @staticmethod
411 def remove(popen_obj):
412 with GCLIENT_CHILDREN_LOCK:
413 GCLIENT_CHILDREN.remove(popen_obj)
414
415 @staticmethod
416 def _attemptToKillChildren():
417 global GCLIENT_CHILDREN
418 with GCLIENT_CHILDREN_LOCK:
419 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
420
421 for zombie in zombies:
422 try:
423 zombie.kill()
424 except OSError:
425 pass
426
427 with GCLIENT_CHILDREN_LOCK:
428 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
429
430 @staticmethod
431 def _areZombies():
432 with GCLIENT_CHILDREN_LOCK:
433 return bool(GCLIENT_CHILDREN)
434
435 @staticmethod
436 def KillAllRemainingChildren():
437 GClientChildren._attemptToKillChildren()
438
439 if GClientChildren._areZombies():
440 time.sleep(0.5)
441 GClientChildren._attemptToKillChildren()
442
443 with GCLIENT_CHILDREN_LOCK:
444 if GCLIENT_CHILDREN:
445 print >> sys.stderr, 'Could not kill the following subprocesses:'
446 for zombie in GCLIENT_CHILDREN:
447 print >> sys.stderr, ' ', zombie.pid
448
449
maruel@chromium.org17d01792010-09-01 18:07:10 +0000450def CheckCallAndFilter(args, stdout=None, filter_fn=None,
451 print_stdout=None, call_filter_on_first_line=False,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000452 retry=False, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000453 """Runs a command and calls back a filter function if needed.
454
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000455 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000456 print_stdout: If True, the command's stdout is forwarded to stdout.
457 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000458 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000459 character trimmed.
460 stdout: Can be any bufferable output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000461 retry: If the process exits non-zero, sleep for a brief interval and try
462 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000463
464 stderr is always redirected to stdout.
465 """
466 assert print_stdout or filter_fn
467 stdout = stdout or sys.stdout
hinoka@google.com267f33e2014-02-28 22:02:32 +0000468 output = cStringIO.StringIO()
maruel@chromium.org17d01792010-09-01 18:07:10 +0000469 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000470
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000471 sleep_interval = RETRY_INITIAL_SLEEP
472 run_cwd = kwargs.get('cwd', os.getcwd())
473 for _ in xrange(RETRY_MAX + 1):
474 kid = subprocess2.Popen(
475 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
476 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000477
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000478 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000479
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000480 # Do a flush of stdout before we begin reading from the subprocess2's stdout
481 stdout.flush()
482
483 # Also, we need to forward stdout to prevent weird re-ordering of output.
484 # This has to be done on a per byte basis to make sure it is not buffered:
485 # normally buffering is done for each line, but if svn requests input, no
486 # end-of-line character is output after the prompt and it would not show up.
487 try:
488 in_byte = kid.stdout.read(1)
489 if in_byte:
490 if call_filter_on_first_line:
491 filter_fn(None)
492 in_line = ''
493 while in_byte:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000494 output.write(in_byte)
495 if print_stdout:
496 stdout.write(in_byte)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000497 if in_byte not in ['\r', '\n']:
498 in_line += in_byte
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000499 else:
500 filter_fn(in_line)
501 in_line = ''
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000502 in_byte = kid.stdout.read(1)
503 # Flush the rest of buffered output. This is only an issue with
504 # stdout/stderr not ending with a \n.
505 if len(in_line):
szager@google.com85d3e3a2011-10-07 17:12:00 +0000506 filter_fn(in_line)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000507 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000508
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000509 # Don't put this in a 'finally,' since the child may still run if we get
510 # an exception.
511 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000512
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000513 except KeyboardInterrupt:
514 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
515 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000516
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000517 if rv == 0:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000518 return output.getvalue()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000519 if not retry:
520 break
521 print ("WARNING: subprocess '%s' in %s failed; will retry after a short "
522 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000523 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000524 sleep_interval *= 2
525 raise subprocess2.CalledProcessError(
526 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000527
528
agable@chromium.org5a306a22014-02-24 22:13:59 +0000529class GitFilter(object):
530 """A filter_fn implementation for quieting down git output messages.
531
532 Allows a custom function to skip certain lines (predicate), and will throttle
533 the output of percentage completed lines to only output every X seconds.
534 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000535 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000536
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000537 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000538 """
539 Args:
540 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
541 XX% complete messages) to only be printed at least |time_throttle|
542 seconds apart.
543 predicate (f(line)): An optional function which is invoked for every line.
544 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000545 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000546 """
547 self.last_time = 0
548 self.time_throttle = time_throttle
549 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000550 self.out_fh = out_fh or sys.stdout
551 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000552
553 def __call__(self, line):
554 # git uses an escape sequence to clear the line; elide it.
555 esc = line.find(unichr(033))
556 if esc > -1:
557 line = line[:esc]
558 if self.predicate and not self.predicate(line):
559 return
560 now = time.time()
561 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000562 if match:
563 if match.group(1) != self.progress_prefix:
564 self.progress_prefix = match.group(1)
565 elif now - self.last_time < self.time_throttle:
566 return
567 self.last_time = now
568 self.out_fh.write('[%s] ' % Elapsed())
569 print >> self.out_fh, line
agable@chromium.org5a306a22014-02-24 22:13:59 +0000570
571
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000572def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000573 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000574 real_from_dir = os.path.realpath(from_dir)
575 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000576 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000577 split_path = os.path.split(path)
578 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000579 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000580 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000581
582 # If we did not find the file in the current directory, make sure we are in a
583 # sub directory that is controlled by this configuration.
584 if path != real_from_dir:
585 entries_filename = os.path.join(path, filename + '_entries')
586 if not os.path.exists(entries_filename):
587 # If .gclient_entries does not exist, a previous call to gclient sync
588 # might have failed. In that case, we cannot verify that the .gclient
589 # is the one we want to use. In order to not to cause too much trouble,
590 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000591 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000592 "file you want to use" % (filename, path))
593 return path
594 scope = {}
595 try:
596 exec(FileRead(entries_filename), scope)
597 except SyntaxError, e:
598 SyntaxErrorToError(filename, e)
599 all_directories = scope['entries'].keys()
600 path_to_check = real_from_dir[len(path)+1:]
601 while path_to_check:
602 if path_to_check in all_directories:
603 return path
604 path_to_check = os.path.dirname(path_to_check)
605 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000606
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000607 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000608 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000609
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000610
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000611def PathDifference(root, subpath):
612 """Returns the difference subpath minus root."""
613 root = os.path.realpath(root)
614 subpath = os.path.realpath(subpath)
615 if not subpath.startswith(root):
616 return None
617 # If the root does not have a trailing \ or /, we add it so the returned
618 # path starts immediately after the seperator regardless of whether it is
619 # provided.
620 root = os.path.join(root, '')
621 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000622
623
624def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000625 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000626
rcui@google.com13595ff2011-10-13 01:25:07 +0000627 Returns nearest upper-level directory with the passed in file.
628 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000629 if not path:
630 path = os.getcwd()
631 path = os.path.realpath(path)
632 while True:
633 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000634 if os.path.exists(file_path):
635 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000636 (new_path, _) = os.path.split(path)
637 if new_path == path:
638 return None
639 path = new_path
640
641
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000642def GetMacWinOrLinux():
643 """Returns 'mac', 'win', or 'linux', matching the current platform."""
644 if sys.platform.startswith(('cygwin', 'win')):
645 return 'win'
646 elif sys.platform.startswith('linux'):
647 return 'linux'
648 elif sys.platform == 'darwin':
649 return 'mac'
650 raise Error('Unknown platform: ' + sys.platform)
651
652
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000653def GetBuildtoolsPath():
654 """Returns the full path to the buildtools directory.
655 This is based on the root of the checkout containing the current directory."""
656 gclient_root = FindGclientRoot(os.getcwd())
657 if not gclient_root:
jochen@chromium.orgaaee92f2014-07-02 07:35:31 +0000658 # Some projects might not use .gclient. Try to see whether we're in a git
659 # checkout.
660 top_dir = [os.getcwd()]
661 def filter_fn(line):
662 top_dir[0] = os.path.normpath(line.rstrip('\n'))
663 try:
664 CheckCallAndFilter(["git", "rev-parse", "--show-toplevel"],
665 print_stdout=False, filter_fn=filter_fn)
666 except Exception:
667 pass
668 top_dir = top_dir[0]
669 if os.path.exists(os.path.join(top_dir, 'buildtools')):
670 return os.path.join(top_dir, 'buildtools')
brettw@chromium.orgcc968fe2014-06-23 17:30:32 +0000671 return None
672 return os.path.join(gclient_root, 'src', 'buildtools')
673
674
675def GetBuildtoolsPlatformBinaryPath():
676 """Returns the full path to the binary directory for the current platform."""
677 # Mac and Windows just have one directory, Linux has two according to whether
678 # it's 32 or 64 bits.
679 buildtools_path = GetBuildtoolsPath()
680 if not buildtools_path:
681 return None
682
683 if sys.platform.startswith(('cygwin', 'win')):
684 subdir = 'win'
685 elif sys.platform == 'darwin':
686 subdir = 'mac'
687 elif sys.platform.startswith('linux'):
688 if sys.maxsize > 2**32:
689 subdir = 'linux64'
690 else:
691 subdir = 'linux32'
692 else:
693 raise Error('Unknown platform: ' + sys.platform)
694 return os.path.join(buildtools_path, subdir)
695
696
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000697def GetExeSuffix():
698 """Returns '' or '.exe' depending on how executables work on this platform."""
699 if sys.platform.startswith(('cygwin', 'win')):
700 return '.exe'
701 return ''
702
703
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000704def GetGClientRootAndEntries(path=None):
705 """Returns the gclient root and the dict of entries."""
706 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000707 root = FindFileUpwards(config_file, path)
708 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000709 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000710 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000711 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000712 env = {}
713 execfile(config_path, env)
714 config_dir = os.path.dirname(config_path)
715 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000716
717
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000718def lockedmethod(method):
719 """Method decorator that holds self.lock for the duration of the call."""
720 def inner(self, *args, **kwargs):
721 try:
722 try:
723 self.lock.acquire()
724 except KeyboardInterrupt:
725 print >> sys.stderr, 'Was deadlocked'
726 raise
727 return method(self, *args, **kwargs)
728 finally:
729 self.lock.release()
730 return inner
731
732
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000733class WorkItem(object):
734 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000735 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
736 # As a workaround, use a single lock. Yep you read it right. Single lock for
737 # all the 100 objects.
738 lock = threading.Lock()
739
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000740 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000741 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000742 self._name = name
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000743 self.outbuf = cStringIO.StringIO()
744 self.start = self.finish = None
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000745
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000746 def run(self, work_queue):
747 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000748 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000749 pass
750
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000751 @property
752 def name(self):
753 return self._name
754
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000755
756class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000757 """Runs a set of WorkItem that have interdependencies and were WorkItem are
758 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000759
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000760 In gclient's case, Dependencies sometime needs to be run out of order due to
761 From() keyword. This class manages that all the required dependencies are run
762 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000763
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000764 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000765 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000766 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000767 """jobs specifies the number of concurrent tasks to allow. progress is a
768 Progress instance."""
769 # Set when a thread is done or a new item is enqueued.
770 self.ready_cond = threading.Condition()
771 # Maximum number of concurrent tasks.
772 self.jobs = jobs
773 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000774 self.queued = []
775 # List of strings representing each Dependency.name that was run.
776 self.ran = []
777 # List of items currently running.
778 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000779 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000780 self.exceptions = Queue.Queue()
781 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000782 self.progress = progress
783 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000784 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000785
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000786 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000787 self.verbose = verbose
788 self.last_join = None
789 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000790
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000791 def enqueue(self, d):
792 """Enqueue one Dependency to be executed later once its requirements are
793 satisfied.
794 """
795 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000796 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000797 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000798 self.queued.append(d)
799 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000800 if self.jobs == 1:
801 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000802 logging.debug('enqueued(%s)' % d.name)
803 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000804 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000805 self.progress.update(0)
806 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000807 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000808 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000809
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000810 def out_cb(self, _):
811 self.last_subproc_output = datetime.datetime.now()
812 return True
813
814 @staticmethod
815 def format_task_output(task, comment=''):
816 if comment:
817 comment = ' (%s)' % comment
818 if task.start and task.finish:
819 elapsed = ' (Elapsed: %s)' % (
820 str(task.finish - task.start).partition('.')[0])
821 else:
822 elapsed = ''
823 return """
824%s%s%s
825----------------------------------------
826%s
827----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000828 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000829
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000830 def flush(self, *args, **kwargs):
831 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000832 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000833 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000834 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000835 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000836 while True:
837 # Check for task to run first, then wait.
838 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000839 if not self.exceptions.empty():
840 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000841 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000842 self._flush_terminated_threads()
843 if (not self.queued and not self.running or
844 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000845 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000846 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000847
848 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000849 for i in xrange(len(self.queued)):
850 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000851 if (self.ignore_requirements or
852 not (set(self.queued[i].requirements) - set(self.ran))):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000853 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000854 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000855 break
856 else:
857 # Couldn't find an item that could run. Break out the outher loop.
858 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000859
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000860 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000861 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000862 break
863 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000864 try:
865 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000866 # If we haven't printed to terminal for a while, but we have received
867 # spew from a suprocess, let the user know we're still progressing.
868 now = datetime.datetime.now()
869 if (now - self.last_join > datetime.timedelta(seconds=60) and
870 self.last_subproc_output > self.last_join):
871 if self.progress:
872 print >> sys.stdout, ''
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000873 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000874 elapsed = Elapsed()
875 print >> sys.stdout, '[%s] Still working on:' % elapsed
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000876 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000877 for task in self.running:
878 print >> sys.stdout, '[%s] %s' % (elapsed, task.item.name)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000879 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000880 except KeyboardInterrupt:
881 # Help debugging by printing some information:
882 print >> sys.stderr, (
883 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
884 'Running: %d') % (
885 self.jobs,
886 len(self.queued),
887 ', '.join(self.ran),
888 len(self.running)))
889 for i in self.queued:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000890 print >> sys.stderr, '%s (not started): %s' % (
891 i.name, ', '.join(i.requirements))
892 for i in self.running:
893 print >> sys.stderr, self.format_task_output(i.item, 'interrupted')
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000894 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000895 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000896 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000897 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000898
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000899 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000900 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000901 if self.progress:
902 print >> sys.stdout, ''
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000903 # To get back the stack location correctly, the raise a, b, c form must be
904 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000905 e, task = self.exceptions.get()
906 print >> sys.stderr, self.format_task_output(task.item, 'ERROR')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000907 raise e[0], e[1], e[2]
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000908 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000909 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000910
maruel@chromium.org3742c842010-09-09 19:27:14 +0000911 def _flush_terminated_threads(self):
912 """Flush threads that have terminated."""
913 running = self.running
914 self.running = []
915 for t in running:
916 if t.isAlive():
917 self.running.append(t)
918 else:
919 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000920 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000921 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000922 if self.verbose:
923 print >> sys.stdout, self.format_task_output(t.item)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000924 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000925 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000926 if t.item.name in self.ran:
927 raise Error(
928 'gclient is confused, "%s" is already in "%s"' % (
929 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000930 if not t.item.name in self.ran:
931 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000932
933 def _run_one_task(self, task_item, args, kwargs):
934 if self.jobs > 1:
935 # Start the thread.
936 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000937 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000938 self.running.append(new_thread)
939 new_thread.start()
940 else:
941 # Run the 'thread' inside the main thread. Don't try to catch any
942 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000943 try:
944 task_item.start = datetime.datetime.now()
945 print >> task_item.outbuf, '[%s] Started.' % Elapsed(task_item.start)
946 task_item.run(*args, **kwargs)
947 task_item.finish = datetime.datetime.now()
948 print >> task_item.outbuf, '[%s] Finished.' % Elapsed(task_item.finish)
949 self.ran.append(task_item.name)
950 if self.verbose:
951 if self.progress:
952 print >> sys.stdout, ''
953 print >> sys.stdout, self.format_task_output(task_item)
954 if self.progress:
955 self.progress.update(1, ', '.join(t.item.name for t in self.running))
956 except KeyboardInterrupt:
957 print >> sys.stderr, self.format_task_output(task_item, 'interrupted')
958 raise
959 except Exception:
960 print >> sys.stderr, self.format_task_output(task_item, 'ERROR')
961 raise
962
maruel@chromium.org3742c842010-09-09 19:27:14 +0000963
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000964 class _Worker(threading.Thread):
965 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000966 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000967 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000968 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000969 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000970 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000971 self.args = args
972 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000973 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000974
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000975 def run(self):
976 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000977 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000978 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000979 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000980 self.item.start = datetime.datetime.now()
981 print >> self.item.outbuf, '[%s] Started.' % Elapsed(self.item.start)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000982 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000983 self.item.finish = datetime.datetime.now()
984 print >> self.item.outbuf, '[%s] Finished.' % Elapsed(self.item.finish)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000985 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000986 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000987 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000988 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000989 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000990 except Exception:
991 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000992 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000993 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000994 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000995 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000996 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000997 work_queue.ready_cond.acquire()
998 try:
999 work_queue.ready_cond.notifyAll()
1000 finally:
1001 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001002
1003
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001004def GetEditor(git, git_editor=None):
1005 """Returns the most plausible editor to use.
1006
1007 In order of preference:
1008 - GIT_EDITOR/SVN_EDITOR environment variable
1009 - core.editor git configuration variable (if supplied by git-cl)
1010 - VISUAL environment variable
1011 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001012 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001013
1014 In the case of git-cl, this matches git's behaviour, except that it does not
1015 include dumb terminal detection.
1016
1017 In the case of gcl, this matches svn's behaviour, except that it does not
1018 accept a command-line flag or check the editor-cmd configuration variable.
1019 """
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001020 if git:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001021 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001022 else:
1023 editor = os.environ.get('SVN_EDITOR')
1024 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001025 editor = os.environ.get('VISUAL')
1026 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001027 editor = os.environ.get('EDITOR')
1028 if not editor:
1029 if sys.platform.startswith('win'):
1030 editor = 'notepad'
1031 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001032 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001033 return editor
1034
1035
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001036def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001037 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001038 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001039 # Make sure CRLF is handled properly by requiring none.
1040 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +00001041 print >> sys.stderr, (
1042 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001043 fileobj = os.fdopen(file_handle, 'w')
1044 # Still remove \r if present.
1045 fileobj.write(re.sub('\r?\n', '\n', content))
1046 fileobj.close()
1047
1048 try:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001049 editor = GetEditor(git, git_editor=git_editor)
1050 if not editor:
1051 return None
1052 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001053 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1054 # Msysgit requires the usage of 'env' to be present.
1055 cmd = 'env ' + cmd
1056 try:
1057 # shell=True to allow the shell to handle all forms of quotes in
1058 # $EDITOR.
1059 subprocess2.check_call(cmd, shell=True)
1060 except subprocess2.CalledProcessError:
1061 return None
1062 return FileRead(filename)
1063 finally:
1064 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001065
1066
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001067def UpgradeToHttps(url):
1068 """Upgrades random urls to https://.
1069
1070 Do not touch unknown urls like ssh:// or git://.
1071 Do not touch http:// urls with a port number,
1072 Fixes invalid GAE url.
1073 """
1074 if not url:
1075 return url
1076 if not re.match(r'[a-z\-]+\://.*', url):
1077 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1078 # relative url and will use http:///foo. Note that it defaults to http://
1079 # for compatibility with naked url like "localhost:8080".
1080 url = 'http://%s' % url
1081 parsed = list(urlparse.urlparse(url))
1082 # Do not automatically upgrade http to https if a port number is provided.
1083 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1084 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001085 return urlparse.urlunparse(parsed)
1086
1087
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001088def ParseCodereviewSettingsContent(content):
1089 """Process a codereview.settings file properly."""
1090 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1091 try:
1092 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1093 except ValueError:
1094 raise Error(
1095 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001096 def fix_url(key):
1097 if keyvals.get(key):
1098 keyvals[key] = UpgradeToHttps(keyvals[key])
1099 fix_url('CODE_REVIEW_SERVER')
1100 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001101 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001102
1103
1104def NumLocalCpus():
1105 """Returns the number of processors.
1106
1107 Python on OSX 10.6 raises a NotImplementedError exception.
1108 """
1109 try:
1110 import multiprocessing
1111 return multiprocessing.cpu_count()
1112 except: # pylint: disable=W0702
1113 # Mac OS 10.6 only
1114 # pylint: disable=E1101
1115 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
szager@chromium.orgfc616382014-03-18 20:32:04 +00001116
1117def DefaultDeltaBaseCacheLimit():
1118 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1119
1120 The primary constraint is the address space of virtual memory. The cache
1121 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1122 parameter is set too high.
1123 """
1124 if platform.architecture()[0].startswith('64'):
1125 return '2g'
1126 else:
1127 return '512m'
1128
szager@chromium.orgff113292014-03-25 06:02:08 +00001129def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001130 """Return reasonable default values for configuring git-index-pack.
1131
1132 Experiments suggest that higher values for pack.threads don't improve
1133 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001134 cache_limit = DefaultDeltaBaseCacheLimit()
1135 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1136 if url in THREADED_INDEX_PACK_BLACKLIST:
1137 result.extend(['-c', 'pack.threads=1'])
1138 return result