blob: 918b489c7b3e244f0b4adfd34a5986f66934d8ba [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.org17d01792010-09-01 18:07:10 +0000174def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
175 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000176
maruel@chromium.org17d01792010-09-01 18:07:10 +0000177 If |always| is True, a message indicating what is being done
178 is printed to stdout all the time even if not output is generated. Otherwise
179 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000180 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000181 stdout = kwargs.get('stdout', None) or sys.stdout
182 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000183 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000184 % (' '.join(args), kwargs.get('cwd', '.')))
185 else:
186 filter_fn = kwargs.get('filter_fn', None)
187 def filter_msg(line):
188 if line is None:
189 stdout.write('\n________ running \'%s\' in \'%s\'\n'
190 % (' '.join(args), kwargs.get('cwd', '.')))
191 elif filter_fn:
192 filter_fn(line)
193 kwargs['filter_fn'] = filter_msg
194 kwargs['call_filter_on_first_line'] = True
195 # Obviously.
196 kwargs['print_stdout'] = True
197 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000198
maruel@chromium.org17d01792010-09-01 18:07:10 +0000199
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000200def SoftClone(obj):
201 """Clones an object. copy.copy() doesn't work on 'file' objects."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000202 if obj.__class__.__name__ == 'SoftCloned':
203 return obj
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000204 class SoftCloned(object):
205 pass
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000206 new_obj = SoftCloned()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000207 for member in dir(obj):
208 if member.startswith('_'):
209 continue
210 setattr(new_obj, member, getattr(obj, member))
211 return new_obj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000212
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000213
214def MakeFileAutoFlush(fileobj, delay=10):
215 """Creates a file object clone to automatically flush after N seconds."""
216 if hasattr(fileobj, 'last_flushed_at'):
217 # Already patched. Just update delay.
218 fileobj.delay = delay
219 return fileobj
220
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000221 # Attribute 'XXX' defined outside __init__
222 # pylint: disable=W0201
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000223 new_fileobj = SoftClone(fileobj)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000224 if not hasattr(new_fileobj, 'lock'):
225 new_fileobj.lock = threading.Lock()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000226 new_fileobj.last_flushed_at = time.time()
227 new_fileobj.delay = delay
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000228 new_fileobj.old_auto_flush_write = new_fileobj.write
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000229 # Silence pylint.
230 new_fileobj.flush = fileobj.flush
231
232 def auto_flush_write(out):
233 new_fileobj.old_auto_flush_write(out)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000234 should_flush = False
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000235 new_fileobj.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000236 try:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000237 if (new_fileobj.delay and
238 (time.time() - new_fileobj.last_flushed_at) > new_fileobj.delay):
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000239 should_flush = True
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000240 new_fileobj.last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000241 finally:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000242 new_fileobj.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000243 if should_flush:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000244 new_fileobj.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000245
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000246 new_fileobj.write = auto_flush_write
247 return new_fileobj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000248
249
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000250def MakeFileAnnotated(fileobj, include_zero=False):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000251 """Creates a file object clone to automatically prepends every line in worker
252 threads with a NN> prefix."""
253 if hasattr(fileobj, 'output_buffers'):
254 # Already patched.
255 return fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000256
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000257 # Attribute 'XXX' defined outside __init__
258 # pylint: disable=W0201
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000259 new_fileobj = SoftClone(fileobj)
260 if not hasattr(new_fileobj, 'lock'):
261 new_fileobj.lock = threading.Lock()
262 new_fileobj.output_buffers = {}
263 new_fileobj.old_annotated_write = new_fileobj.write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000264
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000265 def annotated_write(out):
266 index = getattr(threading.currentThread(), 'index', None)
267 if index is None:
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000268 if not include_zero:
269 # Unindexed threads aren't buffered.
270 new_fileobj.old_annotated_write(out)
271 return
272 index = 0
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000273
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000274 new_fileobj.lock.acquire()
275 try:
276 # Use a dummy array to hold the string so the code can be lockless.
277 # Strings are immutable, requiring to keep a lock for the whole dictionary
278 # otherwise. Using an array is faster than using a dummy object.
279 if not index in new_fileobj.output_buffers:
280 obj = new_fileobj.output_buffers[index] = ['']
281 else:
282 obj = new_fileobj.output_buffers[index]
283 finally:
284 new_fileobj.lock.release()
285
286 # Continue lockless.
287 obj[0] += out
288 while '\n' in obj[0]:
289 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000290 if line:
291 new_fileobj.old_annotated_write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000292 obj[0] = remaining
293
294 def full_flush():
295 """Flush buffered output."""
296 orphans = []
297 new_fileobj.lock.acquire()
298 try:
299 # Detect threads no longer existing.
300 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000301 indexes = filter(None, indexes)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000302 for index in new_fileobj.output_buffers:
303 if not index in indexes:
304 orphans.append((index, new_fileobj.output_buffers[index][0]))
305 for orphan in orphans:
306 del new_fileobj.output_buffers[orphan[0]]
307 finally:
308 new_fileobj.lock.release()
309
310 # Don't keep the lock while writting. Will append \n when it shouldn't.
311 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000312 if orphan[1]:
313 new_fileobj.old_annotated_write('%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000314
315 new_fileobj.write = annotated_write
316 new_fileobj.full_flush = full_flush
317 return new_fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000318
319
maruel@chromium.org17d01792010-09-01 18:07:10 +0000320def CheckCallAndFilter(args, stdout=None, filter_fn=None,
321 print_stdout=None, call_filter_on_first_line=False,
322 **kwargs):
323 """Runs a command and calls back a filter function if needed.
324
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000325 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000326 print_stdout: If True, the command's stdout is forwarded to stdout.
327 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000328 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000329 character trimmed.
330 stdout: Can be any bufferable output.
331
332 stderr is always redirected to stdout.
333 """
334 assert print_stdout or filter_fn
335 stdout = stdout or sys.stdout
336 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000337 kid = subprocess2.Popen(
338 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
339 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000340
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000341 # Do a flush of stdout before we begin reading from the subprocess2's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000342 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000343
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000344 # Also, we need to forward stdout to prevent weird re-ordering of output.
345 # This has to be done on a per byte basis to make sure it is not buffered:
346 # normally buffering is done for each line, but if svn requests input, no
347 # end-of-line character is output after the prompt and it would not show up.
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000348 try:
349 in_byte = kid.stdout.read(1)
350 if in_byte:
351 if call_filter_on_first_line:
352 filter_fn(None)
353 in_line = ''
354 while in_byte:
355 if in_byte != '\r':
356 if print_stdout:
357 stdout.write(in_byte)
358 if in_byte != '\n':
359 in_line += in_byte
360 else:
361 filter_fn(in_line)
362 in_line = ''
szager@google.com85d3e3a2011-10-07 17:12:00 +0000363 else:
364 filter_fn(in_line)
365 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000366 in_byte = kid.stdout.read(1)
367 # Flush the rest of buffered output. This is only an issue with
368 # stdout/stderr not ending with a \n.
369 if len(in_line):
370 filter_fn(in_line)
371 rv = kid.wait()
372 except KeyboardInterrupt:
373 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
374 raise
375
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000376 if rv:
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000377 raise subprocess2.CalledProcessError(
378 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000379 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000380
381
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000382def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000383 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000384 real_from_dir = os.path.realpath(from_dir)
385 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000386 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000387 split_path = os.path.split(path)
388 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000389 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000390 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000391
392 # If we did not find the file in the current directory, make sure we are in a
393 # sub directory that is controlled by this configuration.
394 if path != real_from_dir:
395 entries_filename = os.path.join(path, filename + '_entries')
396 if not os.path.exists(entries_filename):
397 # If .gclient_entries does not exist, a previous call to gclient sync
398 # might have failed. In that case, we cannot verify that the .gclient
399 # is the one we want to use. In order to not to cause too much trouble,
400 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000401 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000402 "file you want to use" % (filename, path))
403 return path
404 scope = {}
405 try:
406 exec(FileRead(entries_filename), scope)
407 except SyntaxError, e:
408 SyntaxErrorToError(filename, e)
409 all_directories = scope['entries'].keys()
410 path_to_check = real_from_dir[len(path)+1:]
411 while path_to_check:
412 if path_to_check in all_directories:
413 return path
414 path_to_check = os.path.dirname(path_to_check)
415 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000416
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000417 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000418 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000419
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000420
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000421def PathDifference(root, subpath):
422 """Returns the difference subpath minus root."""
423 root = os.path.realpath(root)
424 subpath = os.path.realpath(subpath)
425 if not subpath.startswith(root):
426 return None
427 # If the root does not have a trailing \ or /, we add it so the returned
428 # path starts immediately after the seperator regardless of whether it is
429 # provided.
430 root = os.path.join(root, '')
431 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000432
433
434def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000435 """Search upwards from the a directory (default: current) to find a file.
436
437 Returns nearest upper-level directory with the passed in file.
438 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000439 if not path:
440 path = os.getcwd()
441 path = os.path.realpath(path)
442 while True:
443 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000444 if os.path.exists(file_path):
445 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000446 (new_path, _) = os.path.split(path)
447 if new_path == path:
448 return None
449 path = new_path
450
451
452def GetGClientRootAndEntries(path=None):
453 """Returns the gclient root and the dict of entries."""
454 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000455 root = FindFileUpwards(config_file, path)
456 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000457 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000458 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000459 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000460 env = {}
461 execfile(config_path, env)
462 config_dir = os.path.dirname(config_path)
463 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000464
465
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000466def lockedmethod(method):
467 """Method decorator that holds self.lock for the duration of the call."""
468 def inner(self, *args, **kwargs):
469 try:
470 try:
471 self.lock.acquire()
472 except KeyboardInterrupt:
473 print >> sys.stderr, 'Was deadlocked'
474 raise
475 return method(self, *args, **kwargs)
476 finally:
477 self.lock.release()
478 return inner
479
480
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000481class WorkItem(object):
482 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000483 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
484 # As a workaround, use a single lock. Yep you read it right. Single lock for
485 # all the 100 objects.
486 lock = threading.Lock()
487
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000488 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000489 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000490 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000491
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000492 def run(self, work_queue):
493 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000494 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000495 pass
496
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000497 @property
498 def name(self):
499 return self._name
500
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000501
502class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000503 """Runs a set of WorkItem that have interdependencies and were WorkItem are
504 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000505
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000506 In gclient's case, Dependencies sometime needs to be run out of order due to
507 From() keyword. This class manages that all the required dependencies are run
508 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000509
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000510 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000511 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000512 def __init__(self, jobs, progress):
513 """jobs specifies the number of concurrent tasks to allow. progress is a
514 Progress instance."""
515 # Set when a thread is done or a new item is enqueued.
516 self.ready_cond = threading.Condition()
517 # Maximum number of concurrent tasks.
518 self.jobs = jobs
519 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000520 self.queued = []
521 # List of strings representing each Dependency.name that was run.
522 self.ran = []
523 # List of items currently running.
524 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000525 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000526 self.exceptions = Queue.Queue()
527 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000528 self.progress = progress
529 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000530 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000531
532 def enqueue(self, d):
533 """Enqueue one Dependency to be executed later once its requirements are
534 satisfied.
535 """
536 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000537 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000538 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000539 self.queued.append(d)
540 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000541 logging.debug('enqueued(%s)' % d.name)
542 if self.progress:
543 self.progress._total = total + 1
544 self.progress.update(0)
545 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000546 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000547 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000548
549 def flush(self, *args, **kwargs):
550 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000551 kwargs['work_queue'] = self
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.org9e5317a2010-08-13 20:35:11 +0000554 while True:
555 # Check for task to run first, then wait.
556 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000557 if not self.exceptions.empty():
558 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000559 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000560 self._flush_terminated_threads()
561 if (not self.queued and not self.running or
562 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000563 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000564 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000565
566 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000567 for i in xrange(len(self.queued)):
568 # Verify its requirements.
569 for r in self.queued[i].requirements:
570 if not r in self.ran:
571 # Requirement not met.
572 break
573 else:
574 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000575 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000576 break
577 else:
578 # Couldn't find an item that could run. Break out the outher loop.
579 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000580
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000581 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000582 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000583 break
584 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000585 try:
586 self.ready_cond.wait(10)
587 except KeyboardInterrupt:
588 # Help debugging by printing some information:
589 print >> sys.stderr, (
590 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
591 'Running: %d') % (
592 self.jobs,
593 len(self.queued),
594 ', '.join(self.ran),
595 len(self.running)))
596 for i in self.queued:
597 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
598 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000599 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000600 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000601 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000602
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000603 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000604 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000605 # To get back the stack location correctly, the raise a, b, c form must be
606 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000607 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000608 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000609 if self.progress:
610 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000611
maruel@chromium.org3742c842010-09-09 19:27:14 +0000612 def _flush_terminated_threads(self):
613 """Flush threads that have terminated."""
614 running = self.running
615 self.running = []
616 for t in running:
617 if t.isAlive():
618 self.running.append(t)
619 else:
620 t.join()
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000621 sys.stdout.full_flush() # pylint: disable=E1101
maruel@chromium.org3742c842010-09-09 19:27:14 +0000622 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000623 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000624 if t.item.name in self.ran:
625 raise Error(
626 'gclient is confused, "%s" is already in "%s"' % (
627 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000628 if not t.item.name in self.ran:
629 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000630
631 def _run_one_task(self, task_item, args, kwargs):
632 if self.jobs > 1:
633 # Start the thread.
634 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000635 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000636 self.running.append(new_thread)
637 new_thread.start()
638 else:
639 # Run the 'thread' inside the main thread. Don't try to catch any
640 # exception.
641 task_item.run(*args, **kwargs)
642 self.ran.append(task_item.name)
643 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000644 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000645
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000646 class _Worker(threading.Thread):
647 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000648 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000649 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000650 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000651 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000652 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000653 self.args = args
654 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000655
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000656 def run(self):
657 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000658 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000659 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000660 try:
661 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000662 except Exception:
663 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000664 logging.info('Caught exception in thread %s' % self.item.name)
665 logging.info(str(sys.exc_info()))
666 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000667 logging.info('_Worker.run(%s) done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000668
maruel@chromium.org3742c842010-09-09 19:27:14 +0000669 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000670 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000671 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000672 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000673 work_queue.ready_cond.release()