blob: 7003fc88676b49e79aba8acbd5f68bdd2cdb7fd3 [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
maruel@chromium.org66c83e62010-09-07 14:18:45 +000030class Error(Exception):
31 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000032 def __init__(self, msg, *args, **kwargs):
33 index = getattr(threading.currentThread(), 'index', 0)
34 if index:
35 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
36 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000037
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000038
msb@chromium.orgac915bb2009-11-13 17:03:01 +000039def SplitUrlRevision(url):
40 """Splits url and returns a two-tuple: url, rev"""
41 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000042 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +000043 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000044 components = re.search(regex, url).groups()
45 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000046 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000047 if len(components) == 1:
48 components += [None]
49 return tuple(components)
50
51
floitsch@google.comeaab7842011-04-28 09:07:58 +000052def IsDateRevision(revision):
53 """Returns true if the given revision is of the form "{ ... }"."""
54 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
55
56
57def MakeDateRevision(date):
58 """Returns a revision representing the latest revision before the given
59 date."""
60 return "{" + date + "}"
61
62
maruel@chromium.org5990f9d2010-07-07 18:02:58 +000063def SyntaxErrorToError(filename, e):
64 """Raises a gclient_utils.Error exception with the human readable message"""
65 try:
66 # Try to construct a human readable error message
67 if filename:
68 error_message = 'There is a syntax error in %s\n' % filename
69 else:
70 error_message = 'There is a syntax error\n'
71 error_message += 'Line #%s, character %s: "%s"' % (
72 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
73 except:
74 # Something went wrong, re-raise the original exception
75 raise e
76 else:
77 raise Error(error_message)
78
79
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000080class PrintableObject(object):
81 def __str__(self):
82 output = ''
83 for i in dir(self):
84 if i.startswith('__'):
85 continue
86 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
87 return output
88
89
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000090def FileRead(filename, mode='rU'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +000091 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +000092 # codecs.open() has different behavior than open() on python 2.6 so use
93 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +000094 s = f.read()
95 try:
96 return s.decode('utf-8')
97 except UnicodeDecodeError:
98 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000099
100
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000101def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000102 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000103 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000104
105
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000106def safe_rename(old, new):
107 """Renames a file reliably.
108
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000109 Sometimes os.rename does not work because a dying git process keeps a handle
110 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000111 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000112 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000113 """
114 # roughly 10s
115 retries = 100
116 for i in range(retries):
117 try:
118 os.rename(old, new)
119 break
120 except OSError:
121 if i == (retries - 1):
122 # Give up.
123 raise
124 # retry
125 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
126 time.sleep(0.1)
127
128
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000129def rmtree(path):
130 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000131
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000132 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000133
134 shutil.rmtree() doesn't work on Windows if any of the files or directories
135 are read-only, which svn repositories and some .svn files are. We need to
136 be able to force the files to be writable (i.e., deletable) as we traverse
137 the tree.
138
139 Even with all this, Windows still sometimes fails to delete a file, citing
140 a permission error (maybe something to do with antivirus scans or disk
141 indexing). The best suggestion any of the user forums had was to wait a
142 bit and try again, so we do that too. It's hand-waving, but sometimes it
143 works. :/
144
145 On POSIX systems, things are a little bit simpler. The modes of the files
146 to be deleted doesn't matter, only the modes of the directories containing
147 them are significant. As the directory tree is traversed, each directory
148 has its mode set appropriately before descending into it. This should
149 result in the entire tree being removed, with the possible exception of
150 *path itself, because nothing attempts to change the mode of its parent.
151 Doing so would be hazardous, as it's not a directory slated for removal.
152 In the ordinary case, this is not a problem: for our purposes, the user
153 will never lack write permission on *path's parent.
154 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000155 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000156 return
157
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000158 if os.path.islink(path) or not os.path.isdir(path):
159 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000160
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000161 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000162 # Give up and use cmd.exe's rd command.
163 path = os.path.normcase(path)
164 for _ in xrange(3):
165 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
166 if exitcode == 0:
167 return
168 else:
169 print >> sys.stderr, 'rd exited with code %d' % exitcode
170 time.sleep(3)
171 raise Exception('Failed to remove path %s' % path)
172
173 # On POSIX systems, we need the x-bit set on the directory to access it,
174 # the r-bit to see its contents, and the w-bit to remove files from it.
175 # The actual modes of the files within the directory is irrelevant.
176 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000177
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000178 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000179 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000180
181 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000182 # If fullpath is a symbolic link that points to a directory, isdir will
183 # be True, but we don't want to descend into that as a directory, we just
184 # want to remove the link. Check islink and treat links as ordinary files
185 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000186 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000187 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000188 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000189 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000190 # Recurse.
191 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000192
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000193 remove(os.rmdir, path)
194
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000195
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000196def safe_makedirs(tree):
197 """Creates the directory in a safe manner.
198
199 Because multiple threads can create these directories concurently, trap the
200 exception and pass on.
201 """
202 count = 0
203 while not os.path.exists(tree):
204 count += 1
205 try:
206 os.makedirs(tree)
207 except OSError, e:
208 # 17 POSIX, 183 Windows
209 if e.errno not in (17, 183):
210 raise
211 if count > 40:
212 # Give up.
213 raise
214
215
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000216def CommandToStr(args):
217 """Converts an arg list into a shell escaped string."""
218 return ' '.join(pipes.quote(arg) for arg in args)
219
220
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000221def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000222 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000223
maruel@chromium.org17d01792010-09-01 18:07:10 +0000224 If |always| is True, a message indicating what is being done
225 is printed to stdout all the time even if not output is generated. Otherwise
226 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000227 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000228 stdout = kwargs.setdefault('stdout', sys.stdout)
229 if header is None:
230 header = "\n________ running '%s' in '%s'\n" % (
ilevy@chromium.org4aad1852013-07-12 21:32:51 +0000231 ' '.join(args), kwargs.get('cwd', '.'))
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000232
maruel@chromium.org17d01792010-09-01 18:07:10 +0000233 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000234 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000235 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000236 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000237 def filter_msg(line):
238 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000239 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000240 elif filter_fn:
241 filter_fn(line)
242 kwargs['filter_fn'] = filter_msg
243 kwargs['call_filter_on_first_line'] = True
244 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000245 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000246 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000247
maruel@chromium.org17d01792010-09-01 18:07:10 +0000248
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000249class Wrapper(object):
250 """Wraps an object, acting as a transparent proxy for all properties by
251 default.
252 """
253 def __init__(self, wrapped):
254 self._wrapped = wrapped
255
256 def __getattr__(self, name):
257 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000258
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000259
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000260class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000261 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000262 def __init__(self, wrapped, delay):
263 super(AutoFlush, self).__init__(wrapped)
264 if not hasattr(self, 'lock'):
265 self.lock = threading.Lock()
266 self.__last_flushed_at = time.time()
267 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000268
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000269 @property
270 def autoflush(self):
271 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000272
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000273 def write(self, out, *args, **kwargs):
274 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000275 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000276 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000277 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000278 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000279 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000280 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000281 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000282 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000283 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000284 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000285
286
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000287class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000288 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000289 threads with a NN> prefix.
290 """
291 def __init__(self, wrapped, include_zero=False):
292 super(Annotated, self).__init__(wrapped)
293 if not hasattr(self, 'lock'):
294 self.lock = threading.Lock()
295 self.__output_buffers = {}
296 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000297
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000298 @property
299 def annotated(self):
300 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000301
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000302 def write(self, out):
303 index = getattr(threading.currentThread(), 'index', 0)
304 if not index and not self.__include_zero:
305 # Unindexed threads aren't buffered.
306 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000307
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000308 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000309 try:
310 # Use a dummy array to hold the string so the code can be lockless.
311 # Strings are immutable, requiring to keep a lock for the whole dictionary
312 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000313 if not index in self.__output_buffers:
314 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000315 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000316 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000317 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000318 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000319
320 # Continue lockless.
321 obj[0] += out
322 while '\n' in obj[0]:
323 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000324 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000325 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000326 obj[0] = remaining
327
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000328 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000329 """Flush buffered output."""
330 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000331 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000332 try:
333 # Detect threads no longer existing.
334 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000335 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000336 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000337 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000338 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000339 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000340 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000341 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000342 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000343
344 # Don't keep the lock while writting. Will append \n when it shouldn't.
345 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000346 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000347 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
348 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000349
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000350
351def MakeFileAutoFlush(fileobj, delay=10):
352 autoflush = getattr(fileobj, 'autoflush', None)
353 if autoflush:
354 autoflush.delay = delay
355 return fileobj
356 return AutoFlush(fileobj, delay)
357
358
359def MakeFileAnnotated(fileobj, include_zero=False):
360 if getattr(fileobj, 'annotated', None):
361 return fileobj
362 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000363
364
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000365GCLIENT_CHILDREN = []
366GCLIENT_CHILDREN_LOCK = threading.Lock()
367
368
369class GClientChildren(object):
370 @staticmethod
371 def add(popen_obj):
372 with GCLIENT_CHILDREN_LOCK:
373 GCLIENT_CHILDREN.append(popen_obj)
374
375 @staticmethod
376 def remove(popen_obj):
377 with GCLIENT_CHILDREN_LOCK:
378 GCLIENT_CHILDREN.remove(popen_obj)
379
380 @staticmethod
381 def _attemptToKillChildren():
382 global GCLIENT_CHILDREN
383 with GCLIENT_CHILDREN_LOCK:
384 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
385
386 for zombie in zombies:
387 try:
388 zombie.kill()
389 except OSError:
390 pass
391
392 with GCLIENT_CHILDREN_LOCK:
393 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
394
395 @staticmethod
396 def _areZombies():
397 with GCLIENT_CHILDREN_LOCK:
398 return bool(GCLIENT_CHILDREN)
399
400 @staticmethod
401 def KillAllRemainingChildren():
402 GClientChildren._attemptToKillChildren()
403
404 if GClientChildren._areZombies():
405 time.sleep(0.5)
406 GClientChildren._attemptToKillChildren()
407
408 with GCLIENT_CHILDREN_LOCK:
409 if GCLIENT_CHILDREN:
410 print >> sys.stderr, 'Could not kill the following subprocesses:'
411 for zombie in GCLIENT_CHILDREN:
412 print >> sys.stderr, ' ', zombie.pid
413
414
maruel@chromium.org17d01792010-09-01 18:07:10 +0000415def CheckCallAndFilter(args, stdout=None, filter_fn=None,
416 print_stdout=None, call_filter_on_first_line=False,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000417 retry=False, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000418 """Runs a command and calls back a filter function if needed.
419
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000420 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000421 print_stdout: If True, the command's stdout is forwarded to stdout.
422 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000423 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000424 character trimmed.
425 stdout: Can be any bufferable output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000426 retry: If the process exits non-zero, sleep for a brief interval and try
427 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000428
429 stderr is always redirected to stdout.
430 """
431 assert print_stdout or filter_fn
432 stdout = stdout or sys.stdout
hinoka@google.com267f33e2014-02-28 22:02:32 +0000433 output = cStringIO.StringIO()
maruel@chromium.org17d01792010-09-01 18:07:10 +0000434 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000435
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000436 sleep_interval = RETRY_INITIAL_SLEEP
437 run_cwd = kwargs.get('cwd', os.getcwd())
438 for _ in xrange(RETRY_MAX + 1):
439 kid = subprocess2.Popen(
440 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
441 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000442
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000443 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000444
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000445 # Do a flush of stdout before we begin reading from the subprocess2's stdout
446 stdout.flush()
447
448 # Also, we need to forward stdout to prevent weird re-ordering of output.
449 # This has to be done on a per byte basis to make sure it is not buffered:
450 # normally buffering is done for each line, but if svn requests input, no
451 # end-of-line character is output after the prompt and it would not show up.
452 try:
453 in_byte = kid.stdout.read(1)
454 if in_byte:
455 if call_filter_on_first_line:
456 filter_fn(None)
457 in_line = ''
458 while in_byte:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000459 output.write(in_byte)
460 if print_stdout:
461 stdout.write(in_byte)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000462 if in_byte != '\r':
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000463 if in_byte != '\n':
464 in_line += in_byte
465 else:
466 filter_fn(in_line)
467 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000468 else:
469 filter_fn(in_line)
470 in_line = ''
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000471 in_byte = kid.stdout.read(1)
472 # Flush the rest of buffered output. This is only an issue with
473 # stdout/stderr not ending with a \n.
474 if len(in_line):
szager@google.com85d3e3a2011-10-07 17:12:00 +0000475 filter_fn(in_line)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000476 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000477
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000478 # Don't put this in a 'finally,' since the child may still run if we get
479 # an exception.
480 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000481
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000482 except KeyboardInterrupt:
483 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
484 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000485
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000486 if rv == 0:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000487 return output.getvalue()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000488 if not retry:
489 break
490 print ("WARNING: subprocess '%s' in %s failed; will retry after a short "
491 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000492 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000493 sleep_interval *= 2
494 raise subprocess2.CalledProcessError(
495 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000496
497
agable@chromium.org5a306a22014-02-24 22:13:59 +0000498class GitFilter(object):
499 """A filter_fn implementation for quieting down git output messages.
500
501 Allows a custom function to skip certain lines (predicate), and will throttle
502 the output of percentage completed lines to only output every X seconds.
503 """
504 PERCENT_RE = re.compile('.* ([0-9]{1,2})% .*')
505
506 def __init__(self, time_throttle=0, predicate=None):
507 """
508 Args:
509 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
510 XX% complete messages) to only be printed at least |time_throttle|
511 seconds apart.
512 predicate (f(line)): An optional function which is invoked for every line.
513 The line will be skipped if predicate(line) returns False.
514 """
515 self.last_time = 0
516 self.time_throttle = time_throttle
517 self.predicate = predicate
518
519 def __call__(self, line):
520 # git uses an escape sequence to clear the line; elide it.
521 esc = line.find(unichr(033))
522 if esc > -1:
523 line = line[:esc]
524 if self.predicate and not self.predicate(line):
525 return
526 now = time.time()
527 match = self.PERCENT_RE.match(line)
528 if not match:
529 self.last_time = 0
530 if (now - self.last_time) >= self.time_throttle:
531 self.last_time = now
532 print line
533
534
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000535def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000536 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000537 real_from_dir = os.path.realpath(from_dir)
538 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000539 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000540 split_path = os.path.split(path)
541 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000542 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000543 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000544
545 # If we did not find the file in the current directory, make sure we are in a
546 # sub directory that is controlled by this configuration.
547 if path != real_from_dir:
548 entries_filename = os.path.join(path, filename + '_entries')
549 if not os.path.exists(entries_filename):
550 # If .gclient_entries does not exist, a previous call to gclient sync
551 # might have failed. In that case, we cannot verify that the .gclient
552 # is the one we want to use. In order to not to cause too much trouble,
553 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000554 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000555 "file you want to use" % (filename, path))
556 return path
557 scope = {}
558 try:
559 exec(FileRead(entries_filename), scope)
560 except SyntaxError, e:
561 SyntaxErrorToError(filename, e)
562 all_directories = scope['entries'].keys()
563 path_to_check = real_from_dir[len(path)+1:]
564 while path_to_check:
565 if path_to_check in all_directories:
566 return path
567 path_to_check = os.path.dirname(path_to_check)
568 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000569
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000570 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000571 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000572
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000573
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000574def PathDifference(root, subpath):
575 """Returns the difference subpath minus root."""
576 root = os.path.realpath(root)
577 subpath = os.path.realpath(subpath)
578 if not subpath.startswith(root):
579 return None
580 # If the root does not have a trailing \ or /, we add it so the returned
581 # path starts immediately after the seperator regardless of whether it is
582 # provided.
583 root = os.path.join(root, '')
584 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000585
586
587def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000588 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000589
rcui@google.com13595ff2011-10-13 01:25:07 +0000590 Returns nearest upper-level directory with the passed in file.
591 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000592 if not path:
593 path = os.getcwd()
594 path = os.path.realpath(path)
595 while True:
596 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000597 if os.path.exists(file_path):
598 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000599 (new_path, _) = os.path.split(path)
600 if new_path == path:
601 return None
602 path = new_path
603
604
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000605def GetMacWinOrLinux():
606 """Returns 'mac', 'win', or 'linux', matching the current platform."""
607 if sys.platform.startswith(('cygwin', 'win')):
608 return 'win'
609 elif sys.platform.startswith('linux'):
610 return 'linux'
611 elif sys.platform == 'darwin':
612 return 'mac'
613 raise Error('Unknown platform: ' + sys.platform)
614
615
616def GetExeSuffix():
617 """Returns '' or '.exe' depending on how executables work on this platform."""
618 if sys.platform.startswith(('cygwin', 'win')):
619 return '.exe'
620 return ''
621
622
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000623def GetGClientRootAndEntries(path=None):
624 """Returns the gclient root and the dict of entries."""
625 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000626 root = FindFileUpwards(config_file, path)
627 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000628 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000629 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000630 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000631 env = {}
632 execfile(config_path, env)
633 config_dir = os.path.dirname(config_path)
634 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000635
636
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000637def lockedmethod(method):
638 """Method decorator that holds self.lock for the duration of the call."""
639 def inner(self, *args, **kwargs):
640 try:
641 try:
642 self.lock.acquire()
643 except KeyboardInterrupt:
644 print >> sys.stderr, 'Was deadlocked'
645 raise
646 return method(self, *args, **kwargs)
647 finally:
648 self.lock.release()
649 return inner
650
651
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000652class WorkItem(object):
653 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000654 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
655 # As a workaround, use a single lock. Yep you read it right. Single lock for
656 # all the 100 objects.
657 lock = threading.Lock()
658
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000659 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000660 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000661 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000662
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000663 def run(self, work_queue):
664 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000665 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000666 pass
667
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000668 @property
669 def name(self):
670 return self._name
671
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000672
673class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000674 """Runs a set of WorkItem that have interdependencies and were WorkItem are
675 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000676
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000677 In gclient's case, Dependencies sometime needs to be run out of order due to
678 From() keyword. This class manages that all the required dependencies are run
679 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000680
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000681 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000682 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000683 def __init__(self, jobs, progress, ignore_requirements):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000684 """jobs specifies the number of concurrent tasks to allow. progress is a
685 Progress instance."""
686 # Set when a thread is done or a new item is enqueued.
687 self.ready_cond = threading.Condition()
688 # Maximum number of concurrent tasks.
689 self.jobs = jobs
690 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000691 self.queued = []
692 # List of strings representing each Dependency.name that was run.
693 self.ran = []
694 # List of items currently running.
695 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000696 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000697 self.exceptions = Queue.Queue()
698 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000699 self.progress = progress
700 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000701 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000702
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000703 self.ignore_requirements = ignore_requirements
704
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000705 def enqueue(self, d):
706 """Enqueue one Dependency to be executed later once its requirements are
707 satisfied.
708 """
709 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000710 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000711 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000712 self.queued.append(d)
713 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000714 logging.debug('enqueued(%s)' % d.name)
715 if self.progress:
716 self.progress._total = total + 1
717 self.progress.update(0)
718 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000719 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000720 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000721
722 def flush(self, *args, **kwargs):
723 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000724 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000725 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000726 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000727 while True:
728 # Check for task to run first, then wait.
729 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000730 if not self.exceptions.empty():
731 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000732 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000733 self._flush_terminated_threads()
734 if (not self.queued and not self.running or
735 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000736 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000737 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000738
739 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000740 for i in xrange(len(self.queued)):
741 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000742 if (self.ignore_requirements or
743 not (set(self.queued[i].requirements) - set(self.ran))):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000744 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000745 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000746 break
747 else:
748 # Couldn't find an item that could run. Break out the outher loop.
749 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000750
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000751 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000752 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000753 break
754 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000755 try:
756 self.ready_cond.wait(10)
757 except KeyboardInterrupt:
758 # Help debugging by printing some information:
759 print >> sys.stderr, (
760 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
761 'Running: %d') % (
762 self.jobs,
763 len(self.queued),
764 ', '.join(self.ran),
765 len(self.running)))
766 for i in self.queued:
767 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
768 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000769 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000770 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000771 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000772
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000773 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000774 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000775 # To get back the stack location correctly, the raise a, b, c form must be
776 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000777 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000778 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000779 if self.progress:
780 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000781
maruel@chromium.org3742c842010-09-09 19:27:14 +0000782 def _flush_terminated_threads(self):
783 """Flush threads that have terminated."""
784 running = self.running
785 self.running = []
786 for t in running:
787 if t.isAlive():
788 self.running.append(t)
789 else:
790 t.join()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000791 sys.stdout.flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000792 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000793 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000794 if t.item.name in self.ran:
795 raise Error(
796 'gclient is confused, "%s" is already in "%s"' % (
797 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000798 if not t.item.name in self.ran:
799 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000800
801 def _run_one_task(self, task_item, args, kwargs):
802 if self.jobs > 1:
803 # Start the thread.
804 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000805 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000806 self.running.append(new_thread)
807 new_thread.start()
808 else:
809 # Run the 'thread' inside the main thread. Don't try to catch any
810 # exception.
811 task_item.run(*args, **kwargs)
812 self.ran.append(task_item.name)
813 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000814 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000815
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000816 class _Worker(threading.Thread):
817 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000818 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000819 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000820 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000821 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000822 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000823 self.args = args
824 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000825 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000826
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000827 def run(self):
828 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000829 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000830 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000831 try:
832 self.item.run(*self.args, **self.kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000833 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000834 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000835 logging.info(str(sys.exc_info()))
836 work_queue.exceptions.put(sys.exc_info())
837 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000838 except Exception:
839 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000840 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000841 logging.info(str(sys.exc_info()))
842 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000843 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000844 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000845 work_queue.ready_cond.acquire()
846 try:
847 work_queue.ready_cond.notifyAll()
848 finally:
849 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000850
851
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000852def GetEditor(git, git_editor=None):
853 """Returns the most plausible editor to use.
854
855 In order of preference:
856 - GIT_EDITOR/SVN_EDITOR environment variable
857 - core.editor git configuration variable (if supplied by git-cl)
858 - VISUAL environment variable
859 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +0000860 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000861
862 In the case of git-cl, this matches git's behaviour, except that it does not
863 include dumb terminal detection.
864
865 In the case of gcl, this matches svn's behaviour, except that it does not
866 accept a command-line flag or check the editor-cmd configuration variable.
867 """
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000868 if git:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000869 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000870 else:
871 editor = os.environ.get('SVN_EDITOR')
872 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000873 editor = os.environ.get('VISUAL')
874 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000875 editor = os.environ.get('EDITOR')
876 if not editor:
877 if sys.platform.startswith('win'):
878 editor = 'notepad'
879 else:
bratell@opera.com65621c72013-12-09 15:05:32 +0000880 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000881 return editor
882
883
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000884def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000885 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +0000886 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000887 # Make sure CRLF is handled properly by requiring none.
888 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000889 print >> sys.stderr, (
890 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000891 fileobj = os.fdopen(file_handle, 'w')
892 # Still remove \r if present.
893 fileobj.write(re.sub('\r?\n', '\n', content))
894 fileobj.close()
895
896 try:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000897 editor = GetEditor(git, git_editor=git_editor)
898 if not editor:
899 return None
900 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000901 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
902 # Msysgit requires the usage of 'env' to be present.
903 cmd = 'env ' + cmd
904 try:
905 # shell=True to allow the shell to handle all forms of quotes in
906 # $EDITOR.
907 subprocess2.check_call(cmd, shell=True)
908 except subprocess2.CalledProcessError:
909 return None
910 return FileRead(filename)
911 finally:
912 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000913
914
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000915def UpgradeToHttps(url):
916 """Upgrades random urls to https://.
917
918 Do not touch unknown urls like ssh:// or git://.
919 Do not touch http:// urls with a port number,
920 Fixes invalid GAE url.
921 """
922 if not url:
923 return url
924 if not re.match(r'[a-z\-]+\://.*', url):
925 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
926 # relative url and will use http:///foo. Note that it defaults to http://
927 # for compatibility with naked url like "localhost:8080".
928 url = 'http://%s' % url
929 parsed = list(urlparse.urlparse(url))
930 # Do not automatically upgrade http to https if a port number is provided.
931 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
932 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000933 return urlparse.urlunparse(parsed)
934
935
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000936def ParseCodereviewSettingsContent(content):
937 """Process a codereview.settings file properly."""
938 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
939 try:
940 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
941 except ValueError:
942 raise Error(
943 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000944 def fix_url(key):
945 if keyvals.get(key):
946 keyvals[key] = UpgradeToHttps(keyvals[key])
947 fix_url('CODE_REVIEW_SERVER')
948 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000949 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +0000950
951
952def NumLocalCpus():
953 """Returns the number of processors.
954
955 Python on OSX 10.6 raises a NotImplementedError exception.
956 """
957 try:
958 import multiprocessing
959 return multiprocessing.cpu_count()
960 except: # pylint: disable=W0702
961 # Mac OS 10.6 only
962 # pylint: disable=E1101
963 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
szager@chromium.orgfc616382014-03-18 20:32:04 +0000964
965def DefaultDeltaBaseCacheLimit():
966 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
967
968 The primary constraint is the address space of virtual memory. The cache
969 size limit is per-thread, and 32-bit systems can hit OOM errors if this
970 parameter is set too high.
971 """
972 if platform.architecture()[0].startswith('64'):
973 return '2g'
974 else:
975 return '512m'
976
977def DefaultIndexPackConfig():
978 """Return reasonable default values for configuring git-index-pack.
979
980 Experiments suggest that higher values for pack.threads don't improve
981 performance."""
982 return ['-c', 'pack.threads=5', '-c',
983 'core.deltaBaseCacheLimit=%s' % DefaultDeltaBaseCacheLimit()]