blob: 47ea50286bf9e2e66347395dd3595966075e0233 [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
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +00008import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00009import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000010import pipes
maruel@chromium.org3742c842010-09-09 19:27:14 +000011import Queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000012import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000013import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000014import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000015import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000016import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000017import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000018import time
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000019import urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000020
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000021import subprocess2
22
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000023
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000024RETRY_MAX = 3
25RETRY_INITIAL_SLEEP = 0.5
26
27
maruel@chromium.org66c83e62010-09-07 14:18:45 +000028class Error(Exception):
29 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000030 def __init__(self, msg, *args, **kwargs):
31 index = getattr(threading.currentThread(), 'index', 0)
32 if index:
33 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
34 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000035
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000036
msb@chromium.orgac915bb2009-11-13 17:03:01 +000037def SplitUrlRevision(url):
38 """Splits url and returns a two-tuple: url, rev"""
39 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000040 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +000041 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000042 components = re.search(regex, url).groups()
43 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000044 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000045 if len(components) == 1:
46 components += [None]
47 return tuple(components)
48
49
floitsch@google.comeaab7842011-04-28 09:07:58 +000050def IsDateRevision(revision):
51 """Returns true if the given revision is of the form "{ ... }"."""
52 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
53
54
55def MakeDateRevision(date):
56 """Returns a revision representing the latest revision before the given
57 date."""
58 return "{" + date + "}"
59
60
maruel@chromium.org5990f9d2010-07-07 18:02:58 +000061def SyntaxErrorToError(filename, e):
62 """Raises a gclient_utils.Error exception with the human readable message"""
63 try:
64 # Try to construct a human readable error message
65 if filename:
66 error_message = 'There is a syntax error in %s\n' % filename
67 else:
68 error_message = 'There is a syntax error\n'
69 error_message += 'Line #%s, character %s: "%s"' % (
70 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
71 except:
72 # Something went wrong, re-raise the original exception
73 raise e
74 else:
75 raise Error(error_message)
76
77
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000078class PrintableObject(object):
79 def __str__(self):
80 output = ''
81 for i in dir(self):
82 if i.startswith('__'):
83 continue
84 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
85 return output
86
87
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000088def FileRead(filename, mode='rU'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +000089 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +000090 # codecs.open() has different behavior than open() on python 2.6 so use
91 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +000092 s = f.read()
93 try:
94 return s.decode('utf-8')
95 except UnicodeDecodeError:
96 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000097
98
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000099def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000100 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000101 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000102
103
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000104def safe_rename(old, new):
105 """Renames a file reliably.
106
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000107 Sometimes os.rename does not work because a dying git process keeps a handle
108 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000109 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000110 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000111 """
112 # roughly 10s
113 retries = 100
114 for i in range(retries):
115 try:
116 os.rename(old, new)
117 break
118 except OSError:
119 if i == (retries - 1):
120 # Give up.
121 raise
122 # retry
123 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
124 time.sleep(0.1)
125
126
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000127def rmtree(path):
128 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000129
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000130 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000131
132 shutil.rmtree() doesn't work on Windows if any of the files or directories
133 are read-only, which svn repositories and some .svn files are. We need to
134 be able to force the files to be writable (i.e., deletable) as we traverse
135 the tree.
136
137 Even with all this, Windows still sometimes fails to delete a file, citing
138 a permission error (maybe something to do with antivirus scans or disk
139 indexing). The best suggestion any of the user forums had was to wait a
140 bit and try again, so we do that too. It's hand-waving, but sometimes it
141 works. :/
142
143 On POSIX systems, things are a little bit simpler. The modes of the files
144 to be deleted doesn't matter, only the modes of the directories containing
145 them are significant. As the directory tree is traversed, each directory
146 has its mode set appropriately before descending into it. This should
147 result in the entire tree being removed, with the possible exception of
148 *path itself, because nothing attempts to change the mode of its parent.
149 Doing so would be hazardous, as it's not a directory slated for removal.
150 In the ordinary case, this is not a problem: for our purposes, the user
151 will never lack write permission on *path's parent.
152 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000153 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000154 return
155
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000156 if os.path.islink(path) or not os.path.isdir(path):
157 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000158
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000159 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000160 # Give up and use cmd.exe's rd command.
161 path = os.path.normcase(path)
162 for _ in xrange(3):
163 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
164 if exitcode == 0:
165 return
166 else:
167 print >> sys.stderr, 'rd exited with code %d' % exitcode
168 time.sleep(3)
169 raise Exception('Failed to remove path %s' % path)
170
171 # On POSIX systems, we need the x-bit set on the directory to access it,
172 # the r-bit to see its contents, and the w-bit to remove files from it.
173 # The actual modes of the files within the directory is irrelevant.
174 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000175
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000176 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000177 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000178
179 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000180 # If fullpath is a symbolic link that points to a directory, isdir will
181 # be True, but we don't want to descend into that as a directory, we just
182 # want to remove the link. Check islink and treat links as ordinary files
183 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000184 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000185 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000186 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000187 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000188 # Recurse.
189 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000190
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000191 remove(os.rmdir, path)
192
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000193
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000194def safe_makedirs(tree):
195 """Creates the directory in a safe manner.
196
197 Because multiple threads can create these directories concurently, trap the
198 exception and pass on.
199 """
200 count = 0
201 while not os.path.exists(tree):
202 count += 1
203 try:
204 os.makedirs(tree)
205 except OSError, e:
206 # 17 POSIX, 183 Windows
207 if e.errno not in (17, 183):
208 raise
209 if count > 40:
210 # Give up.
211 raise
212
213
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000214def CommandToStr(args):
215 """Converts an arg list into a shell escaped string."""
216 return ' '.join(pipes.quote(arg) for arg in args)
217
218
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000219def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000220 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000221
maruel@chromium.org17d01792010-09-01 18:07:10 +0000222 If |always| is True, a message indicating what is being done
223 is printed to stdout all the time even if not output is generated. Otherwise
224 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000225 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000226 stdout = kwargs.setdefault('stdout', sys.stdout)
227 if header is None:
228 header = "\n________ running '%s' in '%s'\n" % (
ilevy@chromium.org4aad1852013-07-12 21:32:51 +0000229 ' '.join(args), kwargs.get('cwd', '.'))
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000230
maruel@chromium.org17d01792010-09-01 18:07:10 +0000231 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000232 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000233 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000234 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000235 def filter_msg(line):
236 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000237 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000238 elif filter_fn:
239 filter_fn(line)
240 kwargs['filter_fn'] = filter_msg
241 kwargs['call_filter_on_first_line'] = True
242 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000243 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000244 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000245
maruel@chromium.org17d01792010-09-01 18:07:10 +0000246
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000247class Wrapper(object):
248 """Wraps an object, acting as a transparent proxy for all properties by
249 default.
250 """
251 def __init__(self, wrapped):
252 self._wrapped = wrapped
253
254 def __getattr__(self, name):
255 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000256
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000257
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000258class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000259 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000260 def __init__(self, wrapped, delay):
261 super(AutoFlush, self).__init__(wrapped)
262 if not hasattr(self, 'lock'):
263 self.lock = threading.Lock()
264 self.__last_flushed_at = time.time()
265 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000266
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000267 @property
268 def autoflush(self):
269 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000270
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000271 def write(self, out, *args, **kwargs):
272 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000273 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000274 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000275 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000276 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000277 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000278 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000279 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000280 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000281 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000282 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000283
284
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000285class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000286 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000287 threads with a NN> prefix.
288 """
289 def __init__(self, wrapped, include_zero=False):
290 super(Annotated, self).__init__(wrapped)
291 if not hasattr(self, 'lock'):
292 self.lock = threading.Lock()
293 self.__output_buffers = {}
294 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000295
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000296 @property
297 def annotated(self):
298 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000299
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000300 def write(self, out):
301 index = getattr(threading.currentThread(), 'index', 0)
302 if not index and not self.__include_zero:
303 # Unindexed threads aren't buffered.
304 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000305
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000306 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000307 try:
308 # Use a dummy array to hold the string so the code can be lockless.
309 # Strings are immutable, requiring to keep a lock for the whole dictionary
310 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000311 if not index in self.__output_buffers:
312 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000313 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000314 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000315 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000316 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000317
318 # Continue lockless.
319 obj[0] += out
320 while '\n' in obj[0]:
321 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000322 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000323 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000324 obj[0] = remaining
325
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000326 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000327 """Flush buffered output."""
328 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000329 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000330 try:
331 # Detect threads no longer existing.
332 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000333 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000334 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000335 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000336 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000337 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000338 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000339 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000340 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000341
342 # Don't keep the lock while writting. Will append \n when it shouldn't.
343 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000344 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000345 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
346 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000347
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000348
349def MakeFileAutoFlush(fileobj, delay=10):
350 autoflush = getattr(fileobj, 'autoflush', None)
351 if autoflush:
352 autoflush.delay = delay
353 return fileobj
354 return AutoFlush(fileobj, delay)
355
356
357def MakeFileAnnotated(fileobj, include_zero=False):
358 if getattr(fileobj, 'annotated', None):
359 return fileobj
360 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000361
362
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000363GCLIENT_CHILDREN = []
364GCLIENT_CHILDREN_LOCK = threading.Lock()
365
366
367class GClientChildren(object):
368 @staticmethod
369 def add(popen_obj):
370 with GCLIENT_CHILDREN_LOCK:
371 GCLIENT_CHILDREN.append(popen_obj)
372
373 @staticmethod
374 def remove(popen_obj):
375 with GCLIENT_CHILDREN_LOCK:
376 GCLIENT_CHILDREN.remove(popen_obj)
377
378 @staticmethod
379 def _attemptToKillChildren():
380 global GCLIENT_CHILDREN
381 with GCLIENT_CHILDREN_LOCK:
382 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
383
384 for zombie in zombies:
385 try:
386 zombie.kill()
387 except OSError:
388 pass
389
390 with GCLIENT_CHILDREN_LOCK:
391 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
392
393 @staticmethod
394 def _areZombies():
395 with GCLIENT_CHILDREN_LOCK:
396 return bool(GCLIENT_CHILDREN)
397
398 @staticmethod
399 def KillAllRemainingChildren():
400 GClientChildren._attemptToKillChildren()
401
402 if GClientChildren._areZombies():
403 time.sleep(0.5)
404 GClientChildren._attemptToKillChildren()
405
406 with GCLIENT_CHILDREN_LOCK:
407 if GCLIENT_CHILDREN:
408 print >> sys.stderr, 'Could not kill the following subprocesses:'
409 for zombie in GCLIENT_CHILDREN:
410 print >> sys.stderr, ' ', zombie.pid
411
412
maruel@chromium.org17d01792010-09-01 18:07:10 +0000413def CheckCallAndFilter(args, stdout=None, filter_fn=None,
414 print_stdout=None, call_filter_on_first_line=False,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000415 retry=False, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000416 """Runs a command and calls back a filter function if needed.
417
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000418 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000419 print_stdout: If True, the command's stdout is forwarded to stdout.
420 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000421 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000422 character trimmed.
423 stdout: Can be any bufferable output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000424 retry: If the process exits non-zero, sleep for a brief interval and try
425 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000426
427 stderr is always redirected to stdout.
428 """
429 assert print_stdout or filter_fn
430 stdout = stdout or sys.stdout
431 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000432
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000433 sleep_interval = RETRY_INITIAL_SLEEP
434 run_cwd = kwargs.get('cwd', os.getcwd())
435 for _ in xrange(RETRY_MAX + 1):
436 kid = subprocess2.Popen(
437 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
438 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000439
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000440 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000441
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000442 # Do a flush of stdout before we begin reading from the subprocess2's stdout
443 stdout.flush()
444
445 # Also, we need to forward stdout to prevent weird re-ordering of output.
446 # This has to be done on a per byte basis to make sure it is not buffered:
447 # normally buffering is done for each line, but if svn requests input, no
448 # end-of-line character is output after the prompt and it would not show up.
449 try:
450 in_byte = kid.stdout.read(1)
451 if in_byte:
452 if call_filter_on_first_line:
453 filter_fn(None)
454 in_line = ''
455 while in_byte:
456 if in_byte != '\r':
457 if print_stdout:
458 stdout.write(in_byte)
459 if in_byte != '\n':
460 in_line += in_byte
461 else:
462 filter_fn(in_line)
463 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000464 else:
465 filter_fn(in_line)
466 in_line = ''
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000467 in_byte = kid.stdout.read(1)
468 # Flush the rest of buffered output. This is only an issue with
469 # stdout/stderr not ending with a \n.
470 if len(in_line):
szager@google.com85d3e3a2011-10-07 17:12:00 +0000471 filter_fn(in_line)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000472 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000473
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000474 # Don't put this in a 'finally,' since the child may still run if we get
475 # an exception.
476 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000477
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000478 except KeyboardInterrupt:
479 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
480 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000481
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000482 if rv == 0:
483 return 0
484 if not retry:
485 break
486 print ("WARNING: subprocess '%s' in %s failed; will retry after a short "
487 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000488 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000489 sleep_interval *= 2
490 raise subprocess2.CalledProcessError(
491 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000492
493
agable@chromium.org5a306a22014-02-24 22:13:59 +0000494class GitFilter(object):
495 """A filter_fn implementation for quieting down git output messages.
496
497 Allows a custom function to skip certain lines (predicate), and will throttle
498 the output of percentage completed lines to only output every X seconds.
499 """
500 PERCENT_RE = re.compile('.* ([0-9]{1,2})% .*')
501
502 def __init__(self, time_throttle=0, predicate=None):
503 """
504 Args:
505 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
506 XX% complete messages) to only be printed at least |time_throttle|
507 seconds apart.
508 predicate (f(line)): An optional function which is invoked for every line.
509 The line will be skipped if predicate(line) returns False.
510 """
511 self.last_time = 0
512 self.time_throttle = time_throttle
513 self.predicate = predicate
514
515 def __call__(self, line):
516 # git uses an escape sequence to clear the line; elide it.
517 esc = line.find(unichr(033))
518 if esc > -1:
519 line = line[:esc]
520 if self.predicate and not self.predicate(line):
521 return
522 now = time.time()
523 match = self.PERCENT_RE.match(line)
524 if not match:
525 self.last_time = 0
526 if (now - self.last_time) >= self.time_throttle:
527 self.last_time = now
528 print line
529
530
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000531def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000532 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000533 real_from_dir = os.path.realpath(from_dir)
534 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000535 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000536 split_path = os.path.split(path)
537 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000538 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000539 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000540
541 # If we did not find the file in the current directory, make sure we are in a
542 # sub directory that is controlled by this configuration.
543 if path != real_from_dir:
544 entries_filename = os.path.join(path, filename + '_entries')
545 if not os.path.exists(entries_filename):
546 # If .gclient_entries does not exist, a previous call to gclient sync
547 # might have failed. In that case, we cannot verify that the .gclient
548 # is the one we want to use. In order to not to cause too much trouble,
549 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000550 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000551 "file you want to use" % (filename, path))
552 return path
553 scope = {}
554 try:
555 exec(FileRead(entries_filename), scope)
556 except SyntaxError, e:
557 SyntaxErrorToError(filename, e)
558 all_directories = scope['entries'].keys()
559 path_to_check = real_from_dir[len(path)+1:]
560 while path_to_check:
561 if path_to_check in all_directories:
562 return path
563 path_to_check = os.path.dirname(path_to_check)
564 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000565
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000566 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000567 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000568
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000569
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000570def PathDifference(root, subpath):
571 """Returns the difference subpath minus root."""
572 root = os.path.realpath(root)
573 subpath = os.path.realpath(subpath)
574 if not subpath.startswith(root):
575 return None
576 # If the root does not have a trailing \ or /, we add it so the returned
577 # path starts immediately after the seperator regardless of whether it is
578 # provided.
579 root = os.path.join(root, '')
580 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000581
582
583def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000584 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000585
rcui@google.com13595ff2011-10-13 01:25:07 +0000586 Returns nearest upper-level directory with the passed in file.
587 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000588 if not path:
589 path = os.getcwd()
590 path = os.path.realpath(path)
591 while True:
592 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000593 if os.path.exists(file_path):
594 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000595 (new_path, _) = os.path.split(path)
596 if new_path == path:
597 return None
598 path = new_path
599
600
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000601def GetMacWinOrLinux():
602 """Returns 'mac', 'win', or 'linux', matching the current platform."""
603 if sys.platform.startswith(('cygwin', 'win')):
604 return 'win'
605 elif sys.platform.startswith('linux'):
606 return 'linux'
607 elif sys.platform == 'darwin':
608 return 'mac'
609 raise Error('Unknown platform: ' + sys.platform)
610
611
612def GetExeSuffix():
613 """Returns '' or '.exe' depending on how executables work on this platform."""
614 if sys.platform.startswith(('cygwin', 'win')):
615 return '.exe'
616 return ''
617
618
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000619def GetGClientRootAndEntries(path=None):
620 """Returns the gclient root and the dict of entries."""
621 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000622 root = FindFileUpwards(config_file, path)
623 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000624 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000625 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000626 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000627 env = {}
628 execfile(config_path, env)
629 config_dir = os.path.dirname(config_path)
630 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000631
632
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000633def lockedmethod(method):
634 """Method decorator that holds self.lock for the duration of the call."""
635 def inner(self, *args, **kwargs):
636 try:
637 try:
638 self.lock.acquire()
639 except KeyboardInterrupt:
640 print >> sys.stderr, 'Was deadlocked'
641 raise
642 return method(self, *args, **kwargs)
643 finally:
644 self.lock.release()
645 return inner
646
647
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000648class WorkItem(object):
649 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000650 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
651 # As a workaround, use a single lock. Yep you read it right. Single lock for
652 # all the 100 objects.
653 lock = threading.Lock()
654
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000655 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000656 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000657 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000658
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000659 def run(self, work_queue):
660 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000661 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000662 pass
663
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000664 @property
665 def name(self):
666 return self._name
667
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000668
669class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000670 """Runs a set of WorkItem that have interdependencies and were WorkItem are
671 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000672
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000673 In gclient's case, Dependencies sometime needs to be run out of order due to
674 From() keyword. This class manages that all the required dependencies are run
675 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000676
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000677 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000678 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000679 def __init__(self, jobs, progress, ignore_requirements):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000680 """jobs specifies the number of concurrent tasks to allow. progress is a
681 Progress instance."""
682 # Set when a thread is done or a new item is enqueued.
683 self.ready_cond = threading.Condition()
684 # Maximum number of concurrent tasks.
685 self.jobs = jobs
686 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000687 self.queued = []
688 # List of strings representing each Dependency.name that was run.
689 self.ran = []
690 # List of items currently running.
691 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000692 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000693 self.exceptions = Queue.Queue()
694 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000695 self.progress = progress
696 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000697 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000698
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000699 self.ignore_requirements = ignore_requirements
700
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000701 def enqueue(self, d):
702 """Enqueue one Dependency to be executed later once its requirements are
703 satisfied.
704 """
705 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000706 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000707 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000708 self.queued.append(d)
709 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000710 logging.debug('enqueued(%s)' % d.name)
711 if self.progress:
712 self.progress._total = total + 1
713 self.progress.update(0)
714 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000715 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000716 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000717
718 def flush(self, *args, **kwargs):
719 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000720 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000721 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000722 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000723 while True:
724 # Check for task to run first, then wait.
725 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000726 if not self.exceptions.empty():
727 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000728 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000729 self._flush_terminated_threads()
730 if (not self.queued and not self.running or
731 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000732 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000733 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000734
735 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000736 for i in xrange(len(self.queued)):
737 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000738 if (self.ignore_requirements or
739 not (set(self.queued[i].requirements) - set(self.ran))):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000740 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000741 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000742 break
743 else:
744 # Couldn't find an item that could run. Break out the outher loop.
745 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000746
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000747 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000748 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000749 break
750 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000751 try:
752 self.ready_cond.wait(10)
753 except KeyboardInterrupt:
754 # Help debugging by printing some information:
755 print >> sys.stderr, (
756 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
757 'Running: %d') % (
758 self.jobs,
759 len(self.queued),
760 ', '.join(self.ran),
761 len(self.running)))
762 for i in self.queued:
763 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
764 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000765 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000766 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000767 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000768
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000769 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000770 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000771 # To get back the stack location correctly, the raise a, b, c form must be
772 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000773 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000774 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000775 if self.progress:
776 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000777
maruel@chromium.org3742c842010-09-09 19:27:14 +0000778 def _flush_terminated_threads(self):
779 """Flush threads that have terminated."""
780 running = self.running
781 self.running = []
782 for t in running:
783 if t.isAlive():
784 self.running.append(t)
785 else:
786 t.join()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000787 sys.stdout.flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000788 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000789 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000790 if t.item.name in self.ran:
791 raise Error(
792 'gclient is confused, "%s" is already in "%s"' % (
793 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000794 if not t.item.name in self.ran:
795 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000796
797 def _run_one_task(self, task_item, args, kwargs):
798 if self.jobs > 1:
799 # Start the thread.
800 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000801 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000802 self.running.append(new_thread)
803 new_thread.start()
804 else:
805 # Run the 'thread' inside the main thread. Don't try to catch any
806 # exception.
807 task_item.run(*args, **kwargs)
808 self.ran.append(task_item.name)
809 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000810 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000811
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000812 class _Worker(threading.Thread):
813 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000814 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000815 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000816 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000817 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000818 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000819 self.args = args
820 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000821 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000822
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000823 def run(self):
824 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000825 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000826 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000827 try:
828 self.item.run(*self.args, **self.kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000829 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000830 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000831 logging.info(str(sys.exc_info()))
832 work_queue.exceptions.put(sys.exc_info())
833 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000834 except Exception:
835 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000836 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000837 logging.info(str(sys.exc_info()))
838 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000839 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000840 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000841 work_queue.ready_cond.acquire()
842 try:
843 work_queue.ready_cond.notifyAll()
844 finally:
845 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000846
847
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000848def GetEditor(git, git_editor=None):
849 """Returns the most plausible editor to use.
850
851 In order of preference:
852 - GIT_EDITOR/SVN_EDITOR environment variable
853 - core.editor git configuration variable (if supplied by git-cl)
854 - VISUAL environment variable
855 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +0000856 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000857
858 In the case of git-cl, this matches git's behaviour, except that it does not
859 include dumb terminal detection.
860
861 In the case of gcl, this matches svn's behaviour, except that it does not
862 accept a command-line flag or check the editor-cmd configuration variable.
863 """
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000864 if git:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000865 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000866 else:
867 editor = os.environ.get('SVN_EDITOR')
868 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000869 editor = os.environ.get('VISUAL')
870 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000871 editor = os.environ.get('EDITOR')
872 if not editor:
873 if sys.platform.startswith('win'):
874 editor = 'notepad'
875 else:
bratell@opera.com65621c72013-12-09 15:05:32 +0000876 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000877 return editor
878
879
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000880def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000881 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +0000882 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000883 # Make sure CRLF is handled properly by requiring none.
884 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000885 print >> sys.stderr, (
886 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000887 fileobj = os.fdopen(file_handle, 'w')
888 # Still remove \r if present.
889 fileobj.write(re.sub('\r?\n', '\n', content))
890 fileobj.close()
891
892 try:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000893 editor = GetEditor(git, git_editor=git_editor)
894 if not editor:
895 return None
896 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000897 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
898 # Msysgit requires the usage of 'env' to be present.
899 cmd = 'env ' + cmd
900 try:
901 # shell=True to allow the shell to handle all forms of quotes in
902 # $EDITOR.
903 subprocess2.check_call(cmd, shell=True)
904 except subprocess2.CalledProcessError:
905 return None
906 return FileRead(filename)
907 finally:
908 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000909
910
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000911def UpgradeToHttps(url):
912 """Upgrades random urls to https://.
913
914 Do not touch unknown urls like ssh:// or git://.
915 Do not touch http:// urls with a port number,
916 Fixes invalid GAE url.
917 """
918 if not url:
919 return url
920 if not re.match(r'[a-z\-]+\://.*', url):
921 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
922 # relative url and will use http:///foo. Note that it defaults to http://
923 # for compatibility with naked url like "localhost:8080".
924 url = 'http://%s' % url
925 parsed = list(urlparse.urlparse(url))
926 # Do not automatically upgrade http to https if a port number is provided.
927 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
928 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000929 return urlparse.urlunparse(parsed)
930
931
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000932def ParseCodereviewSettingsContent(content):
933 """Process a codereview.settings file properly."""
934 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
935 try:
936 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
937 except ValueError:
938 raise Error(
939 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000940 def fix_url(key):
941 if keyvals.get(key):
942 keyvals[key] = UpgradeToHttps(keyvals[key])
943 fix_url('CODE_REVIEW_SERVER')
944 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000945 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +0000946
947
948def NumLocalCpus():
949 """Returns the number of processors.
950
951 Python on OSX 10.6 raises a NotImplementedError exception.
952 """
953 try:
954 import multiprocessing
955 return multiprocessing.cpu_count()
956 except: # pylint: disable=W0702
957 # Mac OS 10.6 only
958 # pylint: disable=E1101
959 return int(os.sysconf('SC_NPROCESSORS_ONLN'))