blob: 34f3db84719e2013ae8168a147eeb4eea968dca7 [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.org51e84fb2012-07-03 23:06:21 +000080 with open(filename, mode=mode) as f:
81 # codecs.open() has different behavior than open() on python 2.6.
82 return f.read().decode('utf-8')
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000083
84
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000085def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +000086 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000087 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000088
89
maruel@chromium.orgf9040722011-03-09 14:47:51 +000090def rmtree(path):
91 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000092
maruel@chromium.orgf9040722011-03-09 14:47:51 +000093 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000094
95 shutil.rmtree() doesn't work on Windows if any of the files or directories
96 are read-only, which svn repositories and some .svn files are. We need to
97 be able to force the files to be writable (i.e., deletable) as we traverse
98 the tree.
99
100 Even with all this, Windows still sometimes fails to delete a file, citing
101 a permission error (maybe something to do with antivirus scans or disk
102 indexing). The best suggestion any of the user forums had was to wait a
103 bit and try again, so we do that too. It's hand-waving, but sometimes it
104 works. :/
105
106 On POSIX systems, things are a little bit simpler. The modes of the files
107 to be deleted doesn't matter, only the modes of the directories containing
108 them are significant. As the directory tree is traversed, each directory
109 has its mode set appropriately before descending into it. This should
110 result in the entire tree being removed, with the possible exception of
111 *path itself, because nothing attempts to change the mode of its parent.
112 Doing so would be hazardous, as it's not a directory slated for removal.
113 In the ordinary case, this is not a problem: for our purposes, the user
114 will never lack write permission on *path's parent.
115 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000116 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000117 return
118
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000119 if os.path.islink(path) or not os.path.isdir(path):
120 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000121
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000122 if sys.platform == 'win32':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000123 # Some people don't have the APIs installed. In that case we'll do without.
maruel@chromium.org1edee692011-03-12 19:39:13 +0000124 win32api = None
125 win32con = None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000126 try:
maruel@chromium.org1edee692011-03-12 19:39:13 +0000127 # Unable to import 'XX'
128 # pylint: disable=F0401
129 import win32api, win32con
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000130 except ImportError:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000131 pass
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000132 else:
133 # On POSIX systems, we need the x-bit set on the directory to access it,
134 # the r-bit to see its contents, and the w-bit to remove files from it.
135 # The actual modes of the files within the directory is irrelevant.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000136 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000137
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000138 def remove(func, subpath):
139 if sys.platform == 'win32':
140 os.chmod(subpath, stat.S_IWRITE)
141 if win32api and win32con:
142 win32api.SetFileAttributes(subpath, win32con.FILE_ATTRIBUTE_NORMAL)
143 try:
144 func(subpath)
145 except OSError, e:
146 if e.errno != errno.EACCES or sys.platform != 'win32':
147 raise
148 # Failed to delete, try again after a 100ms sleep.
149 time.sleep(0.1)
150 func(subpath)
151
152 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000153 # If fullpath is a symbolic link that points to a directory, isdir will
154 # be True, but we don't want to descend into that as a directory, we just
155 # want to remove the link. Check islink and treat links as ordinary files
156 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000157 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000158 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000159 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000160 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000161 # Recurse.
162 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000163
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000164 remove(os.rmdir, path)
165
166# TODO(maruel): Rename the references.
167RemoveDirectory = rmtree
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000168
169
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000170def safe_makedirs(tree):
171 """Creates the directory in a safe manner.
172
173 Because multiple threads can create these directories concurently, trap the
174 exception and pass on.
175 """
176 count = 0
177 while not os.path.exists(tree):
178 count += 1
179 try:
180 os.makedirs(tree)
181 except OSError, e:
182 # 17 POSIX, 183 Windows
183 if e.errno not in (17, 183):
184 raise
185 if count > 40:
186 # Give up.
187 raise
188
189
maruel@chromium.org17d01792010-09-01 18:07:10 +0000190def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
191 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000192
maruel@chromium.org17d01792010-09-01 18:07:10 +0000193 If |always| is True, a message indicating what is being done
194 is printed to stdout all the time even if not output is generated. Otherwise
195 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000196 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000197 stdout = kwargs.get('stdout', None) or sys.stdout
198 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000199 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000200 % (' '.join(args), kwargs.get('cwd', '.')))
201 else:
202 filter_fn = kwargs.get('filter_fn', None)
203 def filter_msg(line):
204 if line is None:
205 stdout.write('\n________ running \'%s\' in \'%s\'\n'
206 % (' '.join(args), kwargs.get('cwd', '.')))
207 elif filter_fn:
208 filter_fn(line)
209 kwargs['filter_fn'] = filter_msg
210 kwargs['call_filter_on_first_line'] = True
211 # Obviously.
212 kwargs['print_stdout'] = True
213 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000214
maruel@chromium.org17d01792010-09-01 18:07:10 +0000215
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000216class Wrapper(object):
217 """Wraps an object, acting as a transparent proxy for all properties by
218 default.
219 """
220 def __init__(self, wrapped):
221 self._wrapped = wrapped
222
223 def __getattr__(self, name):
224 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000225
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000226
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000227class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000228 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000229 def __init__(self, wrapped, delay):
230 super(AutoFlush, self).__init__(wrapped)
231 if not hasattr(self, 'lock'):
232 self.lock = threading.Lock()
233 self.__last_flushed_at = time.time()
234 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000235
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000236 @property
237 def autoflush(self):
238 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000239
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000240 def write(self, out, *args, **kwargs):
241 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000242 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000243 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000244 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000245 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000246 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000247 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000248 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000249 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000250 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000251 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000252
253
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000254class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000255 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000256 threads with a NN> prefix.
257 """
258 def __init__(self, wrapped, include_zero=False):
259 super(Annotated, self).__init__(wrapped)
260 if not hasattr(self, 'lock'):
261 self.lock = threading.Lock()
262 self.__output_buffers = {}
263 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000264
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000265 @property
266 def annotated(self):
267 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000268
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000269 def write(self, out):
270 index = getattr(threading.currentThread(), 'index', 0)
271 if not index and not self.__include_zero:
272 # Unindexed threads aren't buffered.
273 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000274
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000275 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000276 try:
277 # Use a dummy array to hold the string so the code can be lockless.
278 # Strings are immutable, requiring to keep a lock for the whole dictionary
279 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000280 if not index in self.__output_buffers:
281 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000282 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000283 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000284 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000285 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000286
287 # Continue lockless.
288 obj[0] += out
289 while '\n' in obj[0]:
290 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000291 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000292 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000293 obj[0] = remaining
294
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000295 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000296 """Flush buffered output."""
297 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000298 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000299 try:
300 # Detect threads no longer existing.
301 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000302 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000303 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000304 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000305 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000306 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000307 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000308 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000309 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000310
311 # Don't keep the lock while writting. Will append \n when it shouldn't.
312 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000313 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000314 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
315 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000316
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000317
318def MakeFileAutoFlush(fileobj, delay=10):
319 autoflush = getattr(fileobj, 'autoflush', None)
320 if autoflush:
321 autoflush.delay = delay
322 return fileobj
323 return AutoFlush(fileobj, delay)
324
325
326def MakeFileAnnotated(fileobj, include_zero=False):
327 if getattr(fileobj, 'annotated', None):
328 return fileobj
329 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000330
331
maruel@chromium.org17d01792010-09-01 18:07:10 +0000332def CheckCallAndFilter(args, stdout=None, filter_fn=None,
333 print_stdout=None, call_filter_on_first_line=False,
334 **kwargs):
335 """Runs a command and calls back a filter function if needed.
336
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000337 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000338 print_stdout: If True, the command's stdout is forwarded to stdout.
339 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000340 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000341 character trimmed.
342 stdout: Can be any bufferable output.
343
344 stderr is always redirected to stdout.
345 """
346 assert print_stdout or filter_fn
347 stdout = stdout or sys.stdout
348 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000349 kid = subprocess2.Popen(
350 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
351 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000352
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000353 # Do a flush of stdout before we begin reading from the subprocess2's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000354 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000355
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000356 # Also, we need to forward stdout to prevent weird re-ordering of output.
357 # This has to be done on a per byte basis to make sure it is not buffered:
358 # normally buffering is done for each line, but if svn requests input, no
359 # end-of-line character is output after the prompt and it would not show up.
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000360 try:
361 in_byte = kid.stdout.read(1)
362 if in_byte:
363 if call_filter_on_first_line:
364 filter_fn(None)
365 in_line = ''
366 while in_byte:
367 if in_byte != '\r':
368 if print_stdout:
369 stdout.write(in_byte)
370 if in_byte != '\n':
371 in_line += in_byte
372 else:
373 filter_fn(in_line)
374 in_line = ''
szager@google.com85d3e3a2011-10-07 17:12:00 +0000375 else:
376 filter_fn(in_line)
377 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000378 in_byte = kid.stdout.read(1)
379 # Flush the rest of buffered output. This is only an issue with
380 # stdout/stderr not ending with a \n.
381 if len(in_line):
382 filter_fn(in_line)
383 rv = kid.wait()
384 except KeyboardInterrupt:
385 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
386 raise
387
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000388 if rv:
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000389 raise subprocess2.CalledProcessError(
390 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000391 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000392
393
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000394def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000395 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000396 real_from_dir = os.path.realpath(from_dir)
397 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000398 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000399 split_path = os.path.split(path)
400 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000401 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000402 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000403
404 # If we did not find the file in the current directory, make sure we are in a
405 # sub directory that is controlled by this configuration.
406 if path != real_from_dir:
407 entries_filename = os.path.join(path, filename + '_entries')
408 if not os.path.exists(entries_filename):
409 # If .gclient_entries does not exist, a previous call to gclient sync
410 # might have failed. In that case, we cannot verify that the .gclient
411 # is the one we want to use. In order to not to cause too much trouble,
412 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000413 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000414 "file you want to use" % (filename, path))
415 return path
416 scope = {}
417 try:
418 exec(FileRead(entries_filename), scope)
419 except SyntaxError, e:
420 SyntaxErrorToError(filename, e)
421 all_directories = scope['entries'].keys()
422 path_to_check = real_from_dir[len(path)+1:]
423 while path_to_check:
424 if path_to_check in all_directories:
425 return path
426 path_to_check = os.path.dirname(path_to_check)
427 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000428
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000429 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000430 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000431
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000432
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000433def PathDifference(root, subpath):
434 """Returns the difference subpath minus root."""
435 root = os.path.realpath(root)
436 subpath = os.path.realpath(subpath)
437 if not subpath.startswith(root):
438 return None
439 # If the root does not have a trailing \ or /, we add it so the returned
440 # path starts immediately after the seperator regardless of whether it is
441 # provided.
442 root = os.path.join(root, '')
443 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000444
445
446def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000447 """Search upwards from the a directory (default: current) to find a file.
448
449 Returns nearest upper-level directory with the passed in file.
450 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000451 if not path:
452 path = os.getcwd()
453 path = os.path.realpath(path)
454 while True:
455 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000456 if os.path.exists(file_path):
457 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000458 (new_path, _) = os.path.split(path)
459 if new_path == path:
460 return None
461 path = new_path
462
463
464def GetGClientRootAndEntries(path=None):
465 """Returns the gclient root and the dict of entries."""
466 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000467 root = FindFileUpwards(config_file, path)
468 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000469 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000470 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000471 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000472 env = {}
473 execfile(config_path, env)
474 config_dir = os.path.dirname(config_path)
475 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000476
477
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000478def lockedmethod(method):
479 """Method decorator that holds self.lock for the duration of the call."""
480 def inner(self, *args, **kwargs):
481 try:
482 try:
483 self.lock.acquire()
484 except KeyboardInterrupt:
485 print >> sys.stderr, 'Was deadlocked'
486 raise
487 return method(self, *args, **kwargs)
488 finally:
489 self.lock.release()
490 return inner
491
492
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000493class WorkItem(object):
494 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000495 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
496 # As a workaround, use a single lock. Yep you read it right. Single lock for
497 # all the 100 objects.
498 lock = threading.Lock()
499
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000500 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000501 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000502 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000503
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000504 def run(self, work_queue):
505 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000506 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000507 pass
508
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000509 @property
510 def name(self):
511 return self._name
512
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000513
514class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000515 """Runs a set of WorkItem that have interdependencies and were WorkItem are
516 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000517
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000518 In gclient's case, Dependencies sometime needs to be run out of order due to
519 From() keyword. This class manages that all the required dependencies are run
520 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000521
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000522 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000523 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000524 def __init__(self, jobs, progress):
525 """jobs specifies the number of concurrent tasks to allow. progress is a
526 Progress instance."""
527 # Set when a thread is done or a new item is enqueued.
528 self.ready_cond = threading.Condition()
529 # Maximum number of concurrent tasks.
530 self.jobs = jobs
531 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000532 self.queued = []
533 # List of strings representing each Dependency.name that was run.
534 self.ran = []
535 # List of items currently running.
536 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000537 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000538 self.exceptions = Queue.Queue()
539 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000540 self.progress = progress
541 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000542 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000543
544 def enqueue(self, d):
545 """Enqueue one Dependency to be executed later once its requirements are
546 satisfied.
547 """
548 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000549 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000550 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000551 self.queued.append(d)
552 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000553 logging.debug('enqueued(%s)' % d.name)
554 if self.progress:
555 self.progress._total = total + 1
556 self.progress.update(0)
557 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000558 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000559 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000560
561 def flush(self, *args, **kwargs):
562 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000563 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000564 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000565 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000566 while True:
567 # Check for task to run first, then wait.
568 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000569 if not self.exceptions.empty():
570 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000571 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000572 self._flush_terminated_threads()
573 if (not self.queued and not self.running or
574 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000575 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000576 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000577
578 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000579 for i in xrange(len(self.queued)):
580 # Verify its requirements.
581 for r in self.queued[i].requirements:
582 if not r in self.ran:
583 # Requirement not met.
584 break
585 else:
586 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000587 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000588 break
589 else:
590 # Couldn't find an item that could run. Break out the outher loop.
591 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000592
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000593 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000594 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000595 break
596 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000597 try:
598 self.ready_cond.wait(10)
599 except KeyboardInterrupt:
600 # Help debugging by printing some information:
601 print >> sys.stderr, (
602 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
603 'Running: %d') % (
604 self.jobs,
605 len(self.queued),
606 ', '.join(self.ran),
607 len(self.running)))
608 for i in self.queued:
609 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
610 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000611 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000612 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000613 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000614
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000615 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000616 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000617 # To get back the stack location correctly, the raise a, b, c form must be
618 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000619 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000620 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000621 if self.progress:
622 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000623
maruel@chromium.org3742c842010-09-09 19:27:14 +0000624 def _flush_terminated_threads(self):
625 """Flush threads that have terminated."""
626 running = self.running
627 self.running = []
628 for t in running:
629 if t.isAlive():
630 self.running.append(t)
631 else:
632 t.join()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000633 sys.stdout.flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000634 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000635 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000636 if t.item.name in self.ran:
637 raise Error(
638 'gclient is confused, "%s" is already in "%s"' % (
639 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000640 if not t.item.name in self.ran:
641 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000642
643 def _run_one_task(self, task_item, args, kwargs):
644 if self.jobs > 1:
645 # Start the thread.
646 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000647 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000648 self.running.append(new_thread)
649 new_thread.start()
650 else:
651 # Run the 'thread' inside the main thread. Don't try to catch any
652 # exception.
653 task_item.run(*args, **kwargs)
654 self.ran.append(task_item.name)
655 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000656 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000657
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000658 class _Worker(threading.Thread):
659 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000660 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000661 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000662 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000663 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000664 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000665 self.args = args
666 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000667
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000668 def run(self):
669 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000670 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000671 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000672 try:
673 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000674 except Exception:
675 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000676 logging.info('Caught exception in thread %s' % self.item.name)
677 logging.info(str(sys.exc_info()))
678 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000679 logging.info('_Worker.run(%s) done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000680
maruel@chromium.org3742c842010-09-09 19:27:14 +0000681 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000682 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000683 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000684 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000685 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000686
687
688def GetEditor(git):
689 """Returns the most plausible editor to use."""
690 if git:
691 editor = os.environ.get('GIT_EDITOR')
692 else:
693 editor = os.environ.get('SVN_EDITOR')
694 if not editor:
695 editor = os.environ.get('EDITOR')
696 if not editor:
697 if sys.platform.startswith('win'):
698 editor = 'notepad'
699 else:
700 editor = 'vim'
701 return editor
702
703
704def RunEditor(content, git):
705 """Opens up the default editor in the system to get the CL description."""
706 file_handle, filename = tempfile.mkstemp(text=True)
707 # Make sure CRLF is handled properly by requiring none.
708 if '\r' in content:
asvitkine@chromium.org0eff22d2011-10-25 16:11:16 +0000709 print >> sys.stderr, (
710 '!! Please remove \\r from your change description !!')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000711 fileobj = os.fdopen(file_handle, 'w')
712 # Still remove \r if present.
713 fileobj.write(re.sub('\r?\n', '\n', content))
714 fileobj.close()
715
716 try:
717 cmd = '%s %s' % (GetEditor(git), filename)
718 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
719 # Msysgit requires the usage of 'env' to be present.
720 cmd = 'env ' + cmd
721 try:
722 # shell=True to allow the shell to handle all forms of quotes in
723 # $EDITOR.
724 subprocess2.check_call(cmd, shell=True)
725 except subprocess2.CalledProcessError:
726 return None
727 return FileRead(filename)
728 finally:
729 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000730
731
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000732def UpgradeToHttps(url):
733 """Upgrades random urls to https://.
734
735 Do not touch unknown urls like ssh:// or git://.
736 Do not touch http:// urls with a port number,
737 Fixes invalid GAE url.
738 """
739 if not url:
740 return url
741 if not re.match(r'[a-z\-]+\://.*', url):
742 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
743 # relative url and will use http:///foo. Note that it defaults to http://
744 # for compatibility with naked url like "localhost:8080".
745 url = 'http://%s' % url
746 parsed = list(urlparse.urlparse(url))
747 # Do not automatically upgrade http to https if a port number is provided.
748 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
749 parsed[0] = 'https'
750 # Until GAE supports SNI, manually convert the url.
751 if parsed[1] == 'codereview.chromium.org':
752 parsed[1] = 'chromiumcodereview.appspot.com'
753 return urlparse.urlunparse(parsed)
754
755
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000756def ParseCodereviewSettingsContent(content):
757 """Process a codereview.settings file properly."""
758 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
759 try:
760 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
761 except ValueError:
762 raise Error(
763 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000764 def fix_url(key):
765 if keyvals.get(key):
766 keyvals[key] = UpgradeToHttps(keyvals[key])
767 fix_url('CODE_REVIEW_SERVER')
768 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000769 return keyvals