blob: 44dba57df2f8230c247e3bbf96eee5c58e61448c [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
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +00009import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000010import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000011import pipes
szager@chromium.orgfc616382014-03-18 20:32:04 +000012import platform
maruel@chromium.org3742c842010-09-09 19:27:14 +000013import Queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000014import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000015import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000016import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000017import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000018import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000019import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000020import time
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000021import urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000022
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000023import subprocess2
24
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000025
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000026RETRY_MAX = 3
27RETRY_INITIAL_SLEEP = 0.5
28
29
borenet@google.com6a9b1682014-03-24 18:35:23 +000030_WARNINGS = []
31
32
maruel@chromium.org66c83e62010-09-07 14:18:45 +000033class Error(Exception):
34 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000035 def __init__(self, msg, *args, **kwargs):
36 index = getattr(threading.currentThread(), 'index', 0)
37 if index:
38 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
39 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000040
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000041
borenet@google.com6a9b1682014-03-24 18:35:23 +000042def PrintWarnings():
43 """Prints any accumulated warnings."""
44 if _WARNINGS:
45 print >> sys.stderr, '\n\nWarnings:'
46 for warning in _WARNINGS:
47 print >> sys.stderr, warning
48
49
50def AddWarning(msg):
51 """Adds the given warning message to the list of accumulated warnings."""
52 _WARNINGS.append(msg)
53
54
msb@chromium.orgac915bb2009-11-13 17:03:01 +000055def SplitUrlRevision(url):
56 """Splits url and returns a two-tuple: url, rev"""
57 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000058 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +000059 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000060 components = re.search(regex, url).groups()
61 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000062 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000063 if len(components) == 1:
64 components += [None]
65 return tuple(components)
66
67
floitsch@google.comeaab7842011-04-28 09:07:58 +000068def IsDateRevision(revision):
69 """Returns true if the given revision is of the form "{ ... }"."""
70 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
71
72
73def MakeDateRevision(date):
74 """Returns a revision representing the latest revision before the given
75 date."""
76 return "{" + date + "}"
77
78
maruel@chromium.org5990f9d2010-07-07 18:02:58 +000079def SyntaxErrorToError(filename, e):
80 """Raises a gclient_utils.Error exception with the human readable message"""
81 try:
82 # Try to construct a human readable error message
83 if filename:
84 error_message = 'There is a syntax error in %s\n' % filename
85 else:
86 error_message = 'There is a syntax error\n'
87 error_message += 'Line #%s, character %s: "%s"' % (
88 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
89 except:
90 # Something went wrong, re-raise the original exception
91 raise e
92 else:
93 raise Error(error_message)
94
95
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000096class PrintableObject(object):
97 def __str__(self):
98 output = ''
99 for i in dir(self):
100 if i.startswith('__'):
101 continue
102 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
103 return output
104
105
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000106def FileRead(filename, mode='rU'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000107 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +0000108 # codecs.open() has different behavior than open() on python 2.6 so use
109 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000110 s = f.read()
111 try:
112 return s.decode('utf-8')
113 except UnicodeDecodeError:
114 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000115
116
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000117def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000118 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000119 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000120
121
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000122def safe_rename(old, new):
123 """Renames a file reliably.
124
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000125 Sometimes os.rename does not work because a dying git process keeps a handle
126 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000127 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000128 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000129 """
130 # roughly 10s
131 retries = 100
132 for i in range(retries):
133 try:
134 os.rename(old, new)
135 break
136 except OSError:
137 if i == (retries - 1):
138 # Give up.
139 raise
140 # retry
141 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
142 time.sleep(0.1)
143
144
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000145def rmtree(path):
146 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000147
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000148 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000149
150 shutil.rmtree() doesn't work on Windows if any of the files or directories
151 are read-only, which svn repositories and some .svn files are. We need to
152 be able to force the files to be writable (i.e., deletable) as we traverse
153 the tree.
154
155 Even with all this, Windows still sometimes fails to delete a file, citing
156 a permission error (maybe something to do with antivirus scans or disk
157 indexing). The best suggestion any of the user forums had was to wait a
158 bit and try again, so we do that too. It's hand-waving, but sometimes it
159 works. :/
160
161 On POSIX systems, things are a little bit simpler. The modes of the files
162 to be deleted doesn't matter, only the modes of the directories containing
163 them are significant. As the directory tree is traversed, each directory
164 has its mode set appropriately before descending into it. This should
165 result in the entire tree being removed, with the possible exception of
166 *path itself, because nothing attempts to change the mode of its parent.
167 Doing so would be hazardous, as it's not a directory slated for removal.
168 In the ordinary case, this is not a problem: for our purposes, the user
169 will never lack write permission on *path's parent.
170 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000171 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000172 return
173
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000174 if os.path.islink(path) or not os.path.isdir(path):
175 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000176
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000177 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000178 # Give up and use cmd.exe's rd command.
179 path = os.path.normcase(path)
180 for _ in xrange(3):
181 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
182 if exitcode == 0:
183 return
184 else:
185 print >> sys.stderr, 'rd exited with code %d' % exitcode
186 time.sleep(3)
187 raise Exception('Failed to remove path %s' % path)
188
189 # On POSIX systems, we need the x-bit set on the directory to access it,
190 # the r-bit to see its contents, and the w-bit to remove files from it.
191 # The actual modes of the files within the directory is irrelevant.
192 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000193
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000194 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000195 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000196
197 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000198 # If fullpath is a symbolic link that points to a directory, isdir will
199 # be True, but we don't want to descend into that as a directory, we just
200 # want to remove the link. Check islink and treat links as ordinary files
201 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000202 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000203 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000204 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000205 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000206 # Recurse.
207 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000208
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000209 remove(os.rmdir, path)
210
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000211
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000212def safe_makedirs(tree):
213 """Creates the directory in a safe manner.
214
215 Because multiple threads can create these directories concurently, trap the
216 exception and pass on.
217 """
218 count = 0
219 while not os.path.exists(tree):
220 count += 1
221 try:
222 os.makedirs(tree)
223 except OSError, e:
224 # 17 POSIX, 183 Windows
225 if e.errno not in (17, 183):
226 raise
227 if count > 40:
228 # Give up.
229 raise
230
231
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000232def CommandToStr(args):
233 """Converts an arg list into a shell escaped string."""
234 return ' '.join(pipes.quote(arg) for arg in args)
235
236
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000237def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000238 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000239
maruel@chromium.org17d01792010-09-01 18:07:10 +0000240 If |always| is True, a message indicating what is being done
241 is printed to stdout all the time even if not output is generated. Otherwise
242 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000243 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000244 stdout = kwargs.setdefault('stdout', sys.stdout)
245 if header is None:
246 header = "\n________ running '%s' in '%s'\n" % (
ilevy@chromium.org4aad1852013-07-12 21:32:51 +0000247 ' '.join(args), kwargs.get('cwd', '.'))
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000248
maruel@chromium.org17d01792010-09-01 18:07:10 +0000249 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000250 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000251 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000252 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000253 def filter_msg(line):
254 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000255 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000256 elif filter_fn:
257 filter_fn(line)
258 kwargs['filter_fn'] = filter_msg
259 kwargs['call_filter_on_first_line'] = True
260 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000261 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000262 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000263
maruel@chromium.org17d01792010-09-01 18:07:10 +0000264
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000265class Wrapper(object):
266 """Wraps an object, acting as a transparent proxy for all properties by
267 default.
268 """
269 def __init__(self, wrapped):
270 self._wrapped = wrapped
271
272 def __getattr__(self, name):
273 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000274
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000275
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000276class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000277 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000278 def __init__(self, wrapped, delay):
279 super(AutoFlush, self).__init__(wrapped)
280 if not hasattr(self, 'lock'):
281 self.lock = threading.Lock()
282 self.__last_flushed_at = time.time()
283 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000284
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000285 @property
286 def autoflush(self):
287 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000288
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000289 def write(self, out, *args, **kwargs):
290 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000291 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000292 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000293 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000294 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000295 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000296 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000297 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000298 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000299 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000300 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000301
302
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000303class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000304 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000305 threads with a NN> prefix.
306 """
307 def __init__(self, wrapped, include_zero=False):
308 super(Annotated, self).__init__(wrapped)
309 if not hasattr(self, 'lock'):
310 self.lock = threading.Lock()
311 self.__output_buffers = {}
312 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000313
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000314 @property
315 def annotated(self):
316 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000317
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000318 def write(self, out):
319 index = getattr(threading.currentThread(), 'index', 0)
320 if not index and not self.__include_zero:
321 # Unindexed threads aren't buffered.
322 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000323
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000324 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000325 try:
326 # Use a dummy array to hold the string so the code can be lockless.
327 # Strings are immutable, requiring to keep a lock for the whole dictionary
328 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000329 if not index in self.__output_buffers:
330 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000331 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000332 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000333 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000334 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000335
336 # Continue lockless.
337 obj[0] += out
338 while '\n' in obj[0]:
339 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000340 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000341 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000342 obj[0] = remaining
343
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000344 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000345 """Flush buffered output."""
346 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000347 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000348 try:
349 # Detect threads no longer existing.
350 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000351 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000352 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000353 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000354 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000355 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000356 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000357 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000358 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000359
360 # Don't keep the lock while writting. Will append \n when it shouldn't.
361 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000362 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000363 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
364 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000365
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000366
367def MakeFileAutoFlush(fileobj, delay=10):
368 autoflush = getattr(fileobj, 'autoflush', None)
369 if autoflush:
370 autoflush.delay = delay
371 return fileobj
372 return AutoFlush(fileobj, delay)
373
374
375def MakeFileAnnotated(fileobj, include_zero=False):
376 if getattr(fileobj, 'annotated', None):
377 return fileobj
378 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000379
380
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000381GCLIENT_CHILDREN = []
382GCLIENT_CHILDREN_LOCK = threading.Lock()
383
384
385class GClientChildren(object):
386 @staticmethod
387 def add(popen_obj):
388 with GCLIENT_CHILDREN_LOCK:
389 GCLIENT_CHILDREN.append(popen_obj)
390
391 @staticmethod
392 def remove(popen_obj):
393 with GCLIENT_CHILDREN_LOCK:
394 GCLIENT_CHILDREN.remove(popen_obj)
395
396 @staticmethod
397 def _attemptToKillChildren():
398 global GCLIENT_CHILDREN
399 with GCLIENT_CHILDREN_LOCK:
400 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
401
402 for zombie in zombies:
403 try:
404 zombie.kill()
405 except OSError:
406 pass
407
408 with GCLIENT_CHILDREN_LOCK:
409 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
410
411 @staticmethod
412 def _areZombies():
413 with GCLIENT_CHILDREN_LOCK:
414 return bool(GCLIENT_CHILDREN)
415
416 @staticmethod
417 def KillAllRemainingChildren():
418 GClientChildren._attemptToKillChildren()
419
420 if GClientChildren._areZombies():
421 time.sleep(0.5)
422 GClientChildren._attemptToKillChildren()
423
424 with GCLIENT_CHILDREN_LOCK:
425 if GCLIENT_CHILDREN:
426 print >> sys.stderr, 'Could not kill the following subprocesses:'
427 for zombie in GCLIENT_CHILDREN:
428 print >> sys.stderr, ' ', zombie.pid
429
430
maruel@chromium.org17d01792010-09-01 18:07:10 +0000431def CheckCallAndFilter(args, stdout=None, filter_fn=None,
432 print_stdout=None, call_filter_on_first_line=False,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000433 retry=False, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000434 """Runs a command and calls back a filter function if needed.
435
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000436 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000437 print_stdout: If True, the command's stdout is forwarded to stdout.
438 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000439 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000440 character trimmed.
441 stdout: Can be any bufferable output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000442 retry: If the process exits non-zero, sleep for a brief interval and try
443 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000444
445 stderr is always redirected to stdout.
446 """
447 assert print_stdout or filter_fn
448 stdout = stdout or sys.stdout
hinoka@google.com267f33e2014-02-28 22:02:32 +0000449 output = cStringIO.StringIO()
maruel@chromium.org17d01792010-09-01 18:07:10 +0000450 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000451
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000452 sleep_interval = RETRY_INITIAL_SLEEP
453 run_cwd = kwargs.get('cwd', os.getcwd())
454 for _ in xrange(RETRY_MAX + 1):
455 kid = subprocess2.Popen(
456 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
457 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000458
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000459 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000460
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000461 # Do a flush of stdout before we begin reading from the subprocess2's stdout
462 stdout.flush()
463
464 # Also, we need to forward stdout to prevent weird re-ordering of output.
465 # This has to be done on a per byte basis to make sure it is not buffered:
466 # normally buffering is done for each line, but if svn requests input, no
467 # end-of-line character is output after the prompt and it would not show up.
468 try:
469 in_byte = kid.stdout.read(1)
470 if in_byte:
471 if call_filter_on_first_line:
472 filter_fn(None)
473 in_line = ''
474 while in_byte:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000475 output.write(in_byte)
476 if print_stdout:
477 stdout.write(in_byte)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000478 if in_byte != '\r':
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000479 if in_byte != '\n':
480 in_line += in_byte
481 else:
482 filter_fn(in_line)
483 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000484 else:
485 filter_fn(in_line)
486 in_line = ''
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000487 in_byte = kid.stdout.read(1)
488 # Flush the rest of buffered output. This is only an issue with
489 # stdout/stderr not ending with a \n.
490 if len(in_line):
szager@google.com85d3e3a2011-10-07 17:12:00 +0000491 filter_fn(in_line)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000492 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000493
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000494 # Don't put this in a 'finally,' since the child may still run if we get
495 # an exception.
496 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000497
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000498 except KeyboardInterrupt:
499 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
500 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000501
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000502 if rv == 0:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000503 return output.getvalue()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000504 if not retry:
505 break
506 print ("WARNING: subprocess '%s' in %s failed; will retry after a short "
507 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000508 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000509 sleep_interval *= 2
510 raise subprocess2.CalledProcessError(
511 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000512
513
agable@chromium.org5a306a22014-02-24 22:13:59 +0000514class GitFilter(object):
515 """A filter_fn implementation for quieting down git output messages.
516
517 Allows a custom function to skip certain lines (predicate), and will throttle
518 the output of percentage completed lines to only output every X seconds.
519 """
520 PERCENT_RE = re.compile('.* ([0-9]{1,2})% .*')
521
522 def __init__(self, time_throttle=0, predicate=None):
523 """
524 Args:
525 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
526 XX% complete messages) to only be printed at least |time_throttle|
527 seconds apart.
528 predicate (f(line)): An optional function which is invoked for every line.
529 The line will be skipped if predicate(line) returns False.
530 """
531 self.last_time = 0
532 self.time_throttle = time_throttle
533 self.predicate = predicate
534
535 def __call__(self, line):
536 # git uses an escape sequence to clear the line; elide it.
537 esc = line.find(unichr(033))
538 if esc > -1:
539 line = line[:esc]
540 if self.predicate and not self.predicate(line):
541 return
542 now = time.time()
543 match = self.PERCENT_RE.match(line)
544 if not match:
545 self.last_time = 0
546 if (now - self.last_time) >= self.time_throttle:
547 self.last_time = now
548 print line
549
550
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000551def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000552 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000553 real_from_dir = os.path.realpath(from_dir)
554 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000555 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000556 split_path = os.path.split(path)
557 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000558 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000559 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000560
561 # If we did not find the file in the current directory, make sure we are in a
562 # sub directory that is controlled by this configuration.
563 if path != real_from_dir:
564 entries_filename = os.path.join(path, filename + '_entries')
565 if not os.path.exists(entries_filename):
566 # If .gclient_entries does not exist, a previous call to gclient sync
567 # might have failed. In that case, we cannot verify that the .gclient
568 # is the one we want to use. In order to not to cause too much trouble,
569 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000570 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000571 "file you want to use" % (filename, path))
572 return path
573 scope = {}
574 try:
575 exec(FileRead(entries_filename), scope)
576 except SyntaxError, e:
577 SyntaxErrorToError(filename, e)
578 all_directories = scope['entries'].keys()
579 path_to_check = real_from_dir[len(path)+1:]
580 while path_to_check:
581 if path_to_check in all_directories:
582 return path
583 path_to_check = os.path.dirname(path_to_check)
584 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000585
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000586 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000587 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000588
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000589
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000590def PathDifference(root, subpath):
591 """Returns the difference subpath minus root."""
592 root = os.path.realpath(root)
593 subpath = os.path.realpath(subpath)
594 if not subpath.startswith(root):
595 return None
596 # If the root does not have a trailing \ or /, we add it so the returned
597 # path starts immediately after the seperator regardless of whether it is
598 # provided.
599 root = os.path.join(root, '')
600 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000601
602
603def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000604 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000605
rcui@google.com13595ff2011-10-13 01:25:07 +0000606 Returns nearest upper-level directory with the passed in file.
607 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000608 if not path:
609 path = os.getcwd()
610 path = os.path.realpath(path)
611 while True:
612 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000613 if os.path.exists(file_path):
614 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000615 (new_path, _) = os.path.split(path)
616 if new_path == path:
617 return None
618 path = new_path
619
620
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000621def GetMacWinOrLinux():
622 """Returns 'mac', 'win', or 'linux', matching the current platform."""
623 if sys.platform.startswith(('cygwin', 'win')):
624 return 'win'
625 elif sys.platform.startswith('linux'):
626 return 'linux'
627 elif sys.platform == 'darwin':
628 return 'mac'
629 raise Error('Unknown platform: ' + sys.platform)
630
631
632def GetExeSuffix():
633 """Returns '' or '.exe' depending on how executables work on this platform."""
634 if sys.platform.startswith(('cygwin', 'win')):
635 return '.exe'
636 return ''
637
638
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000639def GetGClientRootAndEntries(path=None):
640 """Returns the gclient root and the dict of entries."""
641 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000642 root = FindFileUpwards(config_file, path)
643 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000644 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000645 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000646 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000647 env = {}
648 execfile(config_path, env)
649 config_dir = os.path.dirname(config_path)
650 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000651
652
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000653def lockedmethod(method):
654 """Method decorator that holds self.lock for the duration of the call."""
655 def inner(self, *args, **kwargs):
656 try:
657 try:
658 self.lock.acquire()
659 except KeyboardInterrupt:
660 print >> sys.stderr, 'Was deadlocked'
661 raise
662 return method(self, *args, **kwargs)
663 finally:
664 self.lock.release()
665 return inner
666
667
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000668class WorkItem(object):
669 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000670 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
671 # As a workaround, use a single lock. Yep you read it right. Single lock for
672 # all the 100 objects.
673 lock = threading.Lock()
674
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000675 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000676 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000677 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000678
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000679 def run(self, work_queue):
680 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000681 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000682 pass
683
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000684 @property
685 def name(self):
686 return self._name
687
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000688
689class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000690 """Runs a set of WorkItem that have interdependencies and were WorkItem are
691 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000692
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000693 In gclient's case, Dependencies sometime needs to be run out of order due to
694 From() keyword. This class manages that all the required dependencies are run
695 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000696
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000697 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000698 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000699 def __init__(self, jobs, progress, ignore_requirements):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000700 """jobs specifies the number of concurrent tasks to allow. progress is a
701 Progress instance."""
702 # Set when a thread is done or a new item is enqueued.
703 self.ready_cond = threading.Condition()
704 # Maximum number of concurrent tasks.
705 self.jobs = jobs
706 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000707 self.queued = []
708 # List of strings representing each Dependency.name that was run.
709 self.ran = []
710 # List of items currently running.
711 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000712 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000713 self.exceptions = Queue.Queue()
714 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000715 self.progress = progress
716 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000717 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000718
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000719 self.ignore_requirements = ignore_requirements
720
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000721 def enqueue(self, d):
722 """Enqueue one Dependency to be executed later once its requirements are
723 satisfied.
724 """
725 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000726 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000727 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000728 self.queued.append(d)
729 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000730 logging.debug('enqueued(%s)' % d.name)
731 if self.progress:
732 self.progress._total = total + 1
733 self.progress.update(0)
734 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000735 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000736 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000737
738 def flush(self, *args, **kwargs):
739 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000740 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000741 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000742 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000743 while True:
744 # Check for task to run first, then wait.
745 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000746 if not self.exceptions.empty():
747 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000748 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000749 self._flush_terminated_threads()
750 if (not self.queued and not self.running or
751 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000752 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000753 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000754
755 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000756 for i in xrange(len(self.queued)):
757 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000758 if (self.ignore_requirements or
759 not (set(self.queued[i].requirements) - set(self.ran))):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000760 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000761 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000762 break
763 else:
764 # Couldn't find an item that could run. Break out the outher loop.
765 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000766
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000767 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000768 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000769 break
770 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000771 try:
772 self.ready_cond.wait(10)
773 except KeyboardInterrupt:
774 # Help debugging by printing some information:
775 print >> sys.stderr, (
776 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
777 'Running: %d') % (
778 self.jobs,
779 len(self.queued),
780 ', '.join(self.ran),
781 len(self.running)))
782 for i in self.queued:
783 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
784 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000785 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000786 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000787 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000788
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000789 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000790 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000791 # To get back the stack location correctly, the raise a, b, c form must be
792 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000793 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000794 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000795 if self.progress:
796 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000797
maruel@chromium.org3742c842010-09-09 19:27:14 +0000798 def _flush_terminated_threads(self):
799 """Flush threads that have terminated."""
800 running = self.running
801 self.running = []
802 for t in running:
803 if t.isAlive():
804 self.running.append(t)
805 else:
806 t.join()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000807 sys.stdout.flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000808 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000809 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000810 if t.item.name in self.ran:
811 raise Error(
812 'gclient is confused, "%s" is already in "%s"' % (
813 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000814 if not t.item.name in self.ran:
815 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000816
817 def _run_one_task(self, task_item, args, kwargs):
818 if self.jobs > 1:
819 # Start the thread.
820 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000821 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000822 self.running.append(new_thread)
823 new_thread.start()
824 else:
825 # Run the 'thread' inside the main thread. Don't try to catch any
826 # exception.
827 task_item.run(*args, **kwargs)
828 self.ran.append(task_item.name)
829 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000830 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000831
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000832 class _Worker(threading.Thread):
833 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000834 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000835 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000836 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000837 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000838 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000839 self.args = args
840 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000841 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000842
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000843 def run(self):
844 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000845 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000846 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000847 try:
848 self.item.run(*self.args, **self.kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000849 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000850 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000851 logging.info(str(sys.exc_info()))
852 work_queue.exceptions.put(sys.exc_info())
853 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000854 except Exception:
855 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000856 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000857 logging.info(str(sys.exc_info()))
858 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000859 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000860 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000861 work_queue.ready_cond.acquire()
862 try:
863 work_queue.ready_cond.notifyAll()
864 finally:
865 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000866
867
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000868def GetEditor(git, git_editor=None):
869 """Returns the most plausible editor to use.
870
871 In order of preference:
872 - GIT_EDITOR/SVN_EDITOR environment variable
873 - core.editor git configuration variable (if supplied by git-cl)
874 - VISUAL environment variable
875 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +0000876 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000877
878 In the case of git-cl, this matches git's behaviour, except that it does not
879 include dumb terminal detection.
880
881 In the case of gcl, this matches svn's behaviour, except that it does not
882 accept a command-line flag or check the editor-cmd configuration variable.
883 """
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000884 if git:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000885 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000886 else:
887 editor = os.environ.get('SVN_EDITOR')
888 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000889 editor = os.environ.get('VISUAL')
890 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000891 editor = os.environ.get('EDITOR')
892 if not editor:
893 if sys.platform.startswith('win'):
894 editor = 'notepad'
895 else:
bratell@opera.com65621c72013-12-09 15:05:32 +0000896 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000897 return editor
898
899
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000900def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000901 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +0000902 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000903 # Make sure CRLF is handled properly by requiring none.
904 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000905 print >> sys.stderr, (
906 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000907 fileobj = os.fdopen(file_handle, 'w')
908 # Still remove \r if present.
909 fileobj.write(re.sub('\r?\n', '\n', content))
910 fileobj.close()
911
912 try:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000913 editor = GetEditor(git, git_editor=git_editor)
914 if not editor:
915 return None
916 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000917 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
918 # Msysgit requires the usage of 'env' to be present.
919 cmd = 'env ' + cmd
920 try:
921 # shell=True to allow the shell to handle all forms of quotes in
922 # $EDITOR.
923 subprocess2.check_call(cmd, shell=True)
924 except subprocess2.CalledProcessError:
925 return None
926 return FileRead(filename)
927 finally:
928 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000929
930
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000931def UpgradeToHttps(url):
932 """Upgrades random urls to https://.
933
934 Do not touch unknown urls like ssh:// or git://.
935 Do not touch http:// urls with a port number,
936 Fixes invalid GAE url.
937 """
938 if not url:
939 return url
940 if not re.match(r'[a-z\-]+\://.*', url):
941 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
942 # relative url and will use http:///foo. Note that it defaults to http://
943 # for compatibility with naked url like "localhost:8080".
944 url = 'http://%s' % url
945 parsed = list(urlparse.urlparse(url))
946 # Do not automatically upgrade http to https if a port number is provided.
947 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
948 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000949 return urlparse.urlunparse(parsed)
950
951
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000952def ParseCodereviewSettingsContent(content):
953 """Process a codereview.settings file properly."""
954 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
955 try:
956 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
957 except ValueError:
958 raise Error(
959 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000960 def fix_url(key):
961 if keyvals.get(key):
962 keyvals[key] = UpgradeToHttps(keyvals[key])
963 fix_url('CODE_REVIEW_SERVER')
964 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000965 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +0000966
967
968def NumLocalCpus():
969 """Returns the number of processors.
970
971 Python on OSX 10.6 raises a NotImplementedError exception.
972 """
973 try:
974 import multiprocessing
975 return multiprocessing.cpu_count()
976 except: # pylint: disable=W0702
977 # Mac OS 10.6 only
978 # pylint: disable=E1101
979 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
szager@chromium.orgfc616382014-03-18 20:32:04 +0000980
981def DefaultDeltaBaseCacheLimit():
982 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
983
984 The primary constraint is the address space of virtual memory. The cache
985 size limit is per-thread, and 32-bit systems can hit OOM errors if this
986 parameter is set too high.
987 """
988 if platform.architecture()[0].startswith('64'):
989 return '2g'
990 else:
991 return '512m'
992
993def DefaultIndexPackConfig():
994 """Return reasonable default values for configuring git-index-pack.
995
996 Experiments suggest that higher values for pack.threads don't improve
997 performance."""
998 return ['-c', 'pack.threads=5', '-c',
999 'core.deltaBaseCacheLimit=%s' % DefaultDeltaBaseCacheLimit()]