blob: f89b601f3a201146e8446aee570cd3bc30f7762a [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:
maruel@chromium.org116704f2010-06-11 17:34:38 +000078 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000079 if len(components) == 1:
80 components += [None]
81 return tuple(components)
82
83
floitsch@google.comeaab7842011-04-28 09:07:58 +000084def IsDateRevision(revision):
85 """Returns true if the given revision is of the form "{ ... }"."""
86 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
87
88
89def MakeDateRevision(date):
90 """Returns a revision representing the latest revision before the given
91 date."""
92 return "{" + date + "}"
93
94
maruel@chromium.org5990f9d2010-07-07 18:02:58 +000095def SyntaxErrorToError(filename, e):
96 """Raises a gclient_utils.Error exception with the human readable message"""
97 try:
98 # Try to construct a human readable error message
99 if filename:
100 error_message = 'There is a syntax error in %s\n' % filename
101 else:
102 error_message = 'There is a syntax error\n'
103 error_message += 'Line #%s, character %s: "%s"' % (
104 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
105 except:
106 # Something went wrong, re-raise the original exception
107 raise e
108 else:
109 raise Error(error_message)
110
111
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000112class PrintableObject(object):
113 def __str__(self):
114 output = ''
115 for i in dir(self):
116 if i.startswith('__'):
117 continue
118 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
119 return output
120
121
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000122def FileRead(filename, mode='rU'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000123 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +0000124 # codecs.open() has different behavior than open() on python 2.6 so use
125 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000126 s = f.read()
127 try:
128 return s.decode('utf-8')
129 except UnicodeDecodeError:
130 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000131
132
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000133def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000134 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000135 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000136
137
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000138def safe_rename(old, new):
139 """Renames a file reliably.
140
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000141 Sometimes os.rename does not work because a dying git process keeps a handle
142 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000143 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000144 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000145 """
146 # roughly 10s
147 retries = 100
148 for i in range(retries):
149 try:
150 os.rename(old, new)
151 break
152 except OSError:
153 if i == (retries - 1):
154 # Give up.
155 raise
156 # retry
157 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
158 time.sleep(0.1)
159
160
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000161def rmtree(path):
162 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000163
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000164 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000165
166 shutil.rmtree() doesn't work on Windows if any of the files or directories
167 are read-only, which svn repositories and some .svn files are. We need to
168 be able to force the files to be writable (i.e., deletable) as we traverse
169 the tree.
170
171 Even with all this, Windows still sometimes fails to delete a file, citing
172 a permission error (maybe something to do with antivirus scans or disk
173 indexing). The best suggestion any of the user forums had was to wait a
174 bit and try again, so we do that too. It's hand-waving, but sometimes it
175 works. :/
176
177 On POSIX systems, things are a little bit simpler. The modes of the files
178 to be deleted doesn't matter, only the modes of the directories containing
179 them are significant. As the directory tree is traversed, each directory
180 has its mode set appropriately before descending into it. This should
181 result in the entire tree being removed, with the possible exception of
182 *path itself, because nothing attempts to change the mode of its parent.
183 Doing so would be hazardous, as it's not a directory slated for removal.
184 In the ordinary case, this is not a problem: for our purposes, the user
185 will never lack write permission on *path's parent.
186 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000187 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000188 return
189
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000190 if os.path.islink(path) or not os.path.isdir(path):
191 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000192
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000193 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000194 # Give up and use cmd.exe's rd command.
195 path = os.path.normcase(path)
196 for _ in xrange(3):
197 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
198 if exitcode == 0:
199 return
200 else:
201 print >> sys.stderr, 'rd exited with code %d' % exitcode
202 time.sleep(3)
203 raise Exception('Failed to remove path %s' % path)
204
205 # On POSIX systems, we need the x-bit set on the directory to access it,
206 # the r-bit to see its contents, and the w-bit to remove files from it.
207 # The actual modes of the files within the directory is irrelevant.
208 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000209
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000210 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000211 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000212
213 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000214 # If fullpath is a symbolic link that points to a directory, isdir will
215 # be True, but we don't want to descend into that as a directory, we just
216 # want to remove the link. Check islink and treat links as ordinary files
217 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000218 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000219 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000220 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000221 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000222 # Recurse.
223 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000224
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000225 remove(os.rmdir, path)
226
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000227
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000228def safe_makedirs(tree):
229 """Creates the directory in a safe manner.
230
231 Because multiple threads can create these directories concurently, trap the
232 exception and pass on.
233 """
234 count = 0
235 while not os.path.exists(tree):
236 count += 1
237 try:
238 os.makedirs(tree)
239 except OSError, e:
240 # 17 POSIX, 183 Windows
241 if e.errno not in (17, 183):
242 raise
243 if count > 40:
244 # Give up.
245 raise
246
247
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000248def CommandToStr(args):
249 """Converts an arg list into a shell escaped string."""
250 return ' '.join(pipes.quote(arg) for arg in args)
251
252
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000253def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000254 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000255
maruel@chromium.org17d01792010-09-01 18:07:10 +0000256 If |always| is True, a message indicating what is being done
257 is printed to stdout all the time even if not output is generated. Otherwise
258 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000259 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000260 stdout = kwargs.setdefault('stdout', sys.stdout)
261 if header is None:
262 header = "\n________ running '%s' in '%s'\n" % (
ilevy@chromium.org4aad1852013-07-12 21:32:51 +0000263 ' '.join(args), kwargs.get('cwd', '.'))
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000264
maruel@chromium.org17d01792010-09-01 18:07:10 +0000265 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000266 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000267 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000268 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000269 def filter_msg(line):
270 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000271 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000272 elif filter_fn:
273 filter_fn(line)
274 kwargs['filter_fn'] = filter_msg
275 kwargs['call_filter_on_first_line'] = True
276 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000277 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000278 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000279
maruel@chromium.org17d01792010-09-01 18:07:10 +0000280
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000281class Wrapper(object):
282 """Wraps an object, acting as a transparent proxy for all properties by
283 default.
284 """
285 def __init__(self, wrapped):
286 self._wrapped = wrapped
287
288 def __getattr__(self, name):
289 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000290
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000291
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000292class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000293 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000294 def __init__(self, wrapped, delay):
295 super(AutoFlush, self).__init__(wrapped)
296 if not hasattr(self, 'lock'):
297 self.lock = threading.Lock()
298 self.__last_flushed_at = time.time()
299 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000300
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000301 @property
302 def autoflush(self):
303 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000304
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000305 def write(self, out, *args, **kwargs):
306 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000307 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000308 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000309 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000310 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000311 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000312 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000313 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000314 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000315 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000316 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000317
318
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000319class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000320 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000321 threads with a NN> prefix.
322 """
323 def __init__(self, wrapped, include_zero=False):
324 super(Annotated, self).__init__(wrapped)
325 if not hasattr(self, 'lock'):
326 self.lock = threading.Lock()
327 self.__output_buffers = {}
328 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000329
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000330 @property
331 def annotated(self):
332 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000333
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000334 def write(self, out):
335 index = getattr(threading.currentThread(), 'index', 0)
336 if not index and not self.__include_zero:
337 # Unindexed threads aren't buffered.
338 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000339
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000340 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000341 try:
342 # Use a dummy array to hold the string so the code can be lockless.
343 # Strings are immutable, requiring to keep a lock for the whole dictionary
344 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000345 if not index in self.__output_buffers:
346 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000347 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000348 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000349 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000350 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000351
352 # Continue lockless.
353 obj[0] += out
354 while '\n' in obj[0]:
355 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000356 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000357 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000358 obj[0] = remaining
359
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000360 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000361 """Flush buffered output."""
362 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000363 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000364 try:
365 # Detect threads no longer existing.
366 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000367 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000368 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000369 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000370 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000371 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000372 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000373 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000374 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000375
376 # Don't keep the lock while writting. Will append \n when it shouldn't.
377 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000378 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000379 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
380 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000381
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000382
383def MakeFileAutoFlush(fileobj, delay=10):
384 autoflush = getattr(fileobj, 'autoflush', None)
385 if autoflush:
386 autoflush.delay = delay
387 return fileobj
388 return AutoFlush(fileobj, delay)
389
390
391def MakeFileAnnotated(fileobj, include_zero=False):
392 if getattr(fileobj, 'annotated', None):
393 return fileobj
394 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000395
396
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000397GCLIENT_CHILDREN = []
398GCLIENT_CHILDREN_LOCK = threading.Lock()
399
400
401class GClientChildren(object):
402 @staticmethod
403 def add(popen_obj):
404 with GCLIENT_CHILDREN_LOCK:
405 GCLIENT_CHILDREN.append(popen_obj)
406
407 @staticmethod
408 def remove(popen_obj):
409 with GCLIENT_CHILDREN_LOCK:
410 GCLIENT_CHILDREN.remove(popen_obj)
411
412 @staticmethod
413 def _attemptToKillChildren():
414 global GCLIENT_CHILDREN
415 with GCLIENT_CHILDREN_LOCK:
416 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
417
418 for zombie in zombies:
419 try:
420 zombie.kill()
421 except OSError:
422 pass
423
424 with GCLIENT_CHILDREN_LOCK:
425 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
426
427 @staticmethod
428 def _areZombies():
429 with GCLIENT_CHILDREN_LOCK:
430 return bool(GCLIENT_CHILDREN)
431
432 @staticmethod
433 def KillAllRemainingChildren():
434 GClientChildren._attemptToKillChildren()
435
436 if GClientChildren._areZombies():
437 time.sleep(0.5)
438 GClientChildren._attemptToKillChildren()
439
440 with GCLIENT_CHILDREN_LOCK:
441 if GCLIENT_CHILDREN:
442 print >> sys.stderr, 'Could not kill the following subprocesses:'
443 for zombie in GCLIENT_CHILDREN:
444 print >> sys.stderr, ' ', zombie.pid
445
446
maruel@chromium.org17d01792010-09-01 18:07:10 +0000447def CheckCallAndFilter(args, stdout=None, filter_fn=None,
448 print_stdout=None, call_filter_on_first_line=False,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000449 retry=False, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000450 """Runs a command and calls back a filter function if needed.
451
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000452 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000453 print_stdout: If True, the command's stdout is forwarded to stdout.
454 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000455 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000456 character trimmed.
457 stdout: Can be any bufferable output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000458 retry: If the process exits non-zero, sleep for a brief interval and try
459 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000460
461 stderr is always redirected to stdout.
462 """
463 assert print_stdout or filter_fn
464 stdout = stdout or sys.stdout
hinoka@google.com267f33e2014-02-28 22:02:32 +0000465 output = cStringIO.StringIO()
maruel@chromium.org17d01792010-09-01 18:07:10 +0000466 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000467
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000468 sleep_interval = RETRY_INITIAL_SLEEP
469 run_cwd = kwargs.get('cwd', os.getcwd())
470 for _ in xrange(RETRY_MAX + 1):
471 kid = subprocess2.Popen(
472 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
473 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000474
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000475 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000476
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000477 # Do a flush of stdout before we begin reading from the subprocess2's stdout
478 stdout.flush()
479
480 # Also, we need to forward stdout to prevent weird re-ordering of output.
481 # This has to be done on a per byte basis to make sure it is not buffered:
482 # normally buffering is done for each line, but if svn requests input, no
483 # end-of-line character is output after the prompt and it would not show up.
484 try:
485 in_byte = kid.stdout.read(1)
486 if in_byte:
487 if call_filter_on_first_line:
488 filter_fn(None)
489 in_line = ''
490 while in_byte:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000491 output.write(in_byte)
492 if print_stdout:
493 stdout.write(in_byte)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000494 if in_byte not in ['\r', '\n']:
495 in_line += in_byte
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000496 else:
497 filter_fn(in_line)
498 in_line = ''
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000499 in_byte = kid.stdout.read(1)
500 # Flush the rest of buffered output. This is only an issue with
501 # stdout/stderr not ending with a \n.
502 if len(in_line):
szager@google.com85d3e3a2011-10-07 17:12:00 +0000503 filter_fn(in_line)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000504 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000505
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000506 # Don't put this in a 'finally,' since the child may still run if we get
507 # an exception.
508 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000509
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000510 except KeyboardInterrupt:
511 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
512 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000513
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000514 if rv == 0:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000515 return output.getvalue()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000516 if not retry:
517 break
518 print ("WARNING: subprocess '%s' in %s failed; will retry after a short "
519 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000520 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000521 sleep_interval *= 2
522 raise subprocess2.CalledProcessError(
523 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000524
525
agable@chromium.org5a306a22014-02-24 22:13:59 +0000526class GitFilter(object):
527 """A filter_fn implementation for quieting down git output messages.
528
529 Allows a custom function to skip certain lines (predicate), and will throttle
530 the output of percentage completed lines to only output every X seconds.
531 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000532 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000533
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000534 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000535 """
536 Args:
537 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
538 XX% complete messages) to only be printed at least |time_throttle|
539 seconds apart.
540 predicate (f(line)): An optional function which is invoked for every line.
541 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000542 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000543 """
544 self.last_time = 0
545 self.time_throttle = time_throttle
546 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000547 self.out_fh = out_fh or sys.stdout
548 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000549
550 def __call__(self, line):
551 # git uses an escape sequence to clear the line; elide it.
552 esc = line.find(unichr(033))
553 if esc > -1:
554 line = line[:esc]
555 if self.predicate and not self.predicate(line):
556 return
557 now = time.time()
558 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000559 if match:
560 if match.group(1) != self.progress_prefix:
561 self.progress_prefix = match.group(1)
562 elif now - self.last_time < self.time_throttle:
563 return
564 self.last_time = now
565 self.out_fh.write('[%s] ' % Elapsed())
566 print >> self.out_fh, line
agable@chromium.org5a306a22014-02-24 22:13:59 +0000567
568
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000569def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000570 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000571 real_from_dir = os.path.realpath(from_dir)
572 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000573 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000574 split_path = os.path.split(path)
575 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000576 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000577 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000578
579 # If we did not find the file in the current directory, make sure we are in a
580 # sub directory that is controlled by this configuration.
581 if path != real_from_dir:
582 entries_filename = os.path.join(path, filename + '_entries')
583 if not os.path.exists(entries_filename):
584 # If .gclient_entries does not exist, a previous call to gclient sync
585 # might have failed. In that case, we cannot verify that the .gclient
586 # is the one we want to use. In order to not to cause too much trouble,
587 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000588 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000589 "file you want to use" % (filename, path))
590 return path
591 scope = {}
592 try:
593 exec(FileRead(entries_filename), scope)
594 except SyntaxError, e:
595 SyntaxErrorToError(filename, e)
596 all_directories = scope['entries'].keys()
597 path_to_check = real_from_dir[len(path)+1:]
598 while path_to_check:
599 if path_to_check in all_directories:
600 return path
601 path_to_check = os.path.dirname(path_to_check)
602 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000603
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000604 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000605 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000606
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000607
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000608def PathDifference(root, subpath):
609 """Returns the difference subpath minus root."""
610 root = os.path.realpath(root)
611 subpath = os.path.realpath(subpath)
612 if not subpath.startswith(root):
613 return None
614 # If the root does not have a trailing \ or /, we add it so the returned
615 # path starts immediately after the seperator regardless of whether it is
616 # provided.
617 root = os.path.join(root, '')
618 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000619
620
621def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000622 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000623
rcui@google.com13595ff2011-10-13 01:25:07 +0000624 Returns nearest upper-level directory with the passed in file.
625 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000626 if not path:
627 path = os.getcwd()
628 path = os.path.realpath(path)
629 while True:
630 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000631 if os.path.exists(file_path):
632 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000633 (new_path, _) = os.path.split(path)
634 if new_path == path:
635 return None
636 path = new_path
637
638
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000639def GetMacWinOrLinux():
640 """Returns 'mac', 'win', or 'linux', matching the current platform."""
641 if sys.platform.startswith(('cygwin', 'win')):
642 return 'win'
643 elif sys.platform.startswith('linux'):
644 return 'linux'
645 elif sys.platform == 'darwin':
646 return 'mac'
647 raise Error('Unknown platform: ' + sys.platform)
648
649
650def GetExeSuffix():
651 """Returns '' or '.exe' depending on how executables work on this platform."""
652 if sys.platform.startswith(('cygwin', 'win')):
653 return '.exe'
654 return ''
655
656
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000657def GetGClientRootAndEntries(path=None):
658 """Returns the gclient root and the dict of entries."""
659 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000660 root = FindFileUpwards(config_file, path)
661 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000662 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000663 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000664 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000665 env = {}
666 execfile(config_path, env)
667 config_dir = os.path.dirname(config_path)
668 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000669
670
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000671def lockedmethod(method):
672 """Method decorator that holds self.lock for the duration of the call."""
673 def inner(self, *args, **kwargs):
674 try:
675 try:
676 self.lock.acquire()
677 except KeyboardInterrupt:
678 print >> sys.stderr, 'Was deadlocked'
679 raise
680 return method(self, *args, **kwargs)
681 finally:
682 self.lock.release()
683 return inner
684
685
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000686class WorkItem(object):
687 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000688 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
689 # As a workaround, use a single lock. Yep you read it right. Single lock for
690 # all the 100 objects.
691 lock = threading.Lock()
692
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000693 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000694 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000695 self._name = name
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000696 self.outbuf = cStringIO.StringIO()
697 self.start = self.finish = None
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000698
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000699 def run(self, work_queue):
700 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000701 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000702 pass
703
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000704 @property
705 def name(self):
706 return self._name
707
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000708
709class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000710 """Runs a set of WorkItem that have interdependencies and were WorkItem are
711 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000712
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000713 In gclient's case, Dependencies sometime needs to be run out of order due to
714 From() keyword. This class manages that all the required dependencies are run
715 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000716
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000717 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000718 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000719 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000720 """jobs specifies the number of concurrent tasks to allow. progress is a
721 Progress instance."""
722 # Set when a thread is done or a new item is enqueued.
723 self.ready_cond = threading.Condition()
724 # Maximum number of concurrent tasks.
725 self.jobs = jobs
726 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000727 self.queued = []
728 # List of strings representing each Dependency.name that was run.
729 self.ran = []
730 # List of items currently running.
731 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000732 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000733 self.exceptions = Queue.Queue()
734 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000735 self.progress = progress
736 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000737 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000738
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000739 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000740 self.verbose = verbose
741 self.last_join = None
742 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000743
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000744 def enqueue(self, d):
745 """Enqueue one Dependency to be executed later once its requirements are
746 satisfied.
747 """
748 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000749 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000750 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000751 self.queued.append(d)
752 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000753 logging.debug('enqueued(%s)' % d.name)
754 if self.progress:
755 self.progress._total = total + 1
756 self.progress.update(0)
757 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000758 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000759 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000760
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000761 def out_cb(self, _):
762 self.last_subproc_output = datetime.datetime.now()
763 return True
764
765 @staticmethod
766 def format_task_output(task, comment=''):
767 if comment:
768 comment = ' (%s)' % comment
769 if task.start and task.finish:
770 elapsed = ' (Elapsed: %s)' % (
771 str(task.finish - task.start).partition('.')[0])
772 else:
773 elapsed = ''
774 return """
775%s%s%s
776----------------------------------------
777%s
778----------------------------------------""" % (
779 task.name, comment, task.outbuf.getvalue().strip(), elapsed)
780
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000781 def flush(self, *args, **kwargs):
782 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000783 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000784 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000785 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000786 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000787 while True:
788 # Check for task to run first, then wait.
789 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000790 if not self.exceptions.empty():
791 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000792 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000793 self._flush_terminated_threads()
794 if (not self.queued and not self.running or
795 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000796 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000797 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000798
799 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000800 for i in xrange(len(self.queued)):
801 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000802 if (self.ignore_requirements or
803 not (set(self.queued[i].requirements) - set(self.ran))):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000804 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000805 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000806 break
807 else:
808 # Couldn't find an item that could run. Break out the outher loop.
809 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000810
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000811 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000812 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000813 break
814 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000815 try:
816 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000817 # If we haven't printed to terminal for a while, but we have received
818 # spew from a suprocess, let the user know we're still progressing.
819 now = datetime.datetime.now()
820 if (now - self.last_join > datetime.timedelta(seconds=60) and
821 self.last_subproc_output > self.last_join):
822 if self.progress:
823 print >> sys.stdout, ''
824 elapsed = Elapsed()
825 print >> sys.stdout, '[%s] Still working on:' % elapsed
826 for task in self.running:
827 print >> sys.stdout, '[%s] %s' % (elapsed, task.item.name)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000828 except KeyboardInterrupt:
829 # Help debugging by printing some information:
830 print >> sys.stderr, (
831 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
832 'Running: %d') % (
833 self.jobs,
834 len(self.queued),
835 ', '.join(self.ran),
836 len(self.running)))
837 for i in self.queued:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000838 print >> sys.stderr, '%s (not started): %s' % (
839 i.name, ', '.join(i.requirements))
840 for i in self.running:
841 print >> sys.stderr, self.format_task_output(i.item, 'interrupted')
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000842 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000843 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000844 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000845 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000846
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000847 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000848 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000849 if self.progress:
850 print >> sys.stdout, ''
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000851 # To get back the stack location correctly, the raise a, b, c form must be
852 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000853 e, task = self.exceptions.get()
854 print >> sys.stderr, self.format_task_output(task.item, 'ERROR')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000855 raise e[0], e[1], e[2]
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000856 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000857 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000858
maruel@chromium.org3742c842010-09-09 19:27:14 +0000859 def _flush_terminated_threads(self):
860 """Flush threads that have terminated."""
861 running = self.running
862 self.running = []
863 for t in running:
864 if t.isAlive():
865 self.running.append(t)
866 else:
867 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000868 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000869 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000870 if self.verbose:
871 print >> sys.stdout, self.format_task_output(t.item)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000872 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000873 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000874 if t.item.name in self.ran:
875 raise Error(
876 'gclient is confused, "%s" is already in "%s"' % (
877 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000878 if not t.item.name in self.ran:
879 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000880
881 def _run_one_task(self, task_item, args, kwargs):
882 if self.jobs > 1:
883 # Start the thread.
884 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000885 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000886 self.running.append(new_thread)
887 new_thread.start()
888 else:
889 # Run the 'thread' inside the main thread. Don't try to catch any
890 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000891 try:
892 task_item.start = datetime.datetime.now()
893 print >> task_item.outbuf, '[%s] Started.' % Elapsed(task_item.start)
894 task_item.run(*args, **kwargs)
895 task_item.finish = datetime.datetime.now()
896 print >> task_item.outbuf, '[%s] Finished.' % Elapsed(task_item.finish)
897 self.ran.append(task_item.name)
898 if self.verbose:
899 if self.progress:
900 print >> sys.stdout, ''
901 print >> sys.stdout, self.format_task_output(task_item)
902 if self.progress:
903 self.progress.update(1, ', '.join(t.item.name for t in self.running))
904 except KeyboardInterrupt:
905 print >> sys.stderr, self.format_task_output(task_item, 'interrupted')
906 raise
907 except Exception:
908 print >> sys.stderr, self.format_task_output(task_item, 'ERROR')
909 raise
910
maruel@chromium.org3742c842010-09-09 19:27:14 +0000911
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000912 class _Worker(threading.Thread):
913 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000914 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000915 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000916 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000917 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000918 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000919 self.args = args
920 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000921 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000922
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000923 def run(self):
924 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000925 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000926 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000927 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000928 self.item.start = datetime.datetime.now()
929 print >> self.item.outbuf, '[%s] Started.' % Elapsed(self.item.start)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000930 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000931 self.item.finish = datetime.datetime.now()
932 print >> self.item.outbuf, '[%s] Finished.' % Elapsed(self.item.finish)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000933 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000934 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000935 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000936 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000937 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000938 except Exception:
939 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000940 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000941 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000942 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000943 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000944 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000945 work_queue.ready_cond.acquire()
946 try:
947 work_queue.ready_cond.notifyAll()
948 finally:
949 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000950
951
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000952def GetEditor(git, git_editor=None):
953 """Returns the most plausible editor to use.
954
955 In order of preference:
956 - GIT_EDITOR/SVN_EDITOR environment variable
957 - core.editor git configuration variable (if supplied by git-cl)
958 - VISUAL environment variable
959 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +0000960 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000961
962 In the case of git-cl, this matches git's behaviour, except that it does not
963 include dumb terminal detection.
964
965 In the case of gcl, this matches svn's behaviour, except that it does not
966 accept a command-line flag or check the editor-cmd configuration variable.
967 """
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000968 if git:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000969 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000970 else:
971 editor = os.environ.get('SVN_EDITOR')
972 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000973 editor = os.environ.get('VISUAL')
974 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000975 editor = os.environ.get('EDITOR')
976 if not editor:
977 if sys.platform.startswith('win'):
978 editor = 'notepad'
979 else:
bratell@opera.com65621c72013-12-09 15:05:32 +0000980 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000981 return editor
982
983
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000984def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000985 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +0000986 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000987 # Make sure CRLF is handled properly by requiring none.
988 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000989 print >> sys.stderr, (
990 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000991 fileobj = os.fdopen(file_handle, 'w')
992 # Still remove \r if present.
993 fileobj.write(re.sub('\r?\n', '\n', content))
994 fileobj.close()
995
996 try:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000997 editor = GetEditor(git, git_editor=git_editor)
998 if not editor:
999 return None
1000 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001001 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1002 # Msysgit requires the usage of 'env' to be present.
1003 cmd = 'env ' + cmd
1004 try:
1005 # shell=True to allow the shell to handle all forms of quotes in
1006 # $EDITOR.
1007 subprocess2.check_call(cmd, shell=True)
1008 except subprocess2.CalledProcessError:
1009 return None
1010 return FileRead(filename)
1011 finally:
1012 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001013
1014
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001015def UpgradeToHttps(url):
1016 """Upgrades random urls to https://.
1017
1018 Do not touch unknown urls like ssh:// or git://.
1019 Do not touch http:// urls with a port number,
1020 Fixes invalid GAE url.
1021 """
1022 if not url:
1023 return url
1024 if not re.match(r'[a-z\-]+\://.*', url):
1025 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1026 # relative url and will use http:///foo. Note that it defaults to http://
1027 # for compatibility with naked url like "localhost:8080".
1028 url = 'http://%s' % url
1029 parsed = list(urlparse.urlparse(url))
1030 # Do not automatically upgrade http to https if a port number is provided.
1031 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1032 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001033 return urlparse.urlunparse(parsed)
1034
1035
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001036def ParseCodereviewSettingsContent(content):
1037 """Process a codereview.settings file properly."""
1038 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1039 try:
1040 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1041 except ValueError:
1042 raise Error(
1043 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001044 def fix_url(key):
1045 if keyvals.get(key):
1046 keyvals[key] = UpgradeToHttps(keyvals[key])
1047 fix_url('CODE_REVIEW_SERVER')
1048 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001049 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001050
1051
1052def NumLocalCpus():
1053 """Returns the number of processors.
1054
1055 Python on OSX 10.6 raises a NotImplementedError exception.
1056 """
1057 try:
1058 import multiprocessing
1059 return multiprocessing.cpu_count()
1060 except: # pylint: disable=W0702
1061 # Mac OS 10.6 only
1062 # pylint: disable=E1101
1063 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
szager@chromium.orgfc616382014-03-18 20:32:04 +00001064
1065def DefaultDeltaBaseCacheLimit():
1066 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1067
1068 The primary constraint is the address space of virtual memory. The cache
1069 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1070 parameter is set too high.
1071 """
1072 if platform.architecture()[0].startswith('64'):
1073 return '2g'
1074 else:
1075 return '512m'
1076
szager@chromium.orgff113292014-03-25 06:02:08 +00001077def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001078 """Return reasonable default values for configuring git-index-pack.
1079
1080 Experiments suggest that higher values for pack.threads don't improve
1081 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001082 cache_limit = DefaultDeltaBaseCacheLimit()
1083 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1084 if url in THREADED_INDEX_PACK_BLACKLIST:
1085 result.extend(['-c', 'pack.threads=1'])
1086 return result