blob: cad181e786b832f1ae56b7b5755a8547e1da50c2 [file] [log] [blame]
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org06617272010-11-04 13:50:50 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00004
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00005"""Generic utils."""
6
maruel@chromium.orgdae209f2012-07-03 16:08:15 +00007import codecs
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +00008import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00009import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000010import pipes
maruel@chromium.org3742c842010-09-09 19:27:14 +000011import Queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000012import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000013import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000014import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000015import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000016import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000017import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000018import time
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000019import urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000020
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000021import subprocess2
22
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000023
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000024RETRY_MAX = 3
25RETRY_INITIAL_SLEEP = 0.5
26
27
maruel@chromium.org66c83e62010-09-07 14:18:45 +000028class Error(Exception):
29 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000030 def __init__(self, msg, *args, **kwargs):
31 index = getattr(threading.currentThread(), 'index', 0)
32 if index:
33 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
34 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000035
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000036
msb@chromium.orgac915bb2009-11-13 17:03:01 +000037def SplitUrlRevision(url):
38 """Splits url and returns a two-tuple: url, rev"""
39 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000040 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +000041 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000042 components = re.search(regex, url).groups()
43 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000044 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000045 if len(components) == 1:
46 components += [None]
47 return tuple(components)
48
49
floitsch@google.comeaab7842011-04-28 09:07:58 +000050def IsDateRevision(revision):
51 """Returns true if the given revision is of the form "{ ... }"."""
52 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
53
54
55def MakeDateRevision(date):
56 """Returns a revision representing the latest revision before the given
57 date."""
58 return "{" + date + "}"
59
60
maruel@chromium.org5990f9d2010-07-07 18:02:58 +000061def SyntaxErrorToError(filename, e):
62 """Raises a gclient_utils.Error exception with the human readable message"""
63 try:
64 # Try to construct a human readable error message
65 if filename:
66 error_message = 'There is a syntax error in %s\n' % filename
67 else:
68 error_message = 'There is a syntax error\n'
69 error_message += 'Line #%s, character %s: "%s"' % (
70 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
71 except:
72 # Something went wrong, re-raise the original exception
73 raise e
74 else:
75 raise Error(error_message)
76
77
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000078class PrintableObject(object):
79 def __str__(self):
80 output = ''
81 for i in dir(self):
82 if i.startswith('__'):
83 continue
84 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
85 return output
86
87
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000088def FileRead(filename, mode='rU'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +000089 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +000090 # codecs.open() has different behavior than open() on python 2.6 so use
91 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +000092 s = f.read()
93 try:
94 return s.decode('utf-8')
95 except UnicodeDecodeError:
96 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000097
98
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000099def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000100 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000101 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000102
103
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000104def safe_rename(old, new):
105 """Renames a file reliably.
106
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000107 Sometimes os.rename does not work because a dying git process keeps a handle
108 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000109 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000110 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000111 """
112 # roughly 10s
113 retries = 100
114 for i in range(retries):
115 try:
116 os.rename(old, new)
117 break
118 except OSError:
119 if i == (retries - 1):
120 # Give up.
121 raise
122 # retry
123 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
124 time.sleep(0.1)
125
126
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000127def rmtree(path):
128 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000129
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000130 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000131
132 shutil.rmtree() doesn't work on Windows if any of the files or directories
133 are read-only, which svn repositories and some .svn files are. We need to
134 be able to force the files to be writable (i.e., deletable) as we traverse
135 the tree.
136
137 Even with all this, Windows still sometimes fails to delete a file, citing
138 a permission error (maybe something to do with antivirus scans or disk
139 indexing). The best suggestion any of the user forums had was to wait a
140 bit and try again, so we do that too. It's hand-waving, but sometimes it
141 works. :/
142
143 On POSIX systems, things are a little bit simpler. The modes of the files
144 to be deleted doesn't matter, only the modes of the directories containing
145 them are significant. As the directory tree is traversed, each directory
146 has its mode set appropriately before descending into it. This should
147 result in the entire tree being removed, with the possible exception of
148 *path itself, because nothing attempts to change the mode of its parent.
149 Doing so would be hazardous, as it's not a directory slated for removal.
150 In the ordinary case, this is not a problem: for our purposes, the user
151 will never lack write permission on *path's parent.
152 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000153 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000154 return
155
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000156 if os.path.islink(path) or not os.path.isdir(path):
157 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000158
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000159 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000160 # Give up and use cmd.exe's rd command.
161 path = os.path.normcase(path)
162 for _ in xrange(3):
163 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
164 if exitcode == 0:
165 return
166 else:
167 print >> sys.stderr, 'rd exited with code %d' % exitcode
168 time.sleep(3)
169 raise Exception('Failed to remove path %s' % path)
170
171 # On POSIX systems, we need the x-bit set on the directory to access it,
172 # the r-bit to see its contents, and the w-bit to remove files from it.
173 # The actual modes of the files within the directory is irrelevant.
174 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000175
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000176 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000177 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000178
179 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000180 # If fullpath is a symbolic link that points to a directory, isdir will
181 # be True, but we don't want to descend into that as a directory, we just
182 # want to remove the link. Check islink and treat links as ordinary files
183 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000184 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000185 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000186 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000187 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000188 # Recurse.
189 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000190
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000191 remove(os.rmdir, path)
192
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000193
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000194def safe_makedirs(tree):
195 """Creates the directory in a safe manner.
196
197 Because multiple threads can create these directories concurently, trap the
198 exception and pass on.
199 """
200 count = 0
201 while not os.path.exists(tree):
202 count += 1
203 try:
204 os.makedirs(tree)
205 except OSError, e:
206 # 17 POSIX, 183 Windows
207 if e.errno not in (17, 183):
208 raise
209 if count > 40:
210 # Give up.
211 raise
212
213
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000214def CommandToStr(args):
215 """Converts an arg list into a shell escaped string."""
216 return ' '.join(pipes.quote(arg) for arg in args)
217
218
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000219def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000220 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000221
maruel@chromium.org17d01792010-09-01 18:07:10 +0000222 If |always| is True, a message indicating what is being done
223 is printed to stdout all the time even if not output is generated. Otherwise
224 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000225 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000226 stdout = kwargs.setdefault('stdout', sys.stdout)
227 if header is None:
228 header = "\n________ running '%s' in '%s'\n" % (
ilevy@chromium.org4aad1852013-07-12 21:32:51 +0000229 ' '.join(args), kwargs.get('cwd', '.'))
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000230
maruel@chromium.org17d01792010-09-01 18:07:10 +0000231 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000232 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000233 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000234 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000235 def filter_msg(line):
236 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000237 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000238 elif filter_fn:
239 filter_fn(line)
240 kwargs['filter_fn'] = filter_msg
241 kwargs['call_filter_on_first_line'] = True
242 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000243 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000244 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000245
maruel@chromium.org17d01792010-09-01 18:07:10 +0000246
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000247class Wrapper(object):
248 """Wraps an object, acting as a transparent proxy for all properties by
249 default.
250 """
251 def __init__(self, wrapped):
252 self._wrapped = wrapped
253
254 def __getattr__(self, name):
255 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000256
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000257
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000258class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000259 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000260 def __init__(self, wrapped, delay):
261 super(AutoFlush, self).__init__(wrapped)
262 if not hasattr(self, 'lock'):
263 self.lock = threading.Lock()
264 self.__last_flushed_at = time.time()
265 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000266
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000267 @property
268 def autoflush(self):
269 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000270
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000271 def write(self, out, *args, **kwargs):
272 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000273 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000274 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000275 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000276 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000277 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000278 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000279 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000280 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000281 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000282 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000283
284
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000285class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000286 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000287 threads with a NN> prefix.
288 """
289 def __init__(self, wrapped, include_zero=False):
290 super(Annotated, self).__init__(wrapped)
291 if not hasattr(self, 'lock'):
292 self.lock = threading.Lock()
293 self.__output_buffers = {}
294 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000295
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000296 @property
297 def annotated(self):
298 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000299
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000300 def write(self, out):
301 index = getattr(threading.currentThread(), 'index', 0)
302 if not index and not self.__include_zero:
303 # Unindexed threads aren't buffered.
304 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000305
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000306 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000307 try:
308 # Use a dummy array to hold the string so the code can be lockless.
309 # Strings are immutable, requiring to keep a lock for the whole dictionary
310 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000311 if not index in self.__output_buffers:
312 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000313 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000314 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000315 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000316 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000317
318 # Continue lockless.
319 obj[0] += out
320 while '\n' in obj[0]:
321 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000322 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000323 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000324 obj[0] = remaining
325
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000326 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000327 """Flush buffered output."""
328 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000329 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000330 try:
331 # Detect threads no longer existing.
332 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000333 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000334 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000335 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000336 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000337 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000338 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000339 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000340 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000341
342 # Don't keep the lock while writting. Will append \n when it shouldn't.
343 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000344 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000345 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
346 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000347
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000348
349def MakeFileAutoFlush(fileobj, delay=10):
350 autoflush = getattr(fileobj, 'autoflush', None)
351 if autoflush:
352 autoflush.delay = delay
353 return fileobj
354 return AutoFlush(fileobj, delay)
355
356
357def MakeFileAnnotated(fileobj, include_zero=False):
358 if getattr(fileobj, 'annotated', None):
359 return fileobj
360 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000361
362
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000363GCLIENT_CHILDREN = []
364GCLIENT_CHILDREN_LOCK = threading.Lock()
365
366
367class GClientChildren(object):
368 @staticmethod
369 def add(popen_obj):
370 with GCLIENT_CHILDREN_LOCK:
371 GCLIENT_CHILDREN.append(popen_obj)
372
373 @staticmethod
374 def remove(popen_obj):
375 with GCLIENT_CHILDREN_LOCK:
376 GCLIENT_CHILDREN.remove(popen_obj)
377
378 @staticmethod
379 def _attemptToKillChildren():
380 global GCLIENT_CHILDREN
381 with GCLIENT_CHILDREN_LOCK:
382 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
383
384 for zombie in zombies:
385 try:
386 zombie.kill()
387 except OSError:
388 pass
389
390 with GCLIENT_CHILDREN_LOCK:
391 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
392
393 @staticmethod
394 def _areZombies():
395 with GCLIENT_CHILDREN_LOCK:
396 return bool(GCLIENT_CHILDREN)
397
398 @staticmethod
399 def KillAllRemainingChildren():
400 GClientChildren._attemptToKillChildren()
401
402 if GClientChildren._areZombies():
403 time.sleep(0.5)
404 GClientChildren._attemptToKillChildren()
405
406 with GCLIENT_CHILDREN_LOCK:
407 if GCLIENT_CHILDREN:
408 print >> sys.stderr, 'Could not kill the following subprocesses:'
409 for zombie in GCLIENT_CHILDREN:
410 print >> sys.stderr, ' ', zombie.pid
411
412
maruel@chromium.org17d01792010-09-01 18:07:10 +0000413def CheckCallAndFilter(args, stdout=None, filter_fn=None,
414 print_stdout=None, call_filter_on_first_line=False,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000415 retry=False, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000416 """Runs a command and calls back a filter function if needed.
417
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000418 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000419 print_stdout: If True, the command's stdout is forwarded to stdout.
420 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000421 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000422 character trimmed.
423 stdout: Can be any bufferable output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000424 retry: If the process exits non-zero, sleep for a brief interval and try
425 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000426
427 stderr is always redirected to stdout.
428 """
429 assert print_stdout or filter_fn
430 stdout = stdout or sys.stdout
431 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000432
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000433 sleep_interval = RETRY_INITIAL_SLEEP
434 run_cwd = kwargs.get('cwd', os.getcwd())
435 for _ in xrange(RETRY_MAX + 1):
436 kid = subprocess2.Popen(
437 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
438 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000439
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000440 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000441
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000442 # Do a flush of stdout before we begin reading from the subprocess2's stdout
443 stdout.flush()
444
445 # Also, we need to forward stdout to prevent weird re-ordering of output.
446 # This has to be done on a per byte basis to make sure it is not buffered:
447 # normally buffering is done for each line, but if svn requests input, no
448 # end-of-line character is output after the prompt and it would not show up.
449 try:
450 in_byte = kid.stdout.read(1)
451 if in_byte:
452 if call_filter_on_first_line:
453 filter_fn(None)
454 in_line = ''
455 while in_byte:
456 if in_byte != '\r':
457 if print_stdout:
458 stdout.write(in_byte)
459 if in_byte != '\n':
460 in_line += in_byte
461 else:
462 filter_fn(in_line)
463 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000464 else:
465 filter_fn(in_line)
466 in_line = ''
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000467 in_byte = kid.stdout.read(1)
468 # Flush the rest of buffered output. This is only an issue with
469 # stdout/stderr not ending with a \n.
470 if len(in_line):
szager@google.com85d3e3a2011-10-07 17:12:00 +0000471 filter_fn(in_line)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000472 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000473
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000474 # Don't put this in a 'finally,' since the child may still run if we get
475 # an exception.
476 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000477
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000478 except KeyboardInterrupt:
479 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
480 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000481
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000482 if rv == 0:
483 return 0
484 if not retry:
485 break
486 print ("WARNING: subprocess '%s' in %s failed; will retry after a short "
487 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000488 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000489 sleep_interval *= 2
490 raise subprocess2.CalledProcessError(
491 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000492
493
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000494def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000495 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000496 real_from_dir = os.path.realpath(from_dir)
497 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000498 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000499 split_path = os.path.split(path)
500 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000501 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000502 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000503
504 # If we did not find the file in the current directory, make sure we are in a
505 # sub directory that is controlled by this configuration.
506 if path != real_from_dir:
507 entries_filename = os.path.join(path, filename + '_entries')
508 if not os.path.exists(entries_filename):
509 # If .gclient_entries does not exist, a previous call to gclient sync
510 # might have failed. In that case, we cannot verify that the .gclient
511 # is the one we want to use. In order to not to cause too much trouble,
512 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000513 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000514 "file you want to use" % (filename, path))
515 return path
516 scope = {}
517 try:
518 exec(FileRead(entries_filename), scope)
519 except SyntaxError, e:
520 SyntaxErrorToError(filename, e)
521 all_directories = scope['entries'].keys()
522 path_to_check = real_from_dir[len(path)+1:]
523 while path_to_check:
524 if path_to_check in all_directories:
525 return path
526 path_to_check = os.path.dirname(path_to_check)
527 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000528
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000529 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000530 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000531
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000532
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000533def PathDifference(root, subpath):
534 """Returns the difference subpath minus root."""
535 root = os.path.realpath(root)
536 subpath = os.path.realpath(subpath)
537 if not subpath.startswith(root):
538 return None
539 # If the root does not have a trailing \ or /, we add it so the returned
540 # path starts immediately after the seperator regardless of whether it is
541 # provided.
542 root = os.path.join(root, '')
543 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000544
545
546def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000547 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000548
rcui@google.com13595ff2011-10-13 01:25:07 +0000549 Returns nearest upper-level directory with the passed in file.
550 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000551 if not path:
552 path = os.getcwd()
553 path = os.path.realpath(path)
554 while True:
555 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000556 if os.path.exists(file_path):
557 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000558 (new_path, _) = os.path.split(path)
559 if new_path == path:
560 return None
561 path = new_path
562
563
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000564def GetMacWinOrLinux():
565 """Returns 'mac', 'win', or 'linux', matching the current platform."""
566 if sys.platform.startswith(('cygwin', 'win')):
567 return 'win'
568 elif sys.platform.startswith('linux'):
569 return 'linux'
570 elif sys.platform == 'darwin':
571 return 'mac'
572 raise Error('Unknown platform: ' + sys.platform)
573
574
575def GetExeSuffix():
576 """Returns '' or '.exe' depending on how executables work on this platform."""
577 if sys.platform.startswith(('cygwin', 'win')):
578 return '.exe'
579 return ''
580
581
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000582def GetGClientRootAndEntries(path=None):
583 """Returns the gclient root and the dict of entries."""
584 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000585 root = FindFileUpwards(config_file, path)
586 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000587 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000588 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000589 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000590 env = {}
591 execfile(config_path, env)
592 config_dir = os.path.dirname(config_path)
593 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000594
595
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000596def lockedmethod(method):
597 """Method decorator that holds self.lock for the duration of the call."""
598 def inner(self, *args, **kwargs):
599 try:
600 try:
601 self.lock.acquire()
602 except KeyboardInterrupt:
603 print >> sys.stderr, 'Was deadlocked'
604 raise
605 return method(self, *args, **kwargs)
606 finally:
607 self.lock.release()
608 return inner
609
610
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000611class WorkItem(object):
612 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000613 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
614 # As a workaround, use a single lock. Yep you read it right. Single lock for
615 # all the 100 objects.
616 lock = threading.Lock()
617
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000618 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000619 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000620 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000621
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000622 def run(self, work_queue):
623 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000624 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000625 pass
626
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000627 @property
628 def name(self):
629 return self._name
630
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000631
632class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000633 """Runs a set of WorkItem that have interdependencies and were WorkItem are
634 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000635
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000636 In gclient's case, Dependencies sometime needs to be run out of order due to
637 From() keyword. This class manages that all the required dependencies are run
638 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000639
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000640 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000641 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000642 def __init__(self, jobs, progress, ignore_requirements):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000643 """jobs specifies the number of concurrent tasks to allow. progress is a
644 Progress instance."""
645 # Set when a thread is done or a new item is enqueued.
646 self.ready_cond = threading.Condition()
647 # Maximum number of concurrent tasks.
648 self.jobs = jobs
649 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000650 self.queued = []
651 # List of strings representing each Dependency.name that was run.
652 self.ran = []
653 # List of items currently running.
654 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000655 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000656 self.exceptions = Queue.Queue()
657 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000658 self.progress = progress
659 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000660 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000661
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000662 self.ignore_requirements = ignore_requirements
663
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000664 def enqueue(self, d):
665 """Enqueue one Dependency to be executed later once its requirements are
666 satisfied.
667 """
668 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000669 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000670 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000671 self.queued.append(d)
672 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000673 logging.debug('enqueued(%s)' % d.name)
674 if self.progress:
675 self.progress._total = total + 1
676 self.progress.update(0)
677 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000678 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000679 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000680
681 def flush(self, *args, **kwargs):
682 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000683 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000684 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000685 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000686 while True:
687 # Check for task to run first, then wait.
688 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000689 if not self.exceptions.empty():
690 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000691 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000692 self._flush_terminated_threads()
693 if (not self.queued and not self.running or
694 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000695 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000696 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000697
698 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000699 for i in xrange(len(self.queued)):
700 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000701 if (self.ignore_requirements or
702 not (set(self.queued[i].requirements) - set(self.ran))):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000703 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000704 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000705 break
706 else:
707 # Couldn't find an item that could run. Break out the outher loop.
708 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000709
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000710 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000711 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000712 break
713 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000714 try:
715 self.ready_cond.wait(10)
716 except KeyboardInterrupt:
717 # Help debugging by printing some information:
718 print >> sys.stderr, (
719 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
720 'Running: %d') % (
721 self.jobs,
722 len(self.queued),
723 ', '.join(self.ran),
724 len(self.running)))
725 for i in self.queued:
726 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
727 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000728 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000729 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000730 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000731
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000732 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000733 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000734 # To get back the stack location correctly, the raise a, b, c form must be
735 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000736 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000737 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000738 if self.progress:
739 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000740
maruel@chromium.org3742c842010-09-09 19:27:14 +0000741 def _flush_terminated_threads(self):
742 """Flush threads that have terminated."""
743 running = self.running
744 self.running = []
745 for t in running:
746 if t.isAlive():
747 self.running.append(t)
748 else:
749 t.join()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000750 sys.stdout.flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000751 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000752 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000753 if t.item.name in self.ran:
754 raise Error(
755 'gclient is confused, "%s" is already in "%s"' % (
756 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000757 if not t.item.name in self.ran:
758 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000759
760 def _run_one_task(self, task_item, args, kwargs):
761 if self.jobs > 1:
762 # Start the thread.
763 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000764 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000765 self.running.append(new_thread)
766 new_thread.start()
767 else:
768 # Run the 'thread' inside the main thread. Don't try to catch any
769 # exception.
770 task_item.run(*args, **kwargs)
771 self.ran.append(task_item.name)
772 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000773 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000774
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000775 class _Worker(threading.Thread):
776 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000777 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000778 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000779 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000780 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000781 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000782 self.args = args
783 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000784 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000785
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000786 def run(self):
787 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000788 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000789 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000790 try:
791 self.item.run(*self.args, **self.kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000792 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000793 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000794 logging.info(str(sys.exc_info()))
795 work_queue.exceptions.put(sys.exc_info())
796 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000797 except Exception:
798 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000799 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000800 logging.info(str(sys.exc_info()))
801 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000802 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000803 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000804 work_queue.ready_cond.acquire()
805 try:
806 work_queue.ready_cond.notifyAll()
807 finally:
808 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000809
810
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000811def GetEditor(git, git_editor=None):
812 """Returns the most plausible editor to use.
813
814 In order of preference:
815 - GIT_EDITOR/SVN_EDITOR environment variable
816 - core.editor git configuration variable (if supplied by git-cl)
817 - VISUAL environment variable
818 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +0000819 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000820
821 In the case of git-cl, this matches git's behaviour, except that it does not
822 include dumb terminal detection.
823
824 In the case of gcl, this matches svn's behaviour, except that it does not
825 accept a command-line flag or check the editor-cmd configuration variable.
826 """
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000827 if git:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000828 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000829 else:
830 editor = os.environ.get('SVN_EDITOR')
831 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000832 editor = os.environ.get('VISUAL')
833 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000834 editor = os.environ.get('EDITOR')
835 if not editor:
836 if sys.platform.startswith('win'):
837 editor = 'notepad'
838 else:
bratell@opera.com65621c72013-12-09 15:05:32 +0000839 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000840 return editor
841
842
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000843def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000844 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +0000845 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000846 # Make sure CRLF is handled properly by requiring none.
847 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000848 print >> sys.stderr, (
849 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000850 fileobj = os.fdopen(file_handle, 'w')
851 # Still remove \r if present.
852 fileobj.write(re.sub('\r?\n', '\n', content))
853 fileobj.close()
854
855 try:
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000856 editor = GetEditor(git, git_editor=git_editor)
857 if not editor:
858 return None
859 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000860 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
861 # Msysgit requires the usage of 'env' to be present.
862 cmd = 'env ' + cmd
863 try:
864 # shell=True to allow the shell to handle all forms of quotes in
865 # $EDITOR.
866 subprocess2.check_call(cmd, shell=True)
867 except subprocess2.CalledProcessError:
868 return None
869 return FileRead(filename)
870 finally:
871 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000872
873
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000874def UpgradeToHttps(url):
875 """Upgrades random urls to https://.
876
877 Do not touch unknown urls like ssh:// or git://.
878 Do not touch http:// urls with a port number,
879 Fixes invalid GAE url.
880 """
881 if not url:
882 return url
883 if not re.match(r'[a-z\-]+\://.*', url):
884 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
885 # relative url and will use http:///foo. Note that it defaults to http://
886 # for compatibility with naked url like "localhost:8080".
887 url = 'http://%s' % url
888 parsed = list(urlparse.urlparse(url))
889 # Do not automatically upgrade http to https if a port number is provided.
890 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
891 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000892 return urlparse.urlunparse(parsed)
893
894
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000895def ParseCodereviewSettingsContent(content):
896 """Process a codereview.settings file properly."""
897 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
898 try:
899 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
900 except ValueError:
901 raise Error(
902 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000903 def fix_url(key):
904 if keyvals.get(key):
905 keyvals[key] = UpgradeToHttps(keyvals[key])
906 fix_url('CODE_REVIEW_SERVER')
907 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000908 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +0000909
910
911def NumLocalCpus():
912 """Returns the number of processors.
913
914 Python on OSX 10.6 raises a NotImplementedError exception.
915 """
916 try:
917 import multiprocessing
918 return multiprocessing.cpu_count()
919 except: # pylint: disable=W0702
920 # Mac OS 10.6 only
921 # pylint: disable=E1101
922 return int(os.sysconf('SC_NPROCESSORS_ONLN'))