blob: 80eb816b12dc1c540eb72c043c118e3d36dc5dc4 [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,
scottmg@chromium.orgf547c802013-09-27 17:55:26 +0000410 **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
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000434 # Also, we need to forward stdout to prevent weird re-ordering of output.
435 # This has to be done on a per byte basis to make sure it is not buffered:
436 # normally buffering is done for each line, but if svn requests input, no
437 # end-of-line character is output after the prompt and it would not show up.
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000438 try:
439 in_byte = kid.stdout.read(1)
440 if in_byte:
441 if call_filter_on_first_line:
442 filter_fn(None)
443 in_line = ''
444 while in_byte:
445 if in_byte != '\r':
446 if print_stdout:
447 stdout.write(in_byte)
448 if in_byte != '\n':
449 in_line += in_byte
450 else:
451 filter_fn(in_line)
452 in_line = ''
szager@google.com85d3e3a2011-10-07 17:12:00 +0000453 else:
454 filter_fn(in_line)
455 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000456 in_byte = kid.stdout.read(1)
457 # Flush the rest of buffered output. This is only an issue with
458 # stdout/stderr not ending with a \n.
459 if len(in_line):
460 filter_fn(in_line)
461 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000462
463 # Don't put this in a 'finally,' since the child may still run if we get an
464 # exception.
465 GClientChildren.remove(kid)
466
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000467 except KeyboardInterrupt:
468 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
469 raise
470
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000471 if rv:
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000472 raise subprocess2.CalledProcessError(
473 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000474 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000475
476
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000477def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000478 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000479 real_from_dir = os.path.realpath(from_dir)
480 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000481 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000482 split_path = os.path.split(path)
483 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000484 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000485 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000486
487 # If we did not find the file in the current directory, make sure we are in a
488 # sub directory that is controlled by this configuration.
489 if path != real_from_dir:
490 entries_filename = os.path.join(path, filename + '_entries')
491 if not os.path.exists(entries_filename):
492 # If .gclient_entries does not exist, a previous call to gclient sync
493 # might have failed. In that case, we cannot verify that the .gclient
494 # is the one we want to use. In order to not to cause too much trouble,
495 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000496 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000497 "file you want to use" % (filename, path))
498 return path
499 scope = {}
500 try:
501 exec(FileRead(entries_filename), scope)
502 except SyntaxError, e:
503 SyntaxErrorToError(filename, e)
504 all_directories = scope['entries'].keys()
505 path_to_check = real_from_dir[len(path)+1:]
506 while path_to_check:
507 if path_to_check in all_directories:
508 return path
509 path_to_check = os.path.dirname(path_to_check)
510 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000511
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000512 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000513 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000514
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000515
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000516def PathDifference(root, subpath):
517 """Returns the difference subpath minus root."""
518 root = os.path.realpath(root)
519 subpath = os.path.realpath(subpath)
520 if not subpath.startswith(root):
521 return None
522 # If the root does not have a trailing \ or /, we add it so the returned
523 # path starts immediately after the seperator regardless of whether it is
524 # provided.
525 root = os.path.join(root, '')
526 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000527
528
529def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000530 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000531
rcui@google.com13595ff2011-10-13 01:25:07 +0000532 Returns nearest upper-level directory with the passed in file.
533 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000534 if not path:
535 path = os.getcwd()
536 path = os.path.realpath(path)
537 while True:
538 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000539 if os.path.exists(file_path):
540 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000541 (new_path, _) = os.path.split(path)
542 if new_path == path:
543 return None
544 path = new_path
545
546
547def GetGClientRootAndEntries(path=None):
548 """Returns the gclient root and the dict of entries."""
549 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000550 root = FindFileUpwards(config_file, path)
551 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000552 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000553 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000554 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000555 env = {}
556 execfile(config_path, env)
557 config_dir = os.path.dirname(config_path)
558 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000559
560
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000561def lockedmethod(method):
562 """Method decorator that holds self.lock for the duration of the call."""
563 def inner(self, *args, **kwargs):
564 try:
565 try:
566 self.lock.acquire()
567 except KeyboardInterrupt:
568 print >> sys.stderr, 'Was deadlocked'
569 raise
570 return method(self, *args, **kwargs)
571 finally:
572 self.lock.release()
573 return inner
574
575
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000576class WorkItem(object):
577 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000578 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
579 # As a workaround, use a single lock. Yep you read it right. Single lock for
580 # all the 100 objects.
581 lock = threading.Lock()
582
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000583 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000584 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000585 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000586
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000587 def run(self, work_queue):
588 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000589 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000590 pass
591
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000592 @property
593 def name(self):
594 return self._name
595
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000596
597class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000598 """Runs a set of WorkItem that have interdependencies and were WorkItem are
599 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000600
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000601 In gclient's case, Dependencies sometime needs to be run out of order due to
602 From() keyword. This class manages that all the required dependencies are run
603 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000604
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000605 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000606 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000607 def __init__(self, jobs, progress, ignore_requirements):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000608 """jobs specifies the number of concurrent tasks to allow. progress is a
609 Progress instance."""
610 # Set when a thread is done or a new item is enqueued.
611 self.ready_cond = threading.Condition()
612 # Maximum number of concurrent tasks.
613 self.jobs = jobs
614 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000615 self.queued = []
616 # List of strings representing each Dependency.name that was run.
617 self.ran = []
618 # List of items currently running.
619 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000620 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000621 self.exceptions = Queue.Queue()
622 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000623 self.progress = progress
624 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000625 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000626
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000627 self.ignore_requirements = ignore_requirements
628
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000629 def enqueue(self, d):
630 """Enqueue one Dependency to be executed later once its requirements are
631 satisfied.
632 """
633 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000634 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000635 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000636 self.queued.append(d)
637 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000638 logging.debug('enqueued(%s)' % d.name)
639 if self.progress:
640 self.progress._total = total + 1
641 self.progress.update(0)
642 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000643 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000644 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000645
646 def flush(self, *args, **kwargs):
647 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000648 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000649 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000650 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000651 while True:
652 # Check for task to run first, then wait.
653 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000654 if not self.exceptions.empty():
655 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000656 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000657 self._flush_terminated_threads()
658 if (not self.queued and not self.running or
659 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000660 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000661 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000662
663 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000664 for i in xrange(len(self.queued)):
665 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000666 if (self.ignore_requirements or
667 not (set(self.queued[i].requirements) - set(self.ran))):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000668 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000669 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000670 break
671 else:
672 # Couldn't find an item that could run. Break out the outher loop.
673 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000674
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000675 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000676 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000677 break
678 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000679 try:
680 self.ready_cond.wait(10)
681 except KeyboardInterrupt:
682 # Help debugging by printing some information:
683 print >> sys.stderr, (
684 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
685 'Running: %d') % (
686 self.jobs,
687 len(self.queued),
688 ', '.join(self.ran),
689 len(self.running)))
690 for i in self.queued:
691 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
692 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000693 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000694 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000695 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000696
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000697 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000698 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000699 # To get back the stack location correctly, the raise a, b, c form must be
700 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000701 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000702 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000703 if self.progress:
704 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000705
maruel@chromium.org3742c842010-09-09 19:27:14 +0000706 def _flush_terminated_threads(self):
707 """Flush threads that have terminated."""
708 running = self.running
709 self.running = []
710 for t in running:
711 if t.isAlive():
712 self.running.append(t)
713 else:
714 t.join()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000715 sys.stdout.flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000716 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000717 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000718 if t.item.name in self.ran:
719 raise Error(
720 'gclient is confused, "%s" is already in "%s"' % (
721 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000722 if not t.item.name in self.ran:
723 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000724
725 def _run_one_task(self, task_item, args, kwargs):
726 if self.jobs > 1:
727 # Start the thread.
728 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000729 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000730 self.running.append(new_thread)
731 new_thread.start()
732 else:
733 # Run the 'thread' inside the main thread. Don't try to catch any
734 # exception.
735 task_item.run(*args, **kwargs)
736 self.ran.append(task_item.name)
737 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000738 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000739
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000740 class _Worker(threading.Thread):
741 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000742 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000743 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000744 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000745 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000746 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000747 self.args = args
748 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000749 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000750
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000751 def run(self):
752 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000753 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000754 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000755 try:
756 self.item.run(*self.args, **self.kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000757 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000758 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000759 logging.info(str(sys.exc_info()))
760 work_queue.exceptions.put(sys.exc_info())
761 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000762 except Exception:
763 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000764 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000765 logging.info(str(sys.exc_info()))
766 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000767 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000768 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000769 work_queue.ready_cond.acquire()
770 try:
771 work_queue.ready_cond.notifyAll()
772 finally:
773 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000774
775
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000776def GetEditor(git, git_editor=None):
777 """Returns the most plausible editor to use.
778
779 In order of preference:
780 - GIT_EDITOR/SVN_EDITOR environment variable
781 - core.editor git configuration variable (if supplied by git-cl)
782 - VISUAL environment variable
783 - EDITOR environment variable
784 - vim (non-Windows) or notepad (Windows)
785
786 In the case of git-cl, this matches git's behaviour, except that it does not
787 include dumb terminal detection.
788
789 In the case of gcl, this matches svn's behaviour, except that it does not
790 accept a command-line flag or check the editor-cmd configuration variable.
791 """
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000792 if git:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000793 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000794 else:
795 editor = os.environ.get('SVN_EDITOR')
796 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000797 editor = os.environ.get('VISUAL')
798 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000799 editor = os.environ.get('EDITOR')
800 if not editor:
801 if sys.platform.startswith('win'):
802 editor = 'notepad'
803 else:
804 editor = 'vim'
805 return editor
806
807
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000808def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000809 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +0000810 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000811 # Make sure CRLF is handled properly by requiring none.
812 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000813 print >> sys.stderr, (
814 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000815 fileobj = os.fdopen(file_handle, 'w')
816 # Still remove \r if present.
817 fileobj.write(re.sub('\r?\n', '\n', content))
818 fileobj.close()
819
820 try:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000821 editor = GetEditor(git, git_editor=git_editor)
822 if not editor:
823 return None
824 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000825 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
826 # Msysgit requires the usage of 'env' to be present.
827 cmd = 'env ' + cmd
828 try:
829 # shell=True to allow the shell to handle all forms of quotes in
830 # $EDITOR.
831 subprocess2.check_call(cmd, shell=True)
832 except subprocess2.CalledProcessError:
833 return None
834 return FileRead(filename)
835 finally:
836 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000837
838
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000839def UpgradeToHttps(url):
840 """Upgrades random urls to https://.
841
842 Do not touch unknown urls like ssh:// or git://.
843 Do not touch http:// urls with a port number,
844 Fixes invalid GAE url.
845 """
846 if not url:
847 return url
848 if not re.match(r'[a-z\-]+\://.*', url):
849 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
850 # relative url and will use http:///foo. Note that it defaults to http://
851 # for compatibility with naked url like "localhost:8080".
852 url = 'http://%s' % url
853 parsed = list(urlparse.urlparse(url))
854 # Do not automatically upgrade http to https if a port number is provided.
855 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
856 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000857 return urlparse.urlunparse(parsed)
858
859
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000860def ParseCodereviewSettingsContent(content):
861 """Process a codereview.settings file properly."""
862 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
863 try:
864 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
865 except ValueError:
866 raise Error(
867 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000868 def fix_url(key):
869 if keyvals.get(key):
870 keyvals[key] = UpgradeToHttps(keyvals[key])
871 fix_url('CODE_REVIEW_SERVER')
872 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000873 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +0000874
875
876def NumLocalCpus():
877 """Returns the number of processors.
878
879 Python on OSX 10.6 raises a NotImplementedError exception.
880 """
881 try:
882 import multiprocessing
883 return multiprocessing.cpu_count()
884 except: # pylint: disable=W0702
885 # Mac OS 10.6 only
886 # pylint: disable=E1101
887 return int(os.sysconf('SC_NPROCESSORS_ONLN'))