blob: 1981f36e70e07b26eb43052368d298b548915ea8 [file] [log] [blame]
maruel@chromium.orgca0f8392011-09-08 17:15:15 +00001# Copyright (c) 2011 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.org167b9e62009-09-17 17:41:02 +00007import errno
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +00008import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00009import os
maruel@chromium.org3742c842010-09-09 19:27:14 +000010import Queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000011import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000012import stat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000013import sys
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000014import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000015import time
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000016
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000017import subprocess2
18
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000019
maruel@chromium.org66c83e62010-09-07 14:18:45 +000020class Error(Exception):
21 """gclient exception class."""
22 pass
23
24
msb@chromium.orgac915bb2009-11-13 17:03:01 +000025def SplitUrlRevision(url):
26 """Splits url and returns a two-tuple: url, rev"""
27 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000028 # Make sure ssh://user-name@example.com/~/test.git@stable works
29 regex = r'(ssh://(?:[-\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000030 components = re.search(regex, url).groups()
31 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000032 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000033 if len(components) == 1:
34 components += [None]
35 return tuple(components)
36
37
floitsch@google.comeaab7842011-04-28 09:07:58 +000038def IsDateRevision(revision):
39 """Returns true if the given revision is of the form "{ ... }"."""
40 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
41
42
43def MakeDateRevision(date):
44 """Returns a revision representing the latest revision before the given
45 date."""
46 return "{" + date + "}"
47
48
maruel@chromium.org5990f9d2010-07-07 18:02:58 +000049def SyntaxErrorToError(filename, e):
50 """Raises a gclient_utils.Error exception with the human readable message"""
51 try:
52 # Try to construct a human readable error message
53 if filename:
54 error_message = 'There is a syntax error in %s\n' % filename
55 else:
56 error_message = 'There is a syntax error\n'
57 error_message += 'Line #%s, character %s: "%s"' % (
58 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
59 except:
60 # Something went wrong, re-raise the original exception
61 raise e
62 else:
63 raise Error(error_message)
64
65
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000066class PrintableObject(object):
67 def __str__(self):
68 output = ''
69 for i in dir(self):
70 if i.startswith('__'):
71 continue
72 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
73 return output
74
75
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000076def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000077 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000078 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000079 try:
80 content = f.read()
81 finally:
82 f.close()
83 return content
84
85
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000086def FileWrite(filename, content, mode='w'):
87 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000088 try:
89 f.write(content)
90 finally:
91 f.close()
92
93
maruel@chromium.orgf9040722011-03-09 14:47:51 +000094def rmtree(path):
95 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000096
maruel@chromium.orgf9040722011-03-09 14:47:51 +000097 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000098
99 shutil.rmtree() doesn't work on Windows if any of the files or directories
100 are read-only, which svn repositories and some .svn files are. We need to
101 be able to force the files to be writable (i.e., deletable) as we traverse
102 the tree.
103
104 Even with all this, Windows still sometimes fails to delete a file, citing
105 a permission error (maybe something to do with antivirus scans or disk
106 indexing). The best suggestion any of the user forums had was to wait a
107 bit and try again, so we do that too. It's hand-waving, but sometimes it
108 works. :/
109
110 On POSIX systems, things are a little bit simpler. The modes of the files
111 to be deleted doesn't matter, only the modes of the directories containing
112 them are significant. As the directory tree is traversed, each directory
113 has its mode set appropriately before descending into it. This should
114 result in the entire tree being removed, with the possible exception of
115 *path itself, because nothing attempts to change the mode of its parent.
116 Doing so would be hazardous, as it's not a directory slated for removal.
117 In the ordinary case, this is not a problem: for our purposes, the user
118 will never lack write permission on *path's parent.
119 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000120 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000121 return
122
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000123 if os.path.islink(path) or not os.path.isdir(path):
124 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000125
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000126 if sys.platform == 'win32':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000127 # Some people don't have the APIs installed. In that case we'll do without.
maruel@chromium.org1edee692011-03-12 19:39:13 +0000128 win32api = None
129 win32con = None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000130 try:
maruel@chromium.org1edee692011-03-12 19:39:13 +0000131 # Unable to import 'XX'
132 # pylint: disable=F0401
133 import win32api, win32con
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000134 except ImportError:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000135 pass
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000136 else:
137 # On POSIX systems, we need the x-bit set on the directory to access it,
138 # the r-bit to see its contents, and the w-bit to remove files from it.
139 # The actual modes of the files within the directory is irrelevant.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000140 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000141
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000142 def remove(func, subpath):
143 if sys.platform == 'win32':
144 os.chmod(subpath, stat.S_IWRITE)
145 if win32api and win32con:
146 win32api.SetFileAttributes(subpath, win32con.FILE_ATTRIBUTE_NORMAL)
147 try:
148 func(subpath)
149 except OSError, e:
150 if e.errno != errno.EACCES or sys.platform != 'win32':
151 raise
152 # Failed to delete, try again after a 100ms sleep.
153 time.sleep(0.1)
154 func(subpath)
155
156 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000157 # If fullpath is a symbolic link that points to a directory, isdir will
158 # be True, but we don't want to descend into that as a directory, we just
159 # want to remove the link. Check islink and treat links as ordinary files
160 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000161 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000162 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000163 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000164 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000165 # Recurse.
166 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000167
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000168 remove(os.rmdir, path)
169
170# TODO(maruel): Rename the references.
171RemoveDirectory = rmtree
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000172
173
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000174def safe_makedirs(tree):
175 """Creates the directory in a safe manner.
176
177 Because multiple threads can create these directories concurently, trap the
178 exception and pass on.
179 """
180 count = 0
181 while not os.path.exists(tree):
182 count += 1
183 try:
184 os.makedirs(tree)
185 except OSError, e:
186 # 17 POSIX, 183 Windows
187 if e.errno not in (17, 183):
188 raise
189 if count > 40:
190 # Give up.
191 raise
192
193
maruel@chromium.org17d01792010-09-01 18:07:10 +0000194def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
195 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000196
maruel@chromium.org17d01792010-09-01 18:07:10 +0000197 If |always| is True, a message indicating what is being done
198 is printed to stdout all the time even if not output is generated. Otherwise
199 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000200 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000201 stdout = kwargs.get('stdout', None) or sys.stdout
202 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000203 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000204 % (' '.join(args), kwargs.get('cwd', '.')))
205 else:
206 filter_fn = kwargs.get('filter_fn', None)
207 def filter_msg(line):
208 if line is None:
209 stdout.write('\n________ running \'%s\' in \'%s\'\n'
210 % (' '.join(args), kwargs.get('cwd', '.')))
211 elif filter_fn:
212 filter_fn(line)
213 kwargs['filter_fn'] = filter_msg
214 kwargs['call_filter_on_first_line'] = True
215 # Obviously.
216 kwargs['print_stdout'] = True
217 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000218
maruel@chromium.org17d01792010-09-01 18:07:10 +0000219
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000220class Wrapper(object):
221 """Wraps an object, acting as a transparent proxy for all properties by
222 default.
223 """
224 def __init__(self, wrapped):
225 self._wrapped = wrapped
226
227 def __getattr__(self, name):
228 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000229
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000230
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000231class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000232 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000233 def __init__(self, wrapped, delay):
234 super(AutoFlush, self).__init__(wrapped)
235 if not hasattr(self, 'lock'):
236 self.lock = threading.Lock()
237 self.__last_flushed_at = time.time()
238 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000239
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000240 @property
241 def autoflush(self):
242 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000243
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000244 def write(self, out, *args, **kwargs):
245 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000246 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000247 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000248 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000249 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000250 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000251 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000252 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000253 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000254 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000255 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000256
257
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000258class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000259 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000260 threads with a NN> prefix.
261 """
262 def __init__(self, wrapped, include_zero=False):
263 super(Annotated, self).__init__(wrapped)
264 if not hasattr(self, 'lock'):
265 self.lock = threading.Lock()
266 self.__output_buffers = {}
267 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000268
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000269 @property
270 def annotated(self):
271 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000272
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000273 def write(self, out):
274 index = getattr(threading.currentThread(), 'index', 0)
275 if not index and not self.__include_zero:
276 # Unindexed threads aren't buffered.
277 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000278
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000279 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000280 try:
281 # Use a dummy array to hold the string so the code can be lockless.
282 # Strings are immutable, requiring to keep a lock for the whole dictionary
283 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000284 if not index in self.__output_buffers:
285 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000286 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000287 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000288 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000289 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000290
291 # Continue lockless.
292 obj[0] += out
293 while '\n' in obj[0]:
294 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000295 if line:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000296 self._wrapped.write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000297 obj[0] = remaining
298
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000299 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000300 """Flush buffered output."""
301 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000302 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000303 try:
304 # Detect threads no longer existing.
305 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000306 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000307 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000308 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000309 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000310 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000311 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000312 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000313 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000314
315 # Don't keep the lock while writting. Will append \n when it shouldn't.
316 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000317 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000318 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
319 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000320
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000321
322def MakeFileAutoFlush(fileobj, delay=10):
323 autoflush = getattr(fileobj, 'autoflush', None)
324 if autoflush:
325 autoflush.delay = delay
326 return fileobj
327 return AutoFlush(fileobj, delay)
328
329
330def MakeFileAnnotated(fileobj, include_zero=False):
331 if getattr(fileobj, 'annotated', None):
332 return fileobj
333 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000334
335
maruel@chromium.org17d01792010-09-01 18:07:10 +0000336def CheckCallAndFilter(args, stdout=None, filter_fn=None,
337 print_stdout=None, call_filter_on_first_line=False,
338 **kwargs):
339 """Runs a command and calls back a filter function if needed.
340
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000341 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000342 print_stdout: If True, the command's stdout is forwarded to stdout.
343 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000344 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000345 character trimmed.
346 stdout: Can be any bufferable output.
347
348 stderr is always redirected to stdout.
349 """
350 assert print_stdout or filter_fn
351 stdout = stdout or sys.stdout
352 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000353 kid = subprocess2.Popen(
354 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
355 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000356
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000357 # Do a flush of stdout before we begin reading from the subprocess2's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000358 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000359
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000360 # Also, we need to forward stdout to prevent weird re-ordering of output.
361 # This has to be done on a per byte basis to make sure it is not buffered:
362 # normally buffering is done for each line, but if svn requests input, no
363 # end-of-line character is output after the prompt and it would not show up.
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000364 try:
365 in_byte = kid.stdout.read(1)
366 if in_byte:
367 if call_filter_on_first_line:
368 filter_fn(None)
369 in_line = ''
370 while in_byte:
371 if in_byte != '\r':
372 if print_stdout:
373 stdout.write(in_byte)
374 if in_byte != '\n':
375 in_line += in_byte
376 else:
377 filter_fn(in_line)
378 in_line = ''
szager@google.com85d3e3a2011-10-07 17:12:00 +0000379 else:
380 filter_fn(in_line)
381 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000382 in_byte = kid.stdout.read(1)
383 # Flush the rest of buffered output. This is only an issue with
384 # stdout/stderr not ending with a \n.
385 if len(in_line):
386 filter_fn(in_line)
387 rv = kid.wait()
388 except KeyboardInterrupt:
389 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
390 raise
391
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000392 if rv:
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000393 raise subprocess2.CalledProcessError(
394 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000395 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000396
397
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000398def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000399 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000400 real_from_dir = os.path.realpath(from_dir)
401 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000402 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000403 split_path = os.path.split(path)
404 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000405 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000406 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000407
408 # If we did not find the file in the current directory, make sure we are in a
409 # sub directory that is controlled by this configuration.
410 if path != real_from_dir:
411 entries_filename = os.path.join(path, filename + '_entries')
412 if not os.path.exists(entries_filename):
413 # If .gclient_entries does not exist, a previous call to gclient sync
414 # might have failed. In that case, we cannot verify that the .gclient
415 # is the one we want to use. In order to not to cause too much trouble,
416 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000417 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000418 "file you want to use" % (filename, path))
419 return path
420 scope = {}
421 try:
422 exec(FileRead(entries_filename), scope)
423 except SyntaxError, e:
424 SyntaxErrorToError(filename, e)
425 all_directories = scope['entries'].keys()
426 path_to_check = real_from_dir[len(path)+1:]
427 while path_to_check:
428 if path_to_check in all_directories:
429 return path
430 path_to_check = os.path.dirname(path_to_check)
431 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000432
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000433 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000434 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000435
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000436
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000437def PathDifference(root, subpath):
438 """Returns the difference subpath minus root."""
439 root = os.path.realpath(root)
440 subpath = os.path.realpath(subpath)
441 if not subpath.startswith(root):
442 return None
443 # If the root does not have a trailing \ or /, we add it so the returned
444 # path starts immediately after the seperator regardless of whether it is
445 # provided.
446 root = os.path.join(root, '')
447 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000448
449
450def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000451 """Search upwards from the a directory (default: current) to find a file.
452
453 Returns nearest upper-level directory with the passed in file.
454 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000455 if not path:
456 path = os.getcwd()
457 path = os.path.realpath(path)
458 while True:
459 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000460 if os.path.exists(file_path):
461 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000462 (new_path, _) = os.path.split(path)
463 if new_path == path:
464 return None
465 path = new_path
466
467
468def GetGClientRootAndEntries(path=None):
469 """Returns the gclient root and the dict of entries."""
470 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000471 root = FindFileUpwards(config_file, path)
472 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000473 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000474 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000475 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000476 env = {}
477 execfile(config_path, env)
478 config_dir = os.path.dirname(config_path)
479 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000480
481
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000482def lockedmethod(method):
483 """Method decorator that holds self.lock for the duration of the call."""
484 def inner(self, *args, **kwargs):
485 try:
486 try:
487 self.lock.acquire()
488 except KeyboardInterrupt:
489 print >> sys.stderr, 'Was deadlocked'
490 raise
491 return method(self, *args, **kwargs)
492 finally:
493 self.lock.release()
494 return inner
495
496
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000497class WorkItem(object):
498 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000499 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
500 # As a workaround, use a single lock. Yep you read it right. Single lock for
501 # all the 100 objects.
502 lock = threading.Lock()
503
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000504 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000505 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000506 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000507
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000508 def run(self, work_queue):
509 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000510 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000511 pass
512
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000513 @property
514 def name(self):
515 return self._name
516
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000517
518class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000519 """Runs a set of WorkItem that have interdependencies and were WorkItem are
520 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000521
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000522 In gclient's case, Dependencies sometime needs to be run out of order due to
523 From() keyword. This class manages that all the required dependencies are run
524 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000525
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000526 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000527 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000528 def __init__(self, jobs, progress):
529 """jobs specifies the number of concurrent tasks to allow. progress is a
530 Progress instance."""
531 # Set when a thread is done or a new item is enqueued.
532 self.ready_cond = threading.Condition()
533 # Maximum number of concurrent tasks.
534 self.jobs = jobs
535 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000536 self.queued = []
537 # List of strings representing each Dependency.name that was run.
538 self.ran = []
539 # List of items currently running.
540 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000541 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000542 self.exceptions = Queue.Queue()
543 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000544 self.progress = progress
545 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000546 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000547
548 def enqueue(self, d):
549 """Enqueue one Dependency to be executed later once its requirements are
550 satisfied.
551 """
552 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000553 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000554 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000555 self.queued.append(d)
556 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000557 logging.debug('enqueued(%s)' % d.name)
558 if self.progress:
559 self.progress._total = total + 1
560 self.progress.update(0)
561 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000562 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000563 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000564
565 def flush(self, *args, **kwargs):
566 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000567 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000568 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000569 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000570 while True:
571 # Check for task to run first, then wait.
572 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000573 if not self.exceptions.empty():
574 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000575 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000576 self._flush_terminated_threads()
577 if (not self.queued and not self.running or
578 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000579 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000580 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000581
582 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000583 for i in xrange(len(self.queued)):
584 # Verify its requirements.
585 for r in self.queued[i].requirements:
586 if not r in self.ran:
587 # Requirement not met.
588 break
589 else:
590 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000591 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000592 break
593 else:
594 # Couldn't find an item that could run. Break out the outher loop.
595 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000596
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000597 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000598 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000599 break
600 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000601 try:
602 self.ready_cond.wait(10)
603 except KeyboardInterrupt:
604 # Help debugging by printing some information:
605 print >> sys.stderr, (
606 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
607 'Running: %d') % (
608 self.jobs,
609 len(self.queued),
610 ', '.join(self.ran),
611 len(self.running)))
612 for i in self.queued:
613 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
614 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000615 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000616 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000617 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000618
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000619 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000620 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000621 # To get back the stack location correctly, the raise a, b, c form must be
622 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000623 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000624 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000625 if self.progress:
626 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000627
maruel@chromium.org3742c842010-09-09 19:27:14 +0000628 def _flush_terminated_threads(self):
629 """Flush threads that have terminated."""
630 running = self.running
631 self.running = []
632 for t in running:
633 if t.isAlive():
634 self.running.append(t)
635 else:
636 t.join()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000637 sys.stdout.flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000638 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000639 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000640 if t.item.name in self.ran:
641 raise Error(
642 'gclient is confused, "%s" is already in "%s"' % (
643 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000644 if not t.item.name in self.ran:
645 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000646
647 def _run_one_task(self, task_item, args, kwargs):
648 if self.jobs > 1:
649 # Start the thread.
650 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000651 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000652 self.running.append(new_thread)
653 new_thread.start()
654 else:
655 # Run the 'thread' inside the main thread. Don't try to catch any
656 # exception.
657 task_item.run(*args, **kwargs)
658 self.ran.append(task_item.name)
659 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000660 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000661
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000662 class _Worker(threading.Thread):
663 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000664 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000665 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000666 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000667 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000668 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000669 self.args = args
670 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000671
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000672 def run(self):
673 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000674 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000675 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000676 try:
677 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000678 except Exception:
679 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000680 logging.info('Caught exception in thread %s' % self.item.name)
681 logging.info(str(sys.exc_info()))
682 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000683 logging.info('_Worker.run(%s) done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000684
maruel@chromium.org3742c842010-09-09 19:27:14 +0000685 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000686 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000687 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000688 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000689 work_queue.ready_cond.release()