blob: 0d5714acc00ac5f67407f6ff30dd46c37cc8fabb [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
msb@chromium.orgac915bb2009-11-13 17:03:01 +000036def SplitUrlRevision(url):
37 """Splits url and returns a two-tuple: url, rev"""
38 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000039 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +000040 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000041 components = re.search(regex, url).groups()
42 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000043 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000044 if len(components) == 1:
45 components += [None]
46 return tuple(components)
47
48
floitsch@google.comeaab7842011-04-28 09:07:58 +000049def IsDateRevision(revision):
50 """Returns true if the given revision is of the form "{ ... }"."""
51 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
52
53
54def MakeDateRevision(date):
55 """Returns a revision representing the latest revision before the given
56 date."""
57 return "{" + date + "}"
58
59
maruel@chromium.org5990f9d2010-07-07 18:02:58 +000060def SyntaxErrorToError(filename, e):
61 """Raises a gclient_utils.Error exception with the human readable message"""
62 try:
63 # Try to construct a human readable error message
64 if filename:
65 error_message = 'There is a syntax error in %s\n' % filename
66 else:
67 error_message = 'There is a syntax error\n'
68 error_message += 'Line #%s, character %s: "%s"' % (
69 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
70 except:
71 # Something went wrong, re-raise the original exception
72 raise e
73 else:
74 raise Error(error_message)
75
76
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000077class PrintableObject(object):
78 def __str__(self):
79 output = ''
80 for i in dir(self):
81 if i.startswith('__'):
82 continue
83 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
84 return output
85
86
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000087def FileRead(filename, mode='rU'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +000088 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +000089 # codecs.open() has different behavior than open() on python 2.6 so use
90 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +000091 s = f.read()
92 try:
93 return s.decode('utf-8')
94 except UnicodeDecodeError:
95 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000096
97
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000098def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +000099 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000100 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000101
102
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000103def safe_rename(old, new):
104 """Renames a file reliably.
105
106 Sometimes os.rename does not work because a dying git process keeps a handle
107 on it for a few seconds. An exception is then thrown, which make the program
108 give up what it was doing and remove what was deleted.
109 The only solution is to catch the exception and try again until it works.
110 """
111 # roughly 10s
112 retries = 100
113 for i in range(retries):
114 try:
115 os.rename(old, new)
116 break
117 except OSError:
118 if i == (retries - 1):
119 # Give up.
120 raise
121 # retry
122 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
123 time.sleep(0.1)
124
125
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000126def rmtree(path):
127 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000128
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000129 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000130
131 shutil.rmtree() doesn't work on Windows if any of the files or directories
132 are read-only, which svn repositories and some .svn files are. We need to
133 be able to force the files to be writable (i.e., deletable) as we traverse
134 the tree.
135
136 Even with all this, Windows still sometimes fails to delete a file, citing
137 a permission error (maybe something to do with antivirus scans or disk
138 indexing). The best suggestion any of the user forums had was to wait a
139 bit and try again, so we do that too. It's hand-waving, but sometimes it
140 works. :/
141
142 On POSIX systems, things are a little bit simpler. The modes of the files
143 to be deleted doesn't matter, only the modes of the directories containing
144 them are significant. As the directory tree is traversed, each directory
145 has its mode set appropriately before descending into it. This should
146 result in the entire tree being removed, with the possible exception of
147 *path itself, because nothing attempts to change the mode of its parent.
148 Doing so would be hazardous, as it's not a directory slated for removal.
149 In the ordinary case, this is not a problem: for our purposes, the user
150 will never lack write permission on *path's parent.
151 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000152 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000153 return
154
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000155 if os.path.islink(path) or not os.path.isdir(path):
156 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000157
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000158 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000159 # Give up and use cmd.exe's rd command.
160 path = os.path.normcase(path)
161 for _ in xrange(3):
162 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
163 if exitcode == 0:
164 return
165 else:
166 print >> sys.stderr, 'rd exited with code %d' % exitcode
167 time.sleep(3)
168 raise Exception('Failed to remove path %s' % path)
169
170 # On POSIX systems, we need the x-bit set on the directory to access it,
171 # the r-bit to see its contents, and the w-bit to remove files from it.
172 # The actual modes of the files within the directory is irrelevant.
173 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000174
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000175 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000176 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000177
178 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000179 # If fullpath is a symbolic link that points to a directory, isdir will
180 # be True, but we don't want to descend into that as a directory, we just
181 # want to remove the link. Check islink and treat links as ordinary files
182 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000183 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000184 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000185 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000186 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000187 # Recurse.
188 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000189
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000190 remove(os.rmdir, path)
191
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000192
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000193def safe_makedirs(tree):
194 """Creates the directory in a safe manner.
195
196 Because multiple threads can create these directories concurently, trap the
197 exception and pass on.
198 """
199 count = 0
200 while not os.path.exists(tree):
201 count += 1
202 try:
203 os.makedirs(tree)
204 except OSError, e:
205 # 17 POSIX, 183 Windows
206 if e.errno not in (17, 183):
207 raise
208 if count > 40:
209 # Give up.
210 raise
211
212
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000213def CommandToStr(args):
214 """Converts an arg list into a shell escaped string."""
215 return ' '.join(pipes.quote(arg) for arg in args)
216
217
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000218def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000219 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000220
maruel@chromium.org17d01792010-09-01 18:07:10 +0000221 If |always| is True, a message indicating what is being done
222 is printed to stdout all the time even if not output is generated. Otherwise
223 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000224 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000225 stdout = kwargs.setdefault('stdout', sys.stdout)
226 if header is None:
227 header = "\n________ running '%s' in '%s'\n" % (
ilevy@chromium.org4aad1852013-07-12 21:32:51 +0000228 ' '.join(args), kwargs.get('cwd', '.'))
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000229
maruel@chromium.org17d01792010-09-01 18:07:10 +0000230 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000231 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000232 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000233 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000234 def filter_msg(line):
235 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000236 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000237 elif filter_fn:
238 filter_fn(line)
239 kwargs['filter_fn'] = filter_msg
240 kwargs['call_filter_on_first_line'] = True
241 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000242 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000243 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000244
maruel@chromium.org17d01792010-09-01 18:07:10 +0000245
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000246class Wrapper(object):
247 """Wraps an object, acting as a transparent proxy for all properties by
248 default.
249 """
250 def __init__(self, wrapped):
251 self._wrapped = wrapped
252
253 def __getattr__(self, name):
254 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000255
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000256
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000257class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000258 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000259 def __init__(self, wrapped, delay):
260 super(AutoFlush, self).__init__(wrapped)
261 if not hasattr(self, 'lock'):
262 self.lock = threading.Lock()
263 self.__last_flushed_at = time.time()
264 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000265
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000266 @property
267 def autoflush(self):
268 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000269
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000270 def write(self, out, *args, **kwargs):
271 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000272 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000273 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000274 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000275 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000276 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000277 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000278 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000279 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000280 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000281 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000282
283
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000284class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000285 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000286 threads with a NN> prefix.
287 """
288 def __init__(self, wrapped, include_zero=False):
289 super(Annotated, self).__init__(wrapped)
290 if not hasattr(self, 'lock'):
291 self.lock = threading.Lock()
292 self.__output_buffers = {}
293 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000294
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000295 @property
296 def annotated(self):
297 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000298
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000299 def write(self, out):
300 index = getattr(threading.currentThread(), 'index', 0)
301 if not index and not self.__include_zero:
302 # Unindexed threads aren't buffered.
303 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000304
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000305 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000306 try:
307 # Use a dummy array to hold the string so the code can be lockless.
308 # Strings are immutable, requiring to keep a lock for the whole dictionary
309 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000310 if not index in self.__output_buffers:
311 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000312 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000313 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000314 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000315 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000316
317 # Continue lockless.
318 obj[0] += out
319 while '\n' in obj[0]:
320 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000321 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000322 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000323 obj[0] = remaining
324
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000325 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000326 """Flush buffered output."""
327 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000328 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000329 try:
330 # Detect threads no longer existing.
331 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000332 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000333 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000334 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000335 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000336 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000337 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000338 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000339 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000340
341 # Don't keep the lock while writting. Will append \n when it shouldn't.
342 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000343 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000344 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
345 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000346
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000347
348def MakeFileAutoFlush(fileobj, delay=10):
349 autoflush = getattr(fileobj, 'autoflush', None)
350 if autoflush:
351 autoflush.delay = delay
352 return fileobj
353 return AutoFlush(fileobj, delay)
354
355
356def MakeFileAnnotated(fileobj, include_zero=False):
357 if getattr(fileobj, 'annotated', None):
358 return fileobj
359 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000360
361
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000362GCLIENT_CHILDREN = []
363GCLIENT_CHILDREN_LOCK = threading.Lock()
364
365
366class GClientChildren(object):
367 @staticmethod
368 def add(popen_obj):
369 with GCLIENT_CHILDREN_LOCK:
370 GCLIENT_CHILDREN.append(popen_obj)
371
372 @staticmethod
373 def remove(popen_obj):
374 with GCLIENT_CHILDREN_LOCK:
375 GCLIENT_CHILDREN.remove(popen_obj)
376
377 @staticmethod
378 def _attemptToKillChildren():
379 global GCLIENT_CHILDREN
380 with GCLIENT_CHILDREN_LOCK:
381 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
382
383 for zombie in zombies:
384 try:
385 zombie.kill()
386 except OSError:
387 pass
388
389 with GCLIENT_CHILDREN_LOCK:
390 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
391
392 @staticmethod
393 def _areZombies():
394 with GCLIENT_CHILDREN_LOCK:
395 return bool(GCLIENT_CHILDREN)
396
397 @staticmethod
398 def KillAllRemainingChildren():
399 GClientChildren._attemptToKillChildren()
400
401 if GClientChildren._areZombies():
402 time.sleep(0.5)
403 GClientChildren._attemptToKillChildren()
404
405 with GCLIENT_CHILDREN_LOCK:
406 if GCLIENT_CHILDREN:
407 print >> sys.stderr, 'Could not kill the following subprocesses:'
408 for zombie in GCLIENT_CHILDREN:
409 print >> sys.stderr, ' ', zombie.pid
410
411
maruel@chromium.org17d01792010-09-01 18:07:10 +0000412def CheckCallAndFilter(args, stdout=None, filter_fn=None,
413 print_stdout=None, call_filter_on_first_line=False,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000414 retry=False, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000415 """Runs a command and calls back a filter function if needed.
416
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000417 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000418 print_stdout: If True, the command's stdout is forwarded to stdout.
419 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000420 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000421 character trimmed.
422 stdout: Can be any bufferable output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000423 retry: If the process exits non-zero, sleep for a brief interval and try
424 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000425
426 stderr is always redirected to stdout.
427 """
428 assert print_stdout or filter_fn
429 stdout = stdout or sys.stdout
430 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000431
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000432 sleep_interval = RETRY_INITIAL_SLEEP
433 run_cwd = kwargs.get('cwd', os.getcwd())
434 for _ in xrange(RETRY_MAX + 1):
435 kid = subprocess2.Popen(
436 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
437 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000438
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000439 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000440
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000441 # Do a flush of stdout before we begin reading from the subprocess2's stdout
442 stdout.flush()
443
444 # Also, we need to forward stdout to prevent weird re-ordering of output.
445 # This has to be done on a per byte basis to make sure it is not buffered:
446 # normally buffering is done for each line, but if svn requests input, no
447 # end-of-line character is output after the prompt and it would not show up.
448 try:
449 in_byte = kid.stdout.read(1)
450 if in_byte:
451 if call_filter_on_first_line:
452 filter_fn(None)
453 in_line = ''
454 while in_byte:
455 if in_byte != '\r':
456 if print_stdout:
457 stdout.write(in_byte)
458 if in_byte != '\n':
459 in_line += in_byte
460 else:
461 filter_fn(in_line)
462 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000463 else:
464 filter_fn(in_line)
465 in_line = ''
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000466 in_byte = kid.stdout.read(1)
467 # Flush the rest of buffered output. This is only an issue with
468 # stdout/stderr not ending with a \n.
469 if len(in_line):
szager@google.com85d3e3a2011-10-07 17:12:00 +0000470 filter_fn(in_line)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000471 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000472
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000473 # Don't put this in a 'finally,' since the child may still run if we get
474 # an exception.
475 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000476
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000477 except KeyboardInterrupt:
478 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
479 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000480
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000481 if rv == 0:
482 return 0
483 if not retry:
484 break
485 print ("WARNING: subprocess '%s' in %s failed; will retry after a short "
486 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000487 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000488 sleep_interval *= 2
489 raise subprocess2.CalledProcessError(
490 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000491
492
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000493def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000494 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000495 real_from_dir = os.path.realpath(from_dir)
496 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000497 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000498 split_path = os.path.split(path)
499 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000500 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000501 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000502
503 # If we did not find the file in the current directory, make sure we are in a
504 # sub directory that is controlled by this configuration.
505 if path != real_from_dir:
506 entries_filename = os.path.join(path, filename + '_entries')
507 if not os.path.exists(entries_filename):
508 # If .gclient_entries does not exist, a previous call to gclient sync
509 # might have failed. In that case, we cannot verify that the .gclient
510 # is the one we want to use. In order to not to cause too much trouble,
511 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000512 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000513 "file you want to use" % (filename, path))
514 return path
515 scope = {}
516 try:
517 exec(FileRead(entries_filename), scope)
518 except SyntaxError, e:
519 SyntaxErrorToError(filename, e)
520 all_directories = scope['entries'].keys()
521 path_to_check = real_from_dir[len(path)+1:]
522 while path_to_check:
523 if path_to_check in all_directories:
524 return path
525 path_to_check = os.path.dirname(path_to_check)
526 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000527
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000528 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000529 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000530
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000531
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000532def PathDifference(root, subpath):
533 """Returns the difference subpath minus root."""
534 root = os.path.realpath(root)
535 subpath = os.path.realpath(subpath)
536 if not subpath.startswith(root):
537 return None
538 # If the root does not have a trailing \ or /, we add it so the returned
539 # path starts immediately after the seperator regardless of whether it is
540 # provided.
541 root = os.path.join(root, '')
542 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000543
544
545def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000546 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000547
rcui@google.com13595ff2011-10-13 01:25:07 +0000548 Returns nearest upper-level directory with the passed in file.
549 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000550 if not path:
551 path = os.getcwd()
552 path = os.path.realpath(path)
553 while True:
554 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000555 if os.path.exists(file_path):
556 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000557 (new_path, _) = os.path.split(path)
558 if new_path == path:
559 return None
560 path = new_path
561
562
563def GetGClientRootAndEntries(path=None):
564 """Returns the gclient root and the dict of entries."""
565 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000566 root = FindFileUpwards(config_file, path)
567 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000568 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000569 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000570 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000571 env = {}
572 execfile(config_path, env)
573 config_dir = os.path.dirname(config_path)
574 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000575
576
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000577def lockedmethod(method):
578 """Method decorator that holds self.lock for the duration of the call."""
579 def inner(self, *args, **kwargs):
580 try:
581 try:
582 self.lock.acquire()
583 except KeyboardInterrupt:
584 print >> sys.stderr, 'Was deadlocked'
585 raise
586 return method(self, *args, **kwargs)
587 finally:
588 self.lock.release()
589 return inner
590
591
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000592class WorkItem(object):
593 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000594 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
595 # As a workaround, use a single lock. Yep you read it right. Single lock for
596 # all the 100 objects.
597 lock = threading.Lock()
598
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000599 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000600 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000601 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000602
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000603 def run(self, work_queue):
604 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000605 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000606 pass
607
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000608 @property
609 def name(self):
610 return self._name
611
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000612
613class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000614 """Runs a set of WorkItem that have interdependencies and were WorkItem are
615 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000616
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000617 In gclient's case, Dependencies sometime needs to be run out of order due to
618 From() keyword. This class manages that all the required dependencies are run
619 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000620
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000621 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000622 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000623 def __init__(self, jobs, progress, ignore_requirements):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000624 """jobs specifies the number of concurrent tasks to allow. progress is a
625 Progress instance."""
626 # Set when a thread is done or a new item is enqueued.
627 self.ready_cond = threading.Condition()
628 # Maximum number of concurrent tasks.
629 self.jobs = jobs
630 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000631 self.queued = []
632 # List of strings representing each Dependency.name that was run.
633 self.ran = []
634 # List of items currently running.
635 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000636 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000637 self.exceptions = Queue.Queue()
638 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000639 self.progress = progress
640 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000641 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000642
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000643 self.ignore_requirements = ignore_requirements
644
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000645 def enqueue(self, d):
646 """Enqueue one Dependency to be executed later once its requirements are
647 satisfied.
648 """
649 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000650 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000651 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000652 self.queued.append(d)
653 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000654 logging.debug('enqueued(%s)' % d.name)
655 if self.progress:
656 self.progress._total = total + 1
657 self.progress.update(0)
658 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000659 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000660 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000661
662 def flush(self, *args, **kwargs):
663 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000664 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000665 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000666 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000667 while True:
668 # Check for task to run first, then wait.
669 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000670 if not self.exceptions.empty():
671 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000672 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000673 self._flush_terminated_threads()
674 if (not self.queued and not self.running or
675 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000676 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000677 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000678
679 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000680 for i in xrange(len(self.queued)):
681 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000682 if (self.ignore_requirements or
683 not (set(self.queued[i].requirements) - set(self.ran))):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000684 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000685 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000686 break
687 else:
688 # Couldn't find an item that could run. Break out the outher loop.
689 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000690
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000691 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000692 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000693 break
694 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000695 try:
696 self.ready_cond.wait(10)
697 except KeyboardInterrupt:
698 # Help debugging by printing some information:
699 print >> sys.stderr, (
700 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
701 'Running: %d') % (
702 self.jobs,
703 len(self.queued),
704 ', '.join(self.ran),
705 len(self.running)))
706 for i in self.queued:
707 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
708 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000709 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000710 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000711 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000712
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000713 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000714 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000715 # To get back the stack location correctly, the raise a, b, c form must be
716 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000717 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000718 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000719 if self.progress:
720 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000721
maruel@chromium.org3742c842010-09-09 19:27:14 +0000722 def _flush_terminated_threads(self):
723 """Flush threads that have terminated."""
724 running = self.running
725 self.running = []
726 for t in running:
727 if t.isAlive():
728 self.running.append(t)
729 else:
730 t.join()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000731 sys.stdout.flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000732 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000733 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000734 if t.item.name in self.ran:
735 raise Error(
736 'gclient is confused, "%s" is already in "%s"' % (
737 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000738 if not t.item.name in self.ran:
739 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000740
741 def _run_one_task(self, task_item, args, kwargs):
742 if self.jobs > 1:
743 # Start the thread.
744 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000745 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000746 self.running.append(new_thread)
747 new_thread.start()
748 else:
749 # Run the 'thread' inside the main thread. Don't try to catch any
750 # exception.
751 task_item.run(*args, **kwargs)
752 self.ran.append(task_item.name)
753 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000754 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000755
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000756 class _Worker(threading.Thread):
757 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000758 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000759 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000760 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000761 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000762 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000763 self.args = args
764 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000765 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000766
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000767 def run(self):
768 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000769 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000770 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000771 try:
772 self.item.run(*self.args, **self.kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000773 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000774 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000775 logging.info(str(sys.exc_info()))
776 work_queue.exceptions.put(sys.exc_info())
777 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000778 except Exception:
779 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000780 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000781 logging.info(str(sys.exc_info()))
782 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000783 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000784 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000785 work_queue.ready_cond.acquire()
786 try:
787 work_queue.ready_cond.notifyAll()
788 finally:
789 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000790
791
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000792def GetEditor(git, git_editor=None):
793 """Returns the most plausible editor to use.
794
795 In order of preference:
796 - GIT_EDITOR/SVN_EDITOR environment variable
797 - core.editor git configuration variable (if supplied by git-cl)
798 - VISUAL environment variable
799 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +0000800 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000801
802 In the case of git-cl, this matches git's behaviour, except that it does not
803 include dumb terminal detection.
804
805 In the case of gcl, this matches svn's behaviour, except that it does not
806 accept a command-line flag or check the editor-cmd configuration variable.
807 """
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000808 if git:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000809 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000810 else:
811 editor = os.environ.get('SVN_EDITOR')
812 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000813 editor = os.environ.get('VISUAL')
814 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000815 editor = os.environ.get('EDITOR')
816 if not editor:
817 if sys.platform.startswith('win'):
818 editor = 'notepad'
819 else:
bratell@opera.com65621c72013-12-09 15:05:32 +0000820 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000821 return editor
822
823
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000824def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000825 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +0000826 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000827 # Make sure CRLF is handled properly by requiring none.
828 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000829 print >> sys.stderr, (
830 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000831 fileobj = os.fdopen(file_handle, 'w')
832 # Still remove \r if present.
833 fileobj.write(re.sub('\r?\n', '\n', content))
834 fileobj.close()
835
836 try:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000837 editor = GetEditor(git, git_editor=git_editor)
838 if not editor:
839 return None
840 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000841 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
842 # Msysgit requires the usage of 'env' to be present.
843 cmd = 'env ' + cmd
844 try:
845 # shell=True to allow the shell to handle all forms of quotes in
846 # $EDITOR.
847 subprocess2.check_call(cmd, shell=True)
848 except subprocess2.CalledProcessError:
849 return None
850 return FileRead(filename)
851 finally:
852 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000853
854
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000855def UpgradeToHttps(url):
856 """Upgrades random urls to https://.
857
858 Do not touch unknown urls like ssh:// or git://.
859 Do not touch http:// urls with a port number,
860 Fixes invalid GAE url.
861 """
862 if not url:
863 return url
864 if not re.match(r'[a-z\-]+\://.*', url):
865 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
866 # relative url and will use http:///foo. Note that it defaults to http://
867 # for compatibility with naked url like "localhost:8080".
868 url = 'http://%s' % url
869 parsed = list(urlparse.urlparse(url))
870 # Do not automatically upgrade http to https if a port number is provided.
871 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
872 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000873 return urlparse.urlunparse(parsed)
874
875
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000876def ParseCodereviewSettingsContent(content):
877 """Process a codereview.settings file properly."""
878 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
879 try:
880 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
881 except ValueError:
882 raise Error(
883 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000884 def fix_url(key):
885 if keyvals.get(key):
886 keyvals[key] = UpgradeToHttps(keyvals[key])
887 fix_url('CODE_REVIEW_SERVER')
888 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000889 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +0000890
891
892def NumLocalCpus():
893 """Returns the number of processors.
894
895 Python on OSX 10.6 raises a NotImplementedError exception.
896 """
897 try:
898 import multiprocessing
899 return multiprocessing.cpu_count()
900 except: # pylint: disable=W0702
901 # Mac OS 10.6 only
902 # pylint: disable=E1101
903 return int(os.sysconf('SC_NPROCESSORS_ONLN'))