blob: e3c504a9da614fd3dfaee4f7483d5645311188af [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
maruel@chromium.org66c83e62010-09-07 14:18:45 +000024class Error(Exception):
25 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000026 def __init__(self, msg, *args, **kwargs):
27 index = getattr(threading.currentThread(), 'index', 0)
28 if index:
29 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
30 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000031
msb@chromium.orgac915bb2009-11-13 17:03:01 +000032def SplitUrlRevision(url):
33 """Splits url and returns a two-tuple: url, rev"""
34 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000035 # Make sure ssh://user-name@example.com/~/test.git@stable works
36 regex = r'(ssh://(?:[-\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000037 components = re.search(regex, url).groups()
38 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000039 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000040 if len(components) == 1:
41 components += [None]
42 return tuple(components)
43
44
floitsch@google.comeaab7842011-04-28 09:07:58 +000045def IsDateRevision(revision):
46 """Returns true if the given revision is of the form "{ ... }"."""
47 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
48
49
50def MakeDateRevision(date):
51 """Returns a revision representing the latest revision before the given
52 date."""
53 return "{" + date + "}"
54
55
maruel@chromium.org5990f9d2010-07-07 18:02:58 +000056def SyntaxErrorToError(filename, e):
57 """Raises a gclient_utils.Error exception with the human readable message"""
58 try:
59 # Try to construct a human readable error message
60 if filename:
61 error_message = 'There is a syntax error in %s\n' % filename
62 else:
63 error_message = 'There is a syntax error\n'
64 error_message += 'Line #%s, character %s: "%s"' % (
65 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
66 except:
67 # Something went wrong, re-raise the original exception
68 raise e
69 else:
70 raise Error(error_message)
71
72
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000073class PrintableObject(object):
74 def __str__(self):
75 output = ''
76 for i in dir(self):
77 if i.startswith('__'):
78 continue
79 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
80 return output
81
82
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000083def FileRead(filename, mode='rU'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +000084 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +000085 # codecs.open() has different behavior than open() on python 2.6 so use
86 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +000087 s = f.read()
88 try:
89 return s.decode('utf-8')
90 except UnicodeDecodeError:
91 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000092
93
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000094def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +000095 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000096 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000097
98
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +000099def safe_rename(old, new):
100 """Renames a file reliably.
101
102 Sometimes os.rename does not work because a dying git process keeps a handle
103 on it for a few seconds. An exception is then thrown, which make the program
104 give up what it was doing and remove what was deleted.
105 The only solution is to catch the exception and try again until it works.
106 """
107 # roughly 10s
108 retries = 100
109 for i in range(retries):
110 try:
111 os.rename(old, new)
112 break
113 except OSError:
114 if i == (retries - 1):
115 # Give up.
116 raise
117 # retry
118 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
119 time.sleep(0.1)
120
121
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000122def rmtree(path):
123 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000124
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000125 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000126
127 shutil.rmtree() doesn't work on Windows if any of the files or directories
128 are read-only, which svn repositories and some .svn files are. We need to
129 be able to force the files to be writable (i.e., deletable) as we traverse
130 the tree.
131
132 Even with all this, Windows still sometimes fails to delete a file, citing
133 a permission error (maybe something to do with antivirus scans or disk
134 indexing). The best suggestion any of the user forums had was to wait a
135 bit and try again, so we do that too. It's hand-waving, but sometimes it
136 works. :/
137
138 On POSIX systems, things are a little bit simpler. The modes of the files
139 to be deleted doesn't matter, only the modes of the directories containing
140 them are significant. As the directory tree is traversed, each directory
141 has its mode set appropriately before descending into it. This should
142 result in the entire tree being removed, with the possible exception of
143 *path itself, because nothing attempts to change the mode of its parent.
144 Doing so would be hazardous, as it's not a directory slated for removal.
145 In the ordinary case, this is not a problem: for our purposes, the user
146 will never lack write permission on *path's parent.
147 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000148 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000149 return
150
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000151 if os.path.islink(path) or not os.path.isdir(path):
152 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000153
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000154 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000155 # Give up and use cmd.exe's rd command.
156 path = os.path.normcase(path)
157 for _ in xrange(3):
158 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
159 if exitcode == 0:
160 return
161 else:
162 print >> sys.stderr, 'rd exited with code %d' % exitcode
163 time.sleep(3)
164 raise Exception('Failed to remove path %s' % path)
165
166 # On POSIX systems, we need the x-bit set on the directory to access it,
167 # the r-bit to see its contents, and the w-bit to remove files from it.
168 # The actual modes of the files within the directory is irrelevant.
169 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000170
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000171 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000172 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000173
174 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000175 # If fullpath is a symbolic link that points to a directory, isdir will
176 # be True, but we don't want to descend into that as a directory, we just
177 # want to remove the link. Check islink and treat links as ordinary files
178 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000179 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000180 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000181 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000182 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000183 # Recurse.
184 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000185
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000186 remove(os.rmdir, path)
187
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000188
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000189def safe_makedirs(tree):
190 """Creates the directory in a safe manner.
191
192 Because multiple threads can create these directories concurently, trap the
193 exception and pass on.
194 """
195 count = 0
196 while not os.path.exists(tree):
197 count += 1
198 try:
199 os.makedirs(tree)
200 except OSError, e:
201 # 17 POSIX, 183 Windows
202 if e.errno not in (17, 183):
203 raise
204 if count > 40:
205 # Give up.
206 raise
207
208
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000209def CommandToStr(args):
210 """Converts an arg list into a shell escaped string."""
211 return ' '.join(pipes.quote(arg) for arg in args)
212
213
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000214def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000215 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000216
maruel@chromium.org17d01792010-09-01 18:07:10 +0000217 If |always| is True, a message indicating what is being done
218 is printed to stdout all the time even if not output is generated. Otherwise
219 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000220 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000221 stdout = kwargs.setdefault('stdout', sys.stdout)
222 if header is None:
223 header = "\n________ running '%s' in '%s'\n" % (
ilevy@chromium.org4aad1852013-07-12 21:32:51 +0000224 ' '.join(args), kwargs.get('cwd', '.'))
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000225
maruel@chromium.org17d01792010-09-01 18:07:10 +0000226 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000227 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000228 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000229 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000230 def filter_msg(line):
231 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000232 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000233 elif filter_fn:
234 filter_fn(line)
235 kwargs['filter_fn'] = filter_msg
236 kwargs['call_filter_on_first_line'] = True
237 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000238 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000239 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000240
maruel@chromium.org17d01792010-09-01 18:07:10 +0000241
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000242class Wrapper(object):
243 """Wraps an object, acting as a transparent proxy for all properties by
244 default.
245 """
246 def __init__(self, wrapped):
247 self._wrapped = wrapped
248
249 def __getattr__(self, name):
250 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000251
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000252
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000253class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000254 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000255 def __init__(self, wrapped, delay):
256 super(AutoFlush, self).__init__(wrapped)
257 if not hasattr(self, 'lock'):
258 self.lock = threading.Lock()
259 self.__last_flushed_at = time.time()
260 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000261
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000262 @property
263 def autoflush(self):
264 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000265
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000266 def write(self, out, *args, **kwargs):
267 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000268 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000269 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000270 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000271 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000272 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000273 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000274 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000275 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000276 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000277 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000278
279
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000280class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000281 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000282 threads with a NN> prefix.
283 """
284 def __init__(self, wrapped, include_zero=False):
285 super(Annotated, self).__init__(wrapped)
286 if not hasattr(self, 'lock'):
287 self.lock = threading.Lock()
288 self.__output_buffers = {}
289 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000290
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000291 @property
292 def annotated(self):
293 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000294
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000295 def write(self, out):
296 index = getattr(threading.currentThread(), 'index', 0)
297 if not index and not self.__include_zero:
298 # Unindexed threads aren't buffered.
299 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000300
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000301 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000302 try:
303 # Use a dummy array to hold the string so the code can be lockless.
304 # Strings are immutable, requiring to keep a lock for the whole dictionary
305 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000306 if not index in self.__output_buffers:
307 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000308 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000309 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000310 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000311 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000312
313 # Continue lockless.
314 obj[0] += out
315 while '\n' in obj[0]:
316 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000317 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000318 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000319 obj[0] = remaining
320
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000321 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000322 """Flush buffered output."""
323 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000324 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000325 try:
326 # Detect threads no longer existing.
327 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000328 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000329 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000330 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000331 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000332 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000333 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000334 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000335 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000336
337 # Don't keep the lock while writting. Will append \n when it shouldn't.
338 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000339 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000340 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
341 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000342
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000343
344def MakeFileAutoFlush(fileobj, delay=10):
345 autoflush = getattr(fileobj, 'autoflush', None)
346 if autoflush:
347 autoflush.delay = delay
348 return fileobj
349 return AutoFlush(fileobj, delay)
350
351
352def MakeFileAnnotated(fileobj, include_zero=False):
353 if getattr(fileobj, 'annotated', None):
354 return fileobj
355 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000356
357
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000358GCLIENT_CHILDREN = []
359GCLIENT_CHILDREN_LOCK = threading.Lock()
360
361
362class GClientChildren(object):
363 @staticmethod
364 def add(popen_obj):
365 with GCLIENT_CHILDREN_LOCK:
366 GCLIENT_CHILDREN.append(popen_obj)
367
368 @staticmethod
369 def remove(popen_obj):
370 with GCLIENT_CHILDREN_LOCK:
371 GCLIENT_CHILDREN.remove(popen_obj)
372
373 @staticmethod
374 def _attemptToKillChildren():
375 global GCLIENT_CHILDREN
376 with GCLIENT_CHILDREN_LOCK:
377 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
378
379 for zombie in zombies:
380 try:
381 zombie.kill()
382 except OSError:
383 pass
384
385 with GCLIENT_CHILDREN_LOCK:
386 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
387
388 @staticmethod
389 def _areZombies():
390 with GCLIENT_CHILDREN_LOCK:
391 return bool(GCLIENT_CHILDREN)
392
393 @staticmethod
394 def KillAllRemainingChildren():
395 GClientChildren._attemptToKillChildren()
396
397 if GClientChildren._areZombies():
398 time.sleep(0.5)
399 GClientChildren._attemptToKillChildren()
400
401 with GCLIENT_CHILDREN_LOCK:
402 if GCLIENT_CHILDREN:
403 print >> sys.stderr, 'Could not kill the following subprocesses:'
404 for zombie in GCLIENT_CHILDREN:
405 print >> sys.stderr, ' ', zombie.pid
406
407
maruel@chromium.org17d01792010-09-01 18:07:10 +0000408def CheckCallAndFilter(args, stdout=None, filter_fn=None,
409 print_stdout=None, call_filter_on_first_line=False,
szager@chromium.org12b07e72013-05-03 22:06:34 +0000410 nag_timer=None, nag_max=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000411 """Runs a command and calls back a filter function if needed.
412
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000413 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000414 print_stdout: If True, the command's stdout is forwarded to stdout.
415 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000416 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000417 character trimmed.
418 stdout: Can be any bufferable output.
419
420 stderr is always redirected to stdout.
421 """
422 assert print_stdout or filter_fn
423 stdout = stdout or sys.stdout
424 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000425 kid = subprocess2.Popen(
426 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
427 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000428
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000429 GClientChildren.add(kid)
430
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000431 # Do a flush of stdout before we begin reading from the subprocess2's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000432 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000433
szager@chromium.org12b07e72013-05-03 22:06:34 +0000434 nag = None
435 if nag_timer:
436 # Hack thread.index to force correct annotation.
437 index = getattr(threading.currentThread(), 'index', 0)
438 def _nag_cb(elapsed):
439 setattr(threading.currentThread(), 'index', index)
440 stdout.write(' No output for %.0f seconds from command:\n' % elapsed)
441 stdout.write(' %s\n' % kid.cmd_str)
442 if (nag_max and
443 int('%.0f' % (elapsed / nag_timer)) >= nag_max):
444 stdout.write(' ... killing it!\n')
445 kid.kill()
446 nag = subprocess2.NagTimer(nag_timer, _nag_cb)
447 nag.start()
448
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000449 # Also, we need to forward stdout to prevent weird re-ordering of output.
450 # This has to be done on a per byte basis to make sure it is not buffered:
451 # normally buffering is done for each line, but if svn requests input, no
452 # end-of-line character is output after the prompt and it would not show up.
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000453 try:
454 in_byte = kid.stdout.read(1)
455 if in_byte:
szager@chromium.org12b07e72013-05-03 22:06:34 +0000456 if nag:
457 nag.event()
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000458 if call_filter_on_first_line:
459 filter_fn(None)
460 in_line = ''
461 while in_byte:
462 if in_byte != '\r':
463 if print_stdout:
464 stdout.write(in_byte)
465 if in_byte != '\n':
466 in_line += in_byte
467 else:
468 filter_fn(in_line)
469 in_line = ''
szager@google.com85d3e3a2011-10-07 17:12:00 +0000470 else:
471 filter_fn(in_line)
472 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000473 in_byte = kid.stdout.read(1)
szager@chromium.org12b07e72013-05-03 22:06:34 +0000474 if in_byte and nag:
475 nag.event()
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000476 # Flush the rest of buffered output. This is only an issue with
477 # stdout/stderr not ending with a \n.
478 if len(in_line):
479 filter_fn(in_line)
480 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000481
482 # Don't put this in a 'finally,' since the child may still run if we get an
483 # exception.
484 GClientChildren.remove(kid)
485
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000486 except KeyboardInterrupt:
487 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
488 raise
szager@chromium.org12b07e72013-05-03 22:06:34 +0000489 finally:
490 if nag:
491 nag.cancel()
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000492
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000493 if rv:
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000494 raise subprocess2.CalledProcessError(
495 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000496 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000497
498
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000499def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000500 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000501 real_from_dir = os.path.realpath(from_dir)
502 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000503 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000504 split_path = os.path.split(path)
505 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000506 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000507 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000508
509 # If we did not find the file in the current directory, make sure we are in a
510 # sub directory that is controlled by this configuration.
511 if path != real_from_dir:
512 entries_filename = os.path.join(path, filename + '_entries')
513 if not os.path.exists(entries_filename):
514 # If .gclient_entries does not exist, a previous call to gclient sync
515 # might have failed. In that case, we cannot verify that the .gclient
516 # is the one we want to use. In order to not to cause too much trouble,
517 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000518 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000519 "file you want to use" % (filename, path))
520 return path
521 scope = {}
522 try:
523 exec(FileRead(entries_filename), scope)
524 except SyntaxError, e:
525 SyntaxErrorToError(filename, e)
526 all_directories = scope['entries'].keys()
527 path_to_check = real_from_dir[len(path)+1:]
528 while path_to_check:
529 if path_to_check in all_directories:
530 return path
531 path_to_check = os.path.dirname(path_to_check)
532 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000533
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000534 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000535 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000536
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000537
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000538def PathDifference(root, subpath):
539 """Returns the difference subpath minus root."""
540 root = os.path.realpath(root)
541 subpath = os.path.realpath(subpath)
542 if not subpath.startswith(root):
543 return None
544 # If the root does not have a trailing \ or /, we add it so the returned
545 # path starts immediately after the seperator regardless of whether it is
546 # provided.
547 root = os.path.join(root, '')
548 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000549
550
551def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000552 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000553
rcui@google.com13595ff2011-10-13 01:25:07 +0000554 Returns nearest upper-level directory with the passed in file.
555 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000556 if not path:
557 path = os.getcwd()
558 path = os.path.realpath(path)
559 while True:
560 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000561 if os.path.exists(file_path):
562 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000563 (new_path, _) = os.path.split(path)
564 if new_path == path:
565 return None
566 path = new_path
567
568
569def GetGClientRootAndEntries(path=None):
570 """Returns the gclient root and the dict of entries."""
571 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000572 root = FindFileUpwards(config_file, path)
573 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000574 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000575 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000576 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000577 env = {}
578 execfile(config_path, env)
579 config_dir = os.path.dirname(config_path)
580 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000581
582
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000583def lockedmethod(method):
584 """Method decorator that holds self.lock for the duration of the call."""
585 def inner(self, *args, **kwargs):
586 try:
587 try:
588 self.lock.acquire()
589 except KeyboardInterrupt:
590 print >> sys.stderr, 'Was deadlocked'
591 raise
592 return method(self, *args, **kwargs)
593 finally:
594 self.lock.release()
595 return inner
596
597
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000598class WorkItem(object):
599 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000600 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
601 # As a workaround, use a single lock. Yep you read it right. Single lock for
602 # all the 100 objects.
603 lock = threading.Lock()
604
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000605 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000606 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000607 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000608
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000609 def run(self, work_queue):
610 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000611 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000612 pass
613
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000614 @property
615 def name(self):
616 return self._name
617
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000618
619class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000620 """Runs a set of WorkItem that have interdependencies and were WorkItem are
621 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000622
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000623 In gclient's case, Dependencies sometime needs to be run out of order due to
624 From() keyword. This class manages that all the required dependencies are run
625 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000626
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000627 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000628 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000629 def __init__(self, jobs, progress, ignore_requirements):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000630 """jobs specifies the number of concurrent tasks to allow. progress is a
631 Progress instance."""
632 # Set when a thread is done or a new item is enqueued.
633 self.ready_cond = threading.Condition()
634 # Maximum number of concurrent tasks.
635 self.jobs = jobs
636 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000637 self.queued = []
638 # List of strings representing each Dependency.name that was run.
639 self.ran = []
640 # List of items currently running.
641 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000642 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000643 self.exceptions = Queue.Queue()
644 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000645 self.progress = progress
646 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000647 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000648
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000649 self.ignore_requirements = ignore_requirements
650
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000651 def enqueue(self, d):
652 """Enqueue one Dependency to be executed later once its requirements are
653 satisfied.
654 """
655 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000656 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000657 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000658 self.queued.append(d)
659 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000660 logging.debug('enqueued(%s)' % d.name)
661 if self.progress:
662 self.progress._total = total + 1
663 self.progress.update(0)
664 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000665 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000666 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000667
668 def flush(self, *args, **kwargs):
669 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000670 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000671 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000672 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000673 while True:
674 # Check for task to run first, then wait.
675 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000676 if not self.exceptions.empty():
677 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000678 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000679 self._flush_terminated_threads()
680 if (not self.queued and not self.running or
681 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000682 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000683 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000684
685 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000686 for i in xrange(len(self.queued)):
687 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000688 if (self.ignore_requirements or
689 not (set(self.queued[i].requirements) - set(self.ran))):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000690 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000691 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000692 break
693 else:
694 # Couldn't find an item that could run. Break out the outher loop.
695 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000696
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000697 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000698 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000699 break
700 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000701 try:
702 self.ready_cond.wait(10)
703 except KeyboardInterrupt:
704 # Help debugging by printing some information:
705 print >> sys.stderr, (
706 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
707 'Running: %d') % (
708 self.jobs,
709 len(self.queued),
710 ', '.join(self.ran),
711 len(self.running)))
712 for i in self.queued:
713 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
714 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000715 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000716 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000717 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000718
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000719 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000720 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000721 # To get back the stack location correctly, the raise a, b, c form must be
722 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000723 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000724 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000725 if self.progress:
726 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000727
maruel@chromium.org3742c842010-09-09 19:27:14 +0000728 def _flush_terminated_threads(self):
729 """Flush threads that have terminated."""
730 running = self.running
731 self.running = []
732 for t in running:
733 if t.isAlive():
734 self.running.append(t)
735 else:
736 t.join()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000737 sys.stdout.flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000738 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000739 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000740 if t.item.name in self.ran:
741 raise Error(
742 'gclient is confused, "%s" is already in "%s"' % (
743 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000744 if not t.item.name in self.ran:
745 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000746
747 def _run_one_task(self, task_item, args, kwargs):
748 if self.jobs > 1:
749 # Start the thread.
750 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000751 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000752 self.running.append(new_thread)
753 new_thread.start()
754 else:
755 # Run the 'thread' inside the main thread. Don't try to catch any
756 # exception.
757 task_item.run(*args, **kwargs)
758 self.ran.append(task_item.name)
759 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000760 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000761
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000762 class _Worker(threading.Thread):
763 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000764 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000765 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000766 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000767 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000768 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000769 self.args = args
770 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000771 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000772
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000773 def run(self):
774 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000775 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000776 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000777 try:
778 self.item.run(*self.args, **self.kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000779 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000780 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000781 logging.info(str(sys.exc_info()))
782 work_queue.exceptions.put(sys.exc_info())
783 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000784 except Exception:
785 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000786 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000787 logging.info(str(sys.exc_info()))
788 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000789 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000790 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000791 work_queue.ready_cond.acquire()
792 try:
793 work_queue.ready_cond.notifyAll()
794 finally:
795 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000796
797
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000798def GetEditor(git, git_editor=None):
799 """Returns the most plausible editor to use.
800
801 In order of preference:
802 - GIT_EDITOR/SVN_EDITOR environment variable
803 - core.editor git configuration variable (if supplied by git-cl)
804 - VISUAL environment variable
805 - EDITOR environment variable
806 - vim (non-Windows) or notepad (Windows)
807
808 In the case of git-cl, this matches git's behaviour, except that it does not
809 include dumb terminal detection.
810
811 In the case of gcl, this matches svn's behaviour, except that it does not
812 accept a command-line flag or check the editor-cmd configuration variable.
813 """
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000814 if git:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000815 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000816 else:
817 editor = os.environ.get('SVN_EDITOR')
818 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000819 editor = os.environ.get('VISUAL')
820 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000821 editor = os.environ.get('EDITOR')
822 if not editor:
823 if sys.platform.startswith('win'):
824 editor = 'notepad'
825 else:
826 editor = 'vim'
827 return editor
828
829
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000830def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000831 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +0000832 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000833 # Make sure CRLF is handled properly by requiring none.
834 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000835 print >> sys.stderr, (
836 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000837 fileobj = os.fdopen(file_handle, 'w')
838 # Still remove \r if present.
839 fileobj.write(re.sub('\r?\n', '\n', content))
840 fileobj.close()
841
842 try:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000843 editor = GetEditor(git, git_editor=git_editor)
844 if not editor:
845 return None
846 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000847 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
848 # Msysgit requires the usage of 'env' to be present.
849 cmd = 'env ' + cmd
850 try:
851 # shell=True to allow the shell to handle all forms of quotes in
852 # $EDITOR.
853 subprocess2.check_call(cmd, shell=True)
854 except subprocess2.CalledProcessError:
855 return None
856 return FileRead(filename)
857 finally:
858 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000859
860
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000861def UpgradeToHttps(url):
862 """Upgrades random urls to https://.
863
864 Do not touch unknown urls like ssh:// or git://.
865 Do not touch http:// urls with a port number,
866 Fixes invalid GAE url.
867 """
868 if not url:
869 return url
870 if not re.match(r'[a-z\-]+\://.*', url):
871 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
872 # relative url and will use http:///foo. Note that it defaults to http://
873 # for compatibility with naked url like "localhost:8080".
874 url = 'http://%s' % url
875 parsed = list(urlparse.urlparse(url))
876 # Do not automatically upgrade http to https if a port number is provided.
877 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
878 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000879 return urlparse.urlunparse(parsed)
880
881
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000882def ParseCodereviewSettingsContent(content):
883 """Process a codereview.settings file properly."""
884 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
885 try:
886 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
887 except ValueError:
888 raise Error(
889 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000890 def fix_url(key):
891 if keyvals.get(key):
892 keyvals[key] = UpgradeToHttps(keyvals[key])
893 fix_url('CODE_REVIEW_SERVER')
894 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000895 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +0000896
897
898def NumLocalCpus():
899 """Returns the number of processors.
900
901 Python on OSX 10.6 raises a NotImplementedError exception.
902 """
903 try:
904 import multiprocessing
905 return multiprocessing.cpu_count()
906 except: # pylint: disable=W0702
907 # Mac OS 10.6 only
908 # pylint: disable=E1101
909 return int(os.sysconf('SC_NPROCESSORS_ONLN'))