blob: e43577b67ccedc51d26b87f3883880e158d13565 [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.org167b9e62009-09-17 17:41:02 +00008import errno
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +00009import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000010import os
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
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000014import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000015import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000016import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000017import time
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000018import urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000019
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000020import subprocess2
21
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000022
maruel@chromium.org66c83e62010-09-07 14:18:45 +000023class Error(Exception):
24 """gclient exception class."""
25 pass
26
27
msb@chromium.orgac915bb2009-11-13 17:03:01 +000028def SplitUrlRevision(url):
29 """Splits url and returns a two-tuple: url, rev"""
30 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000031 # Make sure ssh://user-name@example.com/~/test.git@stable works
32 regex = r'(ssh://(?:[-\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000033 components = re.search(regex, url).groups()
34 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000035 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000036 if len(components) == 1:
37 components += [None]
38 return tuple(components)
39
40
floitsch@google.comeaab7842011-04-28 09:07:58 +000041def IsDateRevision(revision):
42 """Returns true if the given revision is of the form "{ ... }"."""
43 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
44
45
46def MakeDateRevision(date):
47 """Returns a revision representing the latest revision before the given
48 date."""
49 return "{" + date + "}"
50
51
maruel@chromium.org5990f9d2010-07-07 18:02:58 +000052def SyntaxErrorToError(filename, e):
53 """Raises a gclient_utils.Error exception with the human readable message"""
54 try:
55 # Try to construct a human readable error message
56 if filename:
57 error_message = 'There is a syntax error in %s\n' % filename
58 else:
59 error_message = 'There is a syntax error\n'
60 error_message += 'Line #%s, character %s: "%s"' % (
61 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
62 except:
63 # Something went wrong, re-raise the original exception
64 raise e
65 else:
66 raise Error(error_message)
67
68
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000069class PrintableObject(object):
70 def __str__(self):
71 output = ''
72 for i in dir(self):
73 if i.startswith('__'):
74 continue
75 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
76 return output
77
78
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000079def FileRead(filename, mode='rU'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +000080 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org8a066dc2012-07-03 16:37:37 +000081 content = f.read()
82 if mode.endswith('U'):
83 # codecs.open() has different behavior than open() on python 2.6.
84 return content.replace('\r\n', '\n')
85 return content
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000086
87
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000088def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +000089 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000090 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000091
92
maruel@chromium.orgf9040722011-03-09 14:47:51 +000093def rmtree(path):
94 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000095
maruel@chromium.orgf9040722011-03-09 14:47:51 +000096 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000097
98 shutil.rmtree() doesn't work on Windows if any of the files or directories
99 are read-only, which svn repositories and some .svn files are. We need to
100 be able to force the files to be writable (i.e., deletable) as we traverse
101 the tree.
102
103 Even with all this, Windows still sometimes fails to delete a file, citing
104 a permission error (maybe something to do with antivirus scans or disk
105 indexing). The best suggestion any of the user forums had was to wait a
106 bit and try again, so we do that too. It's hand-waving, but sometimes it
107 works. :/
108
109 On POSIX systems, things are a little bit simpler. The modes of the files
110 to be deleted doesn't matter, only the modes of the directories containing
111 them are significant. As the directory tree is traversed, each directory
112 has its mode set appropriately before descending into it. This should
113 result in the entire tree being removed, with the possible exception of
114 *path itself, because nothing attempts to change the mode of its parent.
115 Doing so would be hazardous, as it's not a directory slated for removal.
116 In the ordinary case, this is not a problem: for our purposes, the user
117 will never lack write permission on *path's parent.
118 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000119 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000120 return
121
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000122 if os.path.islink(path) or not os.path.isdir(path):
123 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000124
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000125 if sys.platform == 'win32':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000126 # Some people don't have the APIs installed. In that case we'll do without.
maruel@chromium.org1edee692011-03-12 19:39:13 +0000127 win32api = None
128 win32con = None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000129 try:
maruel@chromium.org1edee692011-03-12 19:39:13 +0000130 # Unable to import 'XX'
131 # pylint: disable=F0401
132 import win32api, win32con
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000133 except ImportError:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000134 pass
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000135 else:
136 # On POSIX systems, we need the x-bit set on the directory to access it,
137 # the r-bit to see its contents, and the w-bit to remove files from it.
138 # The actual modes of the files within the directory is irrelevant.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000139 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000140
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000141 def remove(func, subpath):
142 if sys.platform == 'win32':
143 os.chmod(subpath, stat.S_IWRITE)
144 if win32api and win32con:
145 win32api.SetFileAttributes(subpath, win32con.FILE_ATTRIBUTE_NORMAL)
146 try:
147 func(subpath)
148 except OSError, e:
149 if e.errno != errno.EACCES or sys.platform != 'win32':
150 raise
151 # Failed to delete, try again after a 100ms sleep.
152 time.sleep(0.1)
153 func(subpath)
154
155 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000156 # If fullpath is a symbolic link that points to a directory, isdir will
157 # be True, but we don't want to descend into that as a directory, we just
158 # want to remove the link. Check islink and treat links as ordinary files
159 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000160 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000161 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000162 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000163 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000164 # Recurse.
165 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000166
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000167 remove(os.rmdir, path)
168
169# TODO(maruel): Rename the references.
170RemoveDirectory = rmtree
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000171
172
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000173def safe_makedirs(tree):
174 """Creates the directory in a safe manner.
175
176 Because multiple threads can create these directories concurently, trap the
177 exception and pass on.
178 """
179 count = 0
180 while not os.path.exists(tree):
181 count += 1
182 try:
183 os.makedirs(tree)
184 except OSError, e:
185 # 17 POSIX, 183 Windows
186 if e.errno not in (17, 183):
187 raise
188 if count > 40:
189 # Give up.
190 raise
191
192
maruel@chromium.org17d01792010-09-01 18:07:10 +0000193def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
194 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000195
maruel@chromium.org17d01792010-09-01 18:07:10 +0000196 If |always| is True, a message indicating what is being done
197 is printed to stdout all the time even if not output is generated. Otherwise
198 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000199 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000200 stdout = kwargs.get('stdout', None) or sys.stdout
201 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000202 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000203 % (' '.join(args), kwargs.get('cwd', '.')))
204 else:
205 filter_fn = kwargs.get('filter_fn', None)
206 def filter_msg(line):
207 if line is None:
208 stdout.write('\n________ running \'%s\' in \'%s\'\n'
209 % (' '.join(args), kwargs.get('cwd', '.')))
210 elif filter_fn:
211 filter_fn(line)
212 kwargs['filter_fn'] = filter_msg
213 kwargs['call_filter_on_first_line'] = True
214 # Obviously.
215 kwargs['print_stdout'] = True
216 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000217
maruel@chromium.org17d01792010-09-01 18:07:10 +0000218
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000219class Wrapper(object):
220 """Wraps an object, acting as a transparent proxy for all properties by
221 default.
222 """
223 def __init__(self, wrapped):
224 self._wrapped = wrapped
225
226 def __getattr__(self, name):
227 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000228
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000229
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000230class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000231 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000232 def __init__(self, wrapped, delay):
233 super(AutoFlush, self).__init__(wrapped)
234 if not hasattr(self, 'lock'):
235 self.lock = threading.Lock()
236 self.__last_flushed_at = time.time()
237 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000238
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000239 @property
240 def autoflush(self):
241 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000242
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000243 def write(self, out, *args, **kwargs):
244 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000245 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000246 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000247 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000248 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000249 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000250 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000251 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000252 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000253 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000254 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000255
256
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000257class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000258 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000259 threads with a NN> prefix.
260 """
261 def __init__(self, wrapped, include_zero=False):
262 super(Annotated, self).__init__(wrapped)
263 if not hasattr(self, 'lock'):
264 self.lock = threading.Lock()
265 self.__output_buffers = {}
266 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000267
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000268 @property
269 def annotated(self):
270 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000271
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000272 def write(self, out):
273 index = getattr(threading.currentThread(), 'index', 0)
274 if not index and not self.__include_zero:
275 # Unindexed threads aren't buffered.
276 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000277
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000278 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000279 try:
280 # Use a dummy array to hold the string so the code can be lockless.
281 # Strings are immutable, requiring to keep a lock for the whole dictionary
282 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000283 if not index in self.__output_buffers:
284 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000285 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000286 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000287 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000288 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000289
290 # Continue lockless.
291 obj[0] += out
292 while '\n' in obj[0]:
293 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000294 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000295 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000296 obj[0] = remaining
297
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000298 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000299 """Flush buffered output."""
300 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000301 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000302 try:
303 # Detect threads no longer existing.
304 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000305 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000306 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000307 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000308 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000309 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000310 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000311 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000312 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000313
314 # Don't keep the lock while writting. Will append \n when it shouldn't.
315 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000316 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000317 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
318 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000319
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000320
321def MakeFileAutoFlush(fileobj, delay=10):
322 autoflush = getattr(fileobj, 'autoflush', None)
323 if autoflush:
324 autoflush.delay = delay
325 return fileobj
326 return AutoFlush(fileobj, delay)
327
328
329def MakeFileAnnotated(fileobj, include_zero=False):
330 if getattr(fileobj, 'annotated', None):
331 return fileobj
332 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000333
334
maruel@chromium.org17d01792010-09-01 18:07:10 +0000335def CheckCallAndFilter(args, stdout=None, filter_fn=None,
336 print_stdout=None, call_filter_on_first_line=False,
337 **kwargs):
338 """Runs a command and calls back a filter function if needed.
339
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000340 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000341 print_stdout: If True, the command's stdout is forwarded to stdout.
342 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000343 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000344 character trimmed.
345 stdout: Can be any bufferable output.
346
347 stderr is always redirected to stdout.
348 """
349 assert print_stdout or filter_fn
350 stdout = stdout or sys.stdout
351 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000352 kid = subprocess2.Popen(
353 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
354 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000355
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000356 # Do a flush of stdout before we begin reading from the subprocess2's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000357 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000358
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000359 # Also, we need to forward stdout to prevent weird re-ordering of output.
360 # This has to be done on a per byte basis to make sure it is not buffered:
361 # normally buffering is done for each line, but if svn requests input, no
362 # end-of-line character is output after the prompt and it would not show up.
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000363 try:
364 in_byte = kid.stdout.read(1)
365 if in_byte:
366 if call_filter_on_first_line:
367 filter_fn(None)
368 in_line = ''
369 while in_byte:
370 if in_byte != '\r':
371 if print_stdout:
372 stdout.write(in_byte)
373 if in_byte != '\n':
374 in_line += in_byte
375 else:
376 filter_fn(in_line)
377 in_line = ''
szager@google.com85d3e3a2011-10-07 17:12:00 +0000378 else:
379 filter_fn(in_line)
380 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000381 in_byte = kid.stdout.read(1)
382 # Flush the rest of buffered output. This is only an issue with
383 # stdout/stderr not ending with a \n.
384 if len(in_line):
385 filter_fn(in_line)
386 rv = kid.wait()
387 except KeyboardInterrupt:
388 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
389 raise
390
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000391 if rv:
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000392 raise subprocess2.CalledProcessError(
393 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000394 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000395
396
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000397def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000398 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000399 real_from_dir = os.path.realpath(from_dir)
400 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000401 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000402 split_path = os.path.split(path)
403 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000404 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000405 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000406
407 # If we did not find the file in the current directory, make sure we are in a
408 # sub directory that is controlled by this configuration.
409 if path != real_from_dir:
410 entries_filename = os.path.join(path, filename + '_entries')
411 if not os.path.exists(entries_filename):
412 # If .gclient_entries does not exist, a previous call to gclient sync
413 # might have failed. In that case, we cannot verify that the .gclient
414 # is the one we want to use. In order to not to cause too much trouble,
415 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000416 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000417 "file you want to use" % (filename, path))
418 return path
419 scope = {}
420 try:
421 exec(FileRead(entries_filename), scope)
422 except SyntaxError, e:
423 SyntaxErrorToError(filename, e)
424 all_directories = scope['entries'].keys()
425 path_to_check = real_from_dir[len(path)+1:]
426 while path_to_check:
427 if path_to_check in all_directories:
428 return path
429 path_to_check = os.path.dirname(path_to_check)
430 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000431
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000432 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000433 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000434
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000435
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000436def PathDifference(root, subpath):
437 """Returns the difference subpath minus root."""
438 root = os.path.realpath(root)
439 subpath = os.path.realpath(subpath)
440 if not subpath.startswith(root):
441 return None
442 # If the root does not have a trailing \ or /, we add it so the returned
443 # path starts immediately after the seperator regardless of whether it is
444 # provided.
445 root = os.path.join(root, '')
446 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000447
448
449def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000450 """Search upwards from the a directory (default: current) to find a file.
451
452 Returns nearest upper-level directory with the passed in file.
453 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000454 if not path:
455 path = os.getcwd()
456 path = os.path.realpath(path)
457 while True:
458 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000459 if os.path.exists(file_path):
460 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000461 (new_path, _) = os.path.split(path)
462 if new_path == path:
463 return None
464 path = new_path
465
466
467def GetGClientRootAndEntries(path=None):
468 """Returns the gclient root and the dict of entries."""
469 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000470 root = FindFileUpwards(config_file, path)
471 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000472 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000473 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000474 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000475 env = {}
476 execfile(config_path, env)
477 config_dir = os.path.dirname(config_path)
478 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000479
480
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000481def lockedmethod(method):
482 """Method decorator that holds self.lock for the duration of the call."""
483 def inner(self, *args, **kwargs):
484 try:
485 try:
486 self.lock.acquire()
487 except KeyboardInterrupt:
488 print >> sys.stderr, 'Was deadlocked'
489 raise
490 return method(self, *args, **kwargs)
491 finally:
492 self.lock.release()
493 return inner
494
495
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000496class WorkItem(object):
497 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000498 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
499 # As a workaround, use a single lock. Yep you read it right. Single lock for
500 # all the 100 objects.
501 lock = threading.Lock()
502
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000503 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000504 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000505 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000506
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000507 def run(self, work_queue):
508 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000509 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000510 pass
511
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000512 @property
513 def name(self):
514 return self._name
515
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000516
517class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000518 """Runs a set of WorkItem that have interdependencies and were WorkItem are
519 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000520
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000521 In gclient's case, Dependencies sometime needs to be run out of order due to
522 From() keyword. This class manages that all the required dependencies are run
523 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000524
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000525 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000526 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000527 def __init__(self, jobs, progress):
528 """jobs specifies the number of concurrent tasks to allow. progress is a
529 Progress instance."""
530 # Set when a thread is done or a new item is enqueued.
531 self.ready_cond = threading.Condition()
532 # Maximum number of concurrent tasks.
533 self.jobs = jobs
534 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000535 self.queued = []
536 # List of strings representing each Dependency.name that was run.
537 self.ran = []
538 # List of items currently running.
539 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000540 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000541 self.exceptions = Queue.Queue()
542 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000543 self.progress = progress
544 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000545 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000546
547 def enqueue(self, d):
548 """Enqueue one Dependency to be executed later once its requirements are
549 satisfied.
550 """
551 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000552 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000553 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000554 self.queued.append(d)
555 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000556 logging.debug('enqueued(%s)' % d.name)
557 if self.progress:
558 self.progress._total = total + 1
559 self.progress.update(0)
560 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000561 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000562 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000563
564 def flush(self, *args, **kwargs):
565 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000566 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000567 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000568 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000569 while True:
570 # Check for task to run first, then wait.
571 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000572 if not self.exceptions.empty():
573 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000574 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000575 self._flush_terminated_threads()
576 if (not self.queued and not self.running or
577 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000578 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000579 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000580
581 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000582 for i in xrange(len(self.queued)):
583 # Verify its requirements.
584 for r in self.queued[i].requirements:
585 if not r in self.ran:
586 # Requirement not met.
587 break
588 else:
589 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000590 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000591 break
592 else:
593 # Couldn't find an item that could run. Break out the outher loop.
594 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000595
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000596 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000597 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000598 break
599 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000600 try:
601 self.ready_cond.wait(10)
602 except KeyboardInterrupt:
603 # Help debugging by printing some information:
604 print >> sys.stderr, (
605 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
606 'Running: %d') % (
607 self.jobs,
608 len(self.queued),
609 ', '.join(self.ran),
610 len(self.running)))
611 for i in self.queued:
612 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
613 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000614 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000615 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000616 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000617
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000618 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000619 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000620 # To get back the stack location correctly, the raise a, b, c form must be
621 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000622 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000623 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000624 if self.progress:
625 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000626
maruel@chromium.org3742c842010-09-09 19:27:14 +0000627 def _flush_terminated_threads(self):
628 """Flush threads that have terminated."""
629 running = self.running
630 self.running = []
631 for t in running:
632 if t.isAlive():
633 self.running.append(t)
634 else:
635 t.join()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000636 sys.stdout.flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000637 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000638 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000639 if t.item.name in self.ran:
640 raise Error(
641 'gclient is confused, "%s" is already in "%s"' % (
642 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000643 if not t.item.name in self.ran:
644 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000645
646 def _run_one_task(self, task_item, args, kwargs):
647 if self.jobs > 1:
648 # Start the thread.
649 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000650 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000651 self.running.append(new_thread)
652 new_thread.start()
653 else:
654 # Run the 'thread' inside the main thread. Don't try to catch any
655 # exception.
656 task_item.run(*args, **kwargs)
657 self.ran.append(task_item.name)
658 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000659 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000660
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000661 class _Worker(threading.Thread):
662 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000663 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000664 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000665 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000666 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000667 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000668 self.args = args
669 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000670
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000671 def run(self):
672 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000673 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000674 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000675 try:
676 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000677 except Exception:
678 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000679 logging.info('Caught exception in thread %s' % self.item.name)
680 logging.info(str(sys.exc_info()))
681 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000682 logging.info('_Worker.run(%s) done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000683
maruel@chromium.org3742c842010-09-09 19:27:14 +0000684 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000685 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000686 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000687 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000688 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000689
690
691def GetEditor(git):
692 """Returns the most plausible editor to use."""
693 if git:
694 editor = os.environ.get('GIT_EDITOR')
695 else:
696 editor = os.environ.get('SVN_EDITOR')
697 if not editor:
698 editor = os.environ.get('EDITOR')
699 if not editor:
700 if sys.platform.startswith('win'):
701 editor = 'notepad'
702 else:
703 editor = 'vim'
704 return editor
705
706
707def RunEditor(content, git):
708 """Opens up the default editor in the system to get the CL description."""
709 file_handle, filename = tempfile.mkstemp(text=True)
710 # Make sure CRLF is handled properly by requiring none.
711 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000712 print >> sys.stderr, (
713 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000714 fileobj = os.fdopen(file_handle, 'w')
715 # Still remove \r if present.
716 fileobj.write(re.sub('\r?\n', '\n', content))
717 fileobj.close()
718
719 try:
720 cmd = '%s %s' % (GetEditor(git), filename)
721 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
722 # Msysgit requires the usage of 'env' to be present.
723 cmd = 'env ' + cmd
724 try:
725 # shell=True to allow the shell to handle all forms of quotes in
726 # $EDITOR.
727 subprocess2.check_call(cmd, shell=True)
728 except subprocess2.CalledProcessError:
729 return None
730 return FileRead(filename)
731 finally:
732 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000733
734
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000735def UpgradeToHttps(url):
736 """Upgrades random urls to https://.
737
738 Do not touch unknown urls like ssh:// or git://.
739 Do not touch http:// urls with a port number,
740 Fixes invalid GAE url.
741 """
742 if not url:
743 return url
744 if not re.match(r'[a-z\-]+\://.*', url):
745 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
746 # relative url and will use http:///foo. Note that it defaults to http://
747 # for compatibility with naked url like "localhost:8080".
748 url = 'http://%s' % url
749 parsed = list(urlparse.urlparse(url))
750 # Do not automatically upgrade http to https if a port number is provided.
751 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
752 parsed[0] = 'https'
753 # Until GAE supports SNI, manually convert the url.
754 if parsed[1] == 'codereview.chromium.org':
755 parsed[1] = 'chromiumcodereview.appspot.com'
756 return urlparse.urlunparse(parsed)
757
758
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000759def ParseCodereviewSettingsContent(content):
760 """Process a codereview.settings file properly."""
761 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
762 try:
763 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
764 except ValueError:
765 raise Error(
766 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000767 def fix_url(key):
768 if keyvals.get(key):
769 keyvals[key] = UpgradeToHttps(keyvals[key])
770 fix_url('CODE_REVIEW_SERVER')
771 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000772 return keyvals