blob: 1801a6b5723e80cb12dd9f609d727e5c13faf647 [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
maruel@chromium.org3742c842010-09-09 19:27:14 +000012import Queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000013import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000014import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000015import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000016import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000017import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000018import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000019import time
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000020import urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000021
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000022import subprocess2
23
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000024
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000025RETRY_MAX = 3
26RETRY_INITIAL_SLEEP = 0.5
27
28
maruel@chromium.org66c83e62010-09-07 14:18:45 +000029class Error(Exception):
30 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000031 def __init__(self, msg, *args, **kwargs):
32 index = getattr(threading.currentThread(), 'index', 0)
33 if index:
34 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
35 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000036
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000037
msb@chromium.orgac915bb2009-11-13 17:03:01 +000038def SplitUrlRevision(url):
39 """Splits url and returns a two-tuple: url, rev"""
40 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000041 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +000042 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000043 components = re.search(regex, url).groups()
44 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000045 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000046 if len(components) == 1:
47 components += [None]
48 return tuple(components)
49
50
floitsch@google.comeaab7842011-04-28 09:07:58 +000051def IsDateRevision(revision):
52 """Returns true if the given revision is of the form "{ ... }"."""
53 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
54
55
56def MakeDateRevision(date):
57 """Returns a revision representing the latest revision before the given
58 date."""
59 return "{" + date + "}"
60
61
maruel@chromium.org5990f9d2010-07-07 18:02:58 +000062def SyntaxErrorToError(filename, e):
63 """Raises a gclient_utils.Error exception with the human readable message"""
64 try:
65 # Try to construct a human readable error message
66 if filename:
67 error_message = 'There is a syntax error in %s\n' % filename
68 else:
69 error_message = 'There is a syntax error\n'
70 error_message += 'Line #%s, character %s: "%s"' % (
71 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
72 except:
73 # Something went wrong, re-raise the original exception
74 raise e
75 else:
76 raise Error(error_message)
77
78
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000079class PrintableObject(object):
80 def __str__(self):
81 output = ''
82 for i in dir(self):
83 if i.startswith('__'):
84 continue
85 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
86 return output
87
88
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000089def FileRead(filename, mode='rU'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +000090 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +000091 # codecs.open() has different behavior than open() on python 2.6 so use
92 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +000093 s = f.read()
94 try:
95 return s.decode('utf-8')
96 except UnicodeDecodeError:
97 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000098
99
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000100def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000101 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000102 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000103
104
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000105def safe_rename(old, new):
106 """Renames a file reliably.
107
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000108 Sometimes os.rename does not work because a dying git process keeps a handle
109 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000110 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000111 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000112 """
113 # roughly 10s
114 retries = 100
115 for i in range(retries):
116 try:
117 os.rename(old, new)
118 break
119 except OSError:
120 if i == (retries - 1):
121 # Give up.
122 raise
123 # retry
124 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
125 time.sleep(0.1)
126
127
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000128def rmtree(path):
129 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000130
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000131 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000132
133 shutil.rmtree() doesn't work on Windows if any of the files or directories
134 are read-only, which svn repositories and some .svn files are. We need to
135 be able to force the files to be writable (i.e., deletable) as we traverse
136 the tree.
137
138 Even with all this, Windows still sometimes fails to delete a file, citing
139 a permission error (maybe something to do with antivirus scans or disk
140 indexing). The best suggestion any of the user forums had was to wait a
141 bit and try again, so we do that too. It's hand-waving, but sometimes it
142 works. :/
143
144 On POSIX systems, things are a little bit simpler. The modes of the files
145 to be deleted doesn't matter, only the modes of the directories containing
146 them are significant. As the directory tree is traversed, each directory
147 has its mode set appropriately before descending into it. This should
148 result in the entire tree being removed, with the possible exception of
149 *path itself, because nothing attempts to change the mode of its parent.
150 Doing so would be hazardous, as it's not a directory slated for removal.
151 In the ordinary case, this is not a problem: for our purposes, the user
152 will never lack write permission on *path's parent.
153 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000154 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000155 return
156
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000157 if os.path.islink(path) or not os.path.isdir(path):
158 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000159
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000160 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000161 # Give up and use cmd.exe's rd command.
162 path = os.path.normcase(path)
163 for _ in xrange(3):
164 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
165 if exitcode == 0:
166 return
167 else:
168 print >> sys.stderr, 'rd exited with code %d' % exitcode
169 time.sleep(3)
170 raise Exception('Failed to remove path %s' % path)
171
172 # On POSIX systems, we need the x-bit set on the directory to access it,
173 # the r-bit to see its contents, and the w-bit to remove files from it.
174 # The actual modes of the files within the directory is irrelevant.
175 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000176
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000177 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000178 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000179
180 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000181 # If fullpath is a symbolic link that points to a directory, isdir will
182 # be True, but we don't want to descend into that as a directory, we just
183 # want to remove the link. Check islink and treat links as ordinary files
184 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000185 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000186 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000187 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000188 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000189 # Recurse.
190 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000191
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000192 remove(os.rmdir, path)
193
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000194
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000195def safe_makedirs(tree):
196 """Creates the directory in a safe manner.
197
198 Because multiple threads can create these directories concurently, trap the
199 exception and pass on.
200 """
201 count = 0
202 while not os.path.exists(tree):
203 count += 1
204 try:
205 os.makedirs(tree)
206 except OSError, e:
207 # 17 POSIX, 183 Windows
208 if e.errno not in (17, 183):
209 raise
210 if count > 40:
211 # Give up.
212 raise
213
214
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000215def CommandToStr(args):
216 """Converts an arg list into a shell escaped string."""
217 return ' '.join(pipes.quote(arg) for arg in args)
218
219
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000220def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000221 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000222
maruel@chromium.org17d01792010-09-01 18:07:10 +0000223 If |always| is True, a message indicating what is being done
224 is printed to stdout all the time even if not output is generated. Otherwise
225 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000226 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000227 stdout = kwargs.setdefault('stdout', sys.stdout)
228 if header is None:
229 header = "\n________ running '%s' in '%s'\n" % (
ilevy@chromium.org4aad1852013-07-12 21:32:51 +0000230 ' '.join(args), kwargs.get('cwd', '.'))
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000231
maruel@chromium.org17d01792010-09-01 18:07:10 +0000232 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000233 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000234 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000235 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000236 def filter_msg(line):
237 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000238 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000239 elif filter_fn:
240 filter_fn(line)
241 kwargs['filter_fn'] = filter_msg
242 kwargs['call_filter_on_first_line'] = True
243 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000244 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000245 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000246
maruel@chromium.org17d01792010-09-01 18:07:10 +0000247
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000248class Wrapper(object):
249 """Wraps an object, acting as a transparent proxy for all properties by
250 default.
251 """
252 def __init__(self, wrapped):
253 self._wrapped = wrapped
254
255 def __getattr__(self, name):
256 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000257
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000258
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000259class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000260 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000261 def __init__(self, wrapped, delay):
262 super(AutoFlush, self).__init__(wrapped)
263 if not hasattr(self, 'lock'):
264 self.lock = threading.Lock()
265 self.__last_flushed_at = time.time()
266 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000267
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000268 @property
269 def autoflush(self):
270 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000271
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000272 def write(self, out, *args, **kwargs):
273 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000274 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000275 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000276 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000277 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000278 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000279 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000280 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000281 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000282 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000283 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000284
285
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000286class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000287 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000288 threads with a NN> prefix.
289 """
290 def __init__(self, wrapped, include_zero=False):
291 super(Annotated, self).__init__(wrapped)
292 if not hasattr(self, 'lock'):
293 self.lock = threading.Lock()
294 self.__output_buffers = {}
295 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000296
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000297 @property
298 def annotated(self):
299 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000300
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000301 def write(self, out):
302 index = getattr(threading.currentThread(), 'index', 0)
303 if not index and not self.__include_zero:
304 # Unindexed threads aren't buffered.
305 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000306
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000307 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000308 try:
309 # Use a dummy array to hold the string so the code can be lockless.
310 # Strings are immutable, requiring to keep a lock for the whole dictionary
311 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000312 if not index in self.__output_buffers:
313 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000314 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000315 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000316 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000317 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000318
319 # Continue lockless.
320 obj[0] += out
321 while '\n' in obj[0]:
322 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000323 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000324 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000325 obj[0] = remaining
326
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000327 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000328 """Flush buffered output."""
329 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000330 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000331 try:
332 # Detect threads no longer existing.
333 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000334 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000335 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000336 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000337 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000338 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000339 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000340 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000341 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000342
343 # Don't keep the lock while writting. Will append \n when it shouldn't.
344 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000345 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000346 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
347 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000348
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000349
350def MakeFileAutoFlush(fileobj, delay=10):
351 autoflush = getattr(fileobj, 'autoflush', None)
352 if autoflush:
353 autoflush.delay = delay
354 return fileobj
355 return AutoFlush(fileobj, delay)
356
357
358def MakeFileAnnotated(fileobj, include_zero=False):
359 if getattr(fileobj, 'annotated', None):
360 return fileobj
361 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000362
363
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000364GCLIENT_CHILDREN = []
365GCLIENT_CHILDREN_LOCK = threading.Lock()
366
367
368class GClientChildren(object):
369 @staticmethod
370 def add(popen_obj):
371 with GCLIENT_CHILDREN_LOCK:
372 GCLIENT_CHILDREN.append(popen_obj)
373
374 @staticmethod
375 def remove(popen_obj):
376 with GCLIENT_CHILDREN_LOCK:
377 GCLIENT_CHILDREN.remove(popen_obj)
378
379 @staticmethod
380 def _attemptToKillChildren():
381 global GCLIENT_CHILDREN
382 with GCLIENT_CHILDREN_LOCK:
383 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
384
385 for zombie in zombies:
386 try:
387 zombie.kill()
388 except OSError:
389 pass
390
391 with GCLIENT_CHILDREN_LOCK:
392 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
393
394 @staticmethod
395 def _areZombies():
396 with GCLIENT_CHILDREN_LOCK:
397 return bool(GCLIENT_CHILDREN)
398
399 @staticmethod
400 def KillAllRemainingChildren():
401 GClientChildren._attemptToKillChildren()
402
403 if GClientChildren._areZombies():
404 time.sleep(0.5)
405 GClientChildren._attemptToKillChildren()
406
407 with GCLIENT_CHILDREN_LOCK:
408 if GCLIENT_CHILDREN:
409 print >> sys.stderr, 'Could not kill the following subprocesses:'
410 for zombie in GCLIENT_CHILDREN:
411 print >> sys.stderr, ' ', zombie.pid
412
413
maruel@chromium.org17d01792010-09-01 18:07:10 +0000414def CheckCallAndFilter(args, stdout=None, filter_fn=None,
415 print_stdout=None, call_filter_on_first_line=False,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000416 retry=False, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000417 """Runs a command and calls back a filter function if needed.
418
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000419 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000420 print_stdout: If True, the command's stdout is forwarded to stdout.
421 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000422 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000423 character trimmed.
424 stdout: Can be any bufferable output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000425 retry: If the process exits non-zero, sleep for a brief interval and try
426 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000427
428 stderr is always redirected to stdout.
429 """
430 assert print_stdout or filter_fn
431 stdout = stdout or sys.stdout
hinoka@google.com267f33e2014-02-28 22:02:32 +0000432 output = cStringIO.StringIO()
maruel@chromium.org17d01792010-09-01 18:07:10 +0000433 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000434
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000435 sleep_interval = RETRY_INITIAL_SLEEP
436 run_cwd = kwargs.get('cwd', os.getcwd())
437 for _ in xrange(RETRY_MAX + 1):
438 kid = subprocess2.Popen(
439 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
440 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000441
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000442 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000443
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000444 # Do a flush of stdout before we begin reading from the subprocess2's stdout
445 stdout.flush()
446
447 # Also, we need to forward stdout to prevent weird re-ordering of output.
448 # This has to be done on a per byte basis to make sure it is not buffered:
449 # normally buffering is done for each line, but if svn requests input, no
450 # end-of-line character is output after the prompt and it would not show up.
451 try:
452 in_byte = kid.stdout.read(1)
453 if in_byte:
454 if call_filter_on_first_line:
455 filter_fn(None)
456 in_line = ''
457 while in_byte:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000458 output.write(in_byte)
459 if print_stdout:
460 stdout.write(in_byte)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000461 if in_byte != '\r':
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000462 if in_byte != '\n':
463 in_line += in_byte
464 else:
465 filter_fn(in_line)
466 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000467 else:
468 filter_fn(in_line)
469 in_line = ''
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000470 in_byte = kid.stdout.read(1)
471 # Flush the rest of buffered output. This is only an issue with
472 # stdout/stderr not ending with a \n.
473 if len(in_line):
szager@google.com85d3e3a2011-10-07 17:12:00 +0000474 filter_fn(in_line)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000475 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000476
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000477 # Don't put this in a 'finally,' since the child may still run if we get
478 # an exception.
479 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000480
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000481 except KeyboardInterrupt:
482 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
483 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000484
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000485 if rv == 0:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000486 return output.getvalue()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000487 if not retry:
488 break
489 print ("WARNING: subprocess '%s' in %s failed; will retry after a short "
490 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000491 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000492 sleep_interval *= 2
493 raise subprocess2.CalledProcessError(
494 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000495
496
agable@chromium.org5a306a22014-02-24 22:13:59 +0000497class GitFilter(object):
498 """A filter_fn implementation for quieting down git output messages.
499
500 Allows a custom function to skip certain lines (predicate), and will throttle
501 the output of percentage completed lines to only output every X seconds.
502 """
503 PERCENT_RE = re.compile('.* ([0-9]{1,2})% .*')
504
505 def __init__(self, time_throttle=0, predicate=None):
506 """
507 Args:
508 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
509 XX% complete messages) to only be printed at least |time_throttle|
510 seconds apart.
511 predicate (f(line)): An optional function which is invoked for every line.
512 The line will be skipped if predicate(line) returns False.
513 """
514 self.last_time = 0
515 self.time_throttle = time_throttle
516 self.predicate = predicate
517
518 def __call__(self, line):
519 # git uses an escape sequence to clear the line; elide it.
520 esc = line.find(unichr(033))
521 if esc > -1:
522 line = line[:esc]
523 if self.predicate and not self.predicate(line):
524 return
525 now = time.time()
526 match = self.PERCENT_RE.match(line)
527 if not match:
528 self.last_time = 0
529 if (now - self.last_time) >= self.time_throttle:
530 self.last_time = now
531 print line
532
533
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000534def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000535 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000536 real_from_dir = os.path.realpath(from_dir)
537 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000538 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000539 split_path = os.path.split(path)
540 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000541 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000542 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000543
544 # If we did not find the file in the current directory, make sure we are in a
545 # sub directory that is controlled by this configuration.
546 if path != real_from_dir:
547 entries_filename = os.path.join(path, filename + '_entries')
548 if not os.path.exists(entries_filename):
549 # If .gclient_entries does not exist, a previous call to gclient sync
550 # might have failed. In that case, we cannot verify that the .gclient
551 # is the one we want to use. In order to not to cause too much trouble,
552 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000553 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000554 "file you want to use" % (filename, path))
555 return path
556 scope = {}
557 try:
558 exec(FileRead(entries_filename), scope)
559 except SyntaxError, e:
560 SyntaxErrorToError(filename, e)
561 all_directories = scope['entries'].keys()
562 path_to_check = real_from_dir[len(path)+1:]
563 while path_to_check:
564 if path_to_check in all_directories:
565 return path
566 path_to_check = os.path.dirname(path_to_check)
567 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000568
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000569 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000570 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000571
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000572
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000573def PathDifference(root, subpath):
574 """Returns the difference subpath minus root."""
575 root = os.path.realpath(root)
576 subpath = os.path.realpath(subpath)
577 if not subpath.startswith(root):
578 return None
579 # If the root does not have a trailing \ or /, we add it so the returned
580 # path starts immediately after the seperator regardless of whether it is
581 # provided.
582 root = os.path.join(root, '')
583 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000584
585
586def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000587 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000588
rcui@google.com13595ff2011-10-13 01:25:07 +0000589 Returns nearest upper-level directory with the passed in file.
590 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000591 if not path:
592 path = os.getcwd()
593 path = os.path.realpath(path)
594 while True:
595 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000596 if os.path.exists(file_path):
597 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000598 (new_path, _) = os.path.split(path)
599 if new_path == path:
600 return None
601 path = new_path
602
603
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000604def GetMacWinOrLinux():
605 """Returns 'mac', 'win', or 'linux', matching the current platform."""
606 if sys.platform.startswith(('cygwin', 'win')):
607 return 'win'
608 elif sys.platform.startswith('linux'):
609 return 'linux'
610 elif sys.platform == 'darwin':
611 return 'mac'
612 raise Error('Unknown platform: ' + sys.platform)
613
614
615def GetExeSuffix():
616 """Returns '' or '.exe' depending on how executables work on this platform."""
617 if sys.platform.startswith(('cygwin', 'win')):
618 return '.exe'
619 return ''
620
621
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000622def GetGClientRootAndEntries(path=None):
623 """Returns the gclient root and the dict of entries."""
624 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000625 root = FindFileUpwards(config_file, path)
626 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000627 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000628 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000629 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000630 env = {}
631 execfile(config_path, env)
632 config_dir = os.path.dirname(config_path)
633 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000634
635
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000636def lockedmethod(method):
637 """Method decorator that holds self.lock for the duration of the call."""
638 def inner(self, *args, **kwargs):
639 try:
640 try:
641 self.lock.acquire()
642 except KeyboardInterrupt:
643 print >> sys.stderr, 'Was deadlocked'
644 raise
645 return method(self, *args, **kwargs)
646 finally:
647 self.lock.release()
648 return inner
649
650
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000651class WorkItem(object):
652 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000653 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
654 # As a workaround, use a single lock. Yep you read it right. Single lock for
655 # all the 100 objects.
656 lock = threading.Lock()
657
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000658 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000659 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000660 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000661
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000662 def run(self, work_queue):
663 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000664 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000665 pass
666
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000667 @property
668 def name(self):
669 return self._name
670
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000671
672class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000673 """Runs a set of WorkItem that have interdependencies and were WorkItem are
674 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000675
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000676 In gclient's case, Dependencies sometime needs to be run out of order due to
677 From() keyword. This class manages that all the required dependencies are run
678 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000679
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000680 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000681 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000682 def __init__(self, jobs, progress, ignore_requirements):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000683 """jobs specifies the number of concurrent tasks to allow. progress is a
684 Progress instance."""
685 # Set when a thread is done or a new item is enqueued.
686 self.ready_cond = threading.Condition()
687 # Maximum number of concurrent tasks.
688 self.jobs = jobs
689 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000690 self.queued = []
691 # List of strings representing each Dependency.name that was run.
692 self.ran = []
693 # List of items currently running.
694 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000695 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000696 self.exceptions = Queue.Queue()
697 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000698 self.progress = progress
699 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000700 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000701
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000702 self.ignore_requirements = ignore_requirements
703
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000704 def enqueue(self, d):
705 """Enqueue one Dependency to be executed later once its requirements are
706 satisfied.
707 """
708 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000709 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000710 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000711 self.queued.append(d)
712 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000713 logging.debug('enqueued(%s)' % d.name)
714 if self.progress:
715 self.progress._total = total + 1
716 self.progress.update(0)
717 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000718 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000719 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000720
721 def flush(self, *args, **kwargs):
722 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000723 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000724 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000725 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000726 while True:
727 # Check for task to run first, then wait.
728 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000729 if not self.exceptions.empty():
730 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000731 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000732 self._flush_terminated_threads()
733 if (not self.queued and not self.running or
734 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000735 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000736 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000737
738 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000739 for i in xrange(len(self.queued)):
740 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000741 if (self.ignore_requirements or
742 not (set(self.queued[i].requirements) - set(self.ran))):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000743 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000744 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000745 break
746 else:
747 # Couldn't find an item that could run. Break out the outher loop.
748 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000749
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000750 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000751 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000752 break
753 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000754 try:
755 self.ready_cond.wait(10)
756 except KeyboardInterrupt:
757 # Help debugging by printing some information:
758 print >> sys.stderr, (
759 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
760 'Running: %d') % (
761 self.jobs,
762 len(self.queued),
763 ', '.join(self.ran),
764 len(self.running)))
765 for i in self.queued:
766 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
767 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000768 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000769 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000770 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000771
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000772 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000773 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000774 # To get back the stack location correctly, the raise a, b, c form must be
775 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000776 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000777 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000778 if self.progress:
779 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000780
maruel@chromium.org3742c842010-09-09 19:27:14 +0000781 def _flush_terminated_threads(self):
782 """Flush threads that have terminated."""
783 running = self.running
784 self.running = []
785 for t in running:
786 if t.isAlive():
787 self.running.append(t)
788 else:
789 t.join()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000790 sys.stdout.flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000791 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000792 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000793 if t.item.name in self.ran:
794 raise Error(
795 'gclient is confused, "%s" is already in "%s"' % (
796 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000797 if not t.item.name in self.ran:
798 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000799
800 def _run_one_task(self, task_item, args, kwargs):
801 if self.jobs > 1:
802 # Start the thread.
803 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000804 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000805 self.running.append(new_thread)
806 new_thread.start()
807 else:
808 # Run the 'thread' inside the main thread. Don't try to catch any
809 # exception.
810 task_item.run(*args, **kwargs)
811 self.ran.append(task_item.name)
812 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000813 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000814
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000815 class _Worker(threading.Thread):
816 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000817 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000818 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000819 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000820 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000821 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000822 self.args = args
823 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000824 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000825
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000826 def run(self):
827 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000828 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000829 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000830 try:
831 self.item.run(*self.args, **self.kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000832 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000833 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000834 logging.info(str(sys.exc_info()))
835 work_queue.exceptions.put(sys.exc_info())
836 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000837 except Exception:
838 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000839 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000840 logging.info(str(sys.exc_info()))
841 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000842 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000843 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000844 work_queue.ready_cond.acquire()
845 try:
846 work_queue.ready_cond.notifyAll()
847 finally:
848 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000849
850
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000851def GetEditor(git, git_editor=None):
852 """Returns the most plausible editor to use.
853
854 In order of preference:
855 - GIT_EDITOR/SVN_EDITOR environment variable
856 - core.editor git configuration variable (if supplied by git-cl)
857 - VISUAL environment variable
858 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +0000859 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000860
861 In the case of git-cl, this matches git's behaviour, except that it does not
862 include dumb terminal detection.
863
864 In the case of gcl, this matches svn's behaviour, except that it does not
865 accept a command-line flag or check the editor-cmd configuration variable.
866 """
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000867 if git:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000868 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000869 else:
870 editor = os.environ.get('SVN_EDITOR')
871 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000872 editor = os.environ.get('VISUAL')
873 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000874 editor = os.environ.get('EDITOR')
875 if not editor:
876 if sys.platform.startswith('win'):
877 editor = 'notepad'
878 else:
bratell@opera.com65621c72013-12-09 15:05:32 +0000879 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000880 return editor
881
882
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000883def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000884 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +0000885 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000886 # Make sure CRLF is handled properly by requiring none.
887 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000888 print >> sys.stderr, (
889 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000890 fileobj = os.fdopen(file_handle, 'w')
891 # Still remove \r if present.
892 fileobj.write(re.sub('\r?\n', '\n', content))
893 fileobj.close()
894
895 try:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000896 editor = GetEditor(git, git_editor=git_editor)
897 if not editor:
898 return None
899 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000900 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
901 # Msysgit requires the usage of 'env' to be present.
902 cmd = 'env ' + cmd
903 try:
904 # shell=True to allow the shell to handle all forms of quotes in
905 # $EDITOR.
906 subprocess2.check_call(cmd, shell=True)
907 except subprocess2.CalledProcessError:
908 return None
909 return FileRead(filename)
910 finally:
911 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000912
913
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000914def UpgradeToHttps(url):
915 """Upgrades random urls to https://.
916
917 Do not touch unknown urls like ssh:// or git://.
918 Do not touch http:// urls with a port number,
919 Fixes invalid GAE url.
920 """
921 if not url:
922 return url
923 if not re.match(r'[a-z\-]+\://.*', url):
924 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
925 # relative url and will use http:///foo. Note that it defaults to http://
926 # for compatibility with naked url like "localhost:8080".
927 url = 'http://%s' % url
928 parsed = list(urlparse.urlparse(url))
929 # Do not automatically upgrade http to https if a port number is provided.
930 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
931 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000932 return urlparse.urlunparse(parsed)
933
934
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000935def ParseCodereviewSettingsContent(content):
936 """Process a codereview.settings file properly."""
937 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
938 try:
939 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
940 except ValueError:
941 raise Error(
942 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000943 def fix_url(key):
944 if keyvals.get(key):
945 keyvals[key] = UpgradeToHttps(keyvals[key])
946 fix_url('CODE_REVIEW_SERVER')
947 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000948 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +0000949
950
951def NumLocalCpus():
952 """Returns the number of processors.
953
954 Python on OSX 10.6 raises a NotImplementedError exception.
955 """
956 try:
957 import multiprocessing
958 return multiprocessing.cpu_count()
959 except: # pylint: disable=W0702
960 # Mac OS 10.6 only
961 # pylint: disable=E1101
962 return int(os.sysconf('SC_NPROCESSORS_ONLN'))