blob: e65893468469fed10c25c2c1f031d2075f8a7123 [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.orge0de9cb2010-09-17 15:07:14 +0000220def SoftClone(obj):
221 """Clones an object. copy.copy() doesn't work on 'file' objects."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000222 if obj.__class__.__name__ == 'SoftCloned':
223 return obj
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000224 class SoftCloned(object):
225 pass
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000226 new_obj = SoftCloned()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000227 for member in dir(obj):
228 if member.startswith('_'):
229 continue
230 setattr(new_obj, member, getattr(obj, member))
231 return new_obj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000232
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000233
234def MakeFileAutoFlush(fileobj, delay=10):
235 """Creates a file object clone to automatically flush after N seconds."""
236 if hasattr(fileobj, 'last_flushed_at'):
237 # Already patched. Just update delay.
238 fileobj.delay = delay
239 return fileobj
240
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000241 # Attribute 'XXX' defined outside __init__
242 # pylint: disable=W0201
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000243 new_fileobj = SoftClone(fileobj)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000244 if not hasattr(new_fileobj, 'lock'):
245 new_fileobj.lock = threading.Lock()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000246 new_fileobj.last_flushed_at = time.time()
247 new_fileobj.delay = delay
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000248 new_fileobj.old_auto_flush_write = new_fileobj.write
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000249 # Silence pylint.
250 new_fileobj.flush = fileobj.flush
251
252 def auto_flush_write(out):
253 new_fileobj.old_auto_flush_write(out)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000254 should_flush = False
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000255 new_fileobj.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000256 try:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000257 if (new_fileobj.delay and
258 (time.time() - new_fileobj.last_flushed_at) > new_fileobj.delay):
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000259 should_flush = True
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000260 new_fileobj.last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000261 finally:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000262 new_fileobj.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000263 if should_flush:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000264 new_fileobj.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000265
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000266 new_fileobj.write = auto_flush_write
267 return new_fileobj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000268
269
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000270def MakeFileAnnotated(fileobj, include_zero=False):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000271 """Creates a file object clone to automatically prepends every line in worker
272 threads with a NN> prefix."""
273 if hasattr(fileobj, 'output_buffers'):
274 # Already patched.
275 return fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000276
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000277 # Attribute 'XXX' defined outside __init__
278 # pylint: disable=W0201
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000279 new_fileobj = SoftClone(fileobj)
280 if not hasattr(new_fileobj, 'lock'):
281 new_fileobj.lock = threading.Lock()
282 new_fileobj.output_buffers = {}
283 new_fileobj.old_annotated_write = new_fileobj.write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000284
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000285 def annotated_write(out):
286 index = getattr(threading.currentThread(), 'index', None)
287 if index is None:
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000288 if not include_zero:
289 # Unindexed threads aren't buffered.
290 new_fileobj.old_annotated_write(out)
291 return
292 index = 0
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000293
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000294 new_fileobj.lock.acquire()
295 try:
296 # Use a dummy array to hold the string so the code can be lockless.
297 # Strings are immutable, requiring to keep a lock for the whole dictionary
298 # otherwise. Using an array is faster than using a dummy object.
299 if not index in new_fileobj.output_buffers:
300 obj = new_fileobj.output_buffers[index] = ['']
301 else:
302 obj = new_fileobj.output_buffers[index]
303 finally:
304 new_fileobj.lock.release()
305
306 # Continue lockless.
307 obj[0] += out
308 while '\n' in obj[0]:
309 line, remaining = obj[0].split('\n', 1)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000310 if line:
311 new_fileobj.old_annotated_write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000312 obj[0] = remaining
313
314 def full_flush():
315 """Flush buffered output."""
316 orphans = []
317 new_fileobj.lock.acquire()
318 try:
319 # Detect threads no longer existing.
320 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000321 indexes = filter(None, indexes)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000322 for index in new_fileobj.output_buffers:
323 if not index in indexes:
324 orphans.append((index, new_fileobj.output_buffers[index][0]))
325 for orphan in orphans:
326 del new_fileobj.output_buffers[orphan[0]]
327 finally:
328 new_fileobj.lock.release()
329
330 # Don't keep the lock while writting. Will append \n when it shouldn't.
331 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000332 if orphan[1]:
333 new_fileobj.old_annotated_write('%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000334
335 new_fileobj.write = annotated_write
336 new_fileobj.full_flush = full_flush
337 return new_fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000338
339
maruel@chromium.org17d01792010-09-01 18:07:10 +0000340def CheckCallAndFilter(args, stdout=None, filter_fn=None,
341 print_stdout=None, call_filter_on_first_line=False,
342 **kwargs):
343 """Runs a command and calls back a filter function if needed.
344
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000345 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000346 print_stdout: If True, the command's stdout is forwarded to stdout.
347 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000348 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000349 character trimmed.
350 stdout: Can be any bufferable output.
351
352 stderr is always redirected to stdout.
353 """
354 assert print_stdout or filter_fn
355 stdout = stdout or sys.stdout
356 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000357 kid = subprocess2.Popen(
358 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
359 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000360
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000361 # Do a flush of stdout before we begin reading from the subprocess2's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000362 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000363
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000364 # Also, we need to forward stdout to prevent weird re-ordering of output.
365 # This has to be done on a per byte basis to make sure it is not buffered:
366 # normally buffering is done for each line, but if svn requests input, no
367 # end-of-line character is output after the prompt and it would not show up.
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000368 try:
369 in_byte = kid.stdout.read(1)
370 if in_byte:
371 if call_filter_on_first_line:
372 filter_fn(None)
373 in_line = ''
374 while in_byte:
375 if in_byte != '\r':
376 if print_stdout:
377 stdout.write(in_byte)
378 if in_byte != '\n':
379 in_line += in_byte
380 else:
381 filter_fn(in_line)
382 in_line = ''
szager@google.com85d3e3a2011-10-07 17:12:00 +0000383 else:
384 filter_fn(in_line)
385 in_line = ''
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000386 in_byte = kid.stdout.read(1)
387 # Flush the rest of buffered output. This is only an issue with
388 # stdout/stderr not ending with a \n.
389 if len(in_line):
390 filter_fn(in_line)
391 rv = kid.wait()
392 except KeyboardInterrupt:
393 print >> sys.stderr, 'Failed while running "%s"' % ' '.join(args)
394 raise
395
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000396 if rv:
maruel@chromium.orga82a8ee2011-09-08 18:41:37 +0000397 raise subprocess2.CalledProcessError(
398 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000399 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000400
401
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000402def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000403 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000404 real_from_dir = os.path.realpath(from_dir)
405 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000406 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000407 split_path = os.path.split(path)
408 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000409 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000410 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000411
412 # If we did not find the file in the current directory, make sure we are in a
413 # sub directory that is controlled by this configuration.
414 if path != real_from_dir:
415 entries_filename = os.path.join(path, filename + '_entries')
416 if not os.path.exists(entries_filename):
417 # If .gclient_entries does not exist, a previous call to gclient sync
418 # might have failed. In that case, we cannot verify that the .gclient
419 # is the one we want to use. In order to not to cause too much trouble,
420 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000421 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000422 "file you want to use" % (filename, path))
423 return path
424 scope = {}
425 try:
426 exec(FileRead(entries_filename), scope)
427 except SyntaxError, e:
428 SyntaxErrorToError(filename, e)
429 all_directories = scope['entries'].keys()
430 path_to_check = real_from_dir[len(path)+1:]
431 while path_to_check:
432 if path_to_check in all_directories:
433 return path
434 path_to_check = os.path.dirname(path_to_check)
435 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000436
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000437 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000438 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000439
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000440
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000441def PathDifference(root, subpath):
442 """Returns the difference subpath minus root."""
443 root = os.path.realpath(root)
444 subpath = os.path.realpath(subpath)
445 if not subpath.startswith(root):
446 return None
447 # If the root does not have a trailing \ or /, we add it so the returned
448 # path starts immediately after the seperator regardless of whether it is
449 # provided.
450 root = os.path.join(root, '')
451 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000452
453
454def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000455 """Search upwards from the a directory (default: current) to find a file.
456
457 Returns nearest upper-level directory with the passed in file.
458 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000459 if not path:
460 path = os.getcwd()
461 path = os.path.realpath(path)
462 while True:
463 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000464 if os.path.exists(file_path):
465 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000466 (new_path, _) = os.path.split(path)
467 if new_path == path:
468 return None
469 path = new_path
470
471
472def GetGClientRootAndEntries(path=None):
473 """Returns the gclient root and the dict of entries."""
474 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000475 root = FindFileUpwards(config_file, path)
476 if not root:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000477 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000478 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000479 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000480 env = {}
481 execfile(config_path, env)
482 config_dir = os.path.dirname(config_path)
483 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000484
485
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000486def lockedmethod(method):
487 """Method decorator that holds self.lock for the duration of the call."""
488 def inner(self, *args, **kwargs):
489 try:
490 try:
491 self.lock.acquire()
492 except KeyboardInterrupt:
493 print >> sys.stderr, 'Was deadlocked'
494 raise
495 return method(self, *args, **kwargs)
496 finally:
497 self.lock.release()
498 return inner
499
500
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000501class WorkItem(object):
502 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000503 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
504 # As a workaround, use a single lock. Yep you read it right. Single lock for
505 # all the 100 objects.
506 lock = threading.Lock()
507
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000508 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000509 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000510 self._name = name
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000511
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000512 def run(self, work_queue):
513 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000514 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000515 pass
516
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000517 @property
518 def name(self):
519 return self._name
520
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000521
522class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000523 """Runs a set of WorkItem that have interdependencies and were WorkItem are
524 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000525
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000526 In gclient's case, Dependencies sometime needs to be run out of order due to
527 From() keyword. This class manages that all the required dependencies are run
528 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000529
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000530 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000531 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000532 def __init__(self, jobs, progress):
533 """jobs specifies the number of concurrent tasks to allow. progress is a
534 Progress instance."""
535 # Set when a thread is done or a new item is enqueued.
536 self.ready_cond = threading.Condition()
537 # Maximum number of concurrent tasks.
538 self.jobs = jobs
539 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000540 self.queued = []
541 # List of strings representing each Dependency.name that was run.
542 self.ran = []
543 # List of items currently running.
544 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000545 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000546 self.exceptions = Queue.Queue()
547 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000548 self.progress = progress
549 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000550 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000551
552 def enqueue(self, d):
553 """Enqueue one Dependency to be executed later once its requirements are
554 satisfied.
555 """
556 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000557 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000558 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000559 self.queued.append(d)
560 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000561 logging.debug('enqueued(%s)' % d.name)
562 if self.progress:
563 self.progress._total = total + 1
564 self.progress.update(0)
565 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000566 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000567 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000568
569 def flush(self, *args, **kwargs):
570 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000571 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000572 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000573 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000574 while True:
575 # Check for task to run first, then wait.
576 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000577 if not self.exceptions.empty():
578 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000579 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000580 self._flush_terminated_threads()
581 if (not self.queued and not self.running or
582 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000583 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000584 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000585
586 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000587 for i in xrange(len(self.queued)):
588 # Verify its requirements.
589 for r in self.queued[i].requirements:
590 if not r in self.ran:
591 # Requirement not met.
592 break
593 else:
594 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000595 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000596 break
597 else:
598 # Couldn't find an item that could run. Break out the outher loop.
599 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000600
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000601 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000602 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000603 break
604 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000605 try:
606 self.ready_cond.wait(10)
607 except KeyboardInterrupt:
608 # Help debugging by printing some information:
609 print >> sys.stderr, (
610 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
611 'Running: %d') % (
612 self.jobs,
613 len(self.queued),
614 ', '.join(self.ran),
615 len(self.running)))
616 for i in self.queued:
617 print >> sys.stderr, '%s: %s' % (i.name, ', '.join(i.requirements))
618 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000619 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000620 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000621 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000622
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000623 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000624 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000625 # To get back the stack location correctly, the raise a, b, c form must be
626 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000627 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000628 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000629 if self.progress:
630 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000631
maruel@chromium.org3742c842010-09-09 19:27:14 +0000632 def _flush_terminated_threads(self):
633 """Flush threads that have terminated."""
634 running = self.running
635 self.running = []
636 for t in running:
637 if t.isAlive():
638 self.running.append(t)
639 else:
640 t.join()
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000641 sys.stdout.full_flush() # pylint: disable=E1101
maruel@chromium.org3742c842010-09-09 19:27:14 +0000642 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000643 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000644 if t.item.name in self.ran:
645 raise Error(
646 'gclient is confused, "%s" is already in "%s"' % (
647 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000648 if not t.item.name in self.ran:
649 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000650
651 def _run_one_task(self, task_item, args, kwargs):
652 if self.jobs > 1:
653 # Start the thread.
654 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000655 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000656 self.running.append(new_thread)
657 new_thread.start()
658 else:
659 # Run the 'thread' inside the main thread. Don't try to catch any
660 # exception.
661 task_item.run(*args, **kwargs)
662 self.ran.append(task_item.name)
663 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000664 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000665
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000666 class _Worker(threading.Thread):
667 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000668 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000669 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000670 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000671 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000672 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000673 self.args = args
674 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000675
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000676 def run(self):
677 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000678 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000679 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000680 try:
681 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000682 except Exception:
683 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000684 logging.info('Caught exception in thread %s' % self.item.name)
685 logging.info(str(sys.exc_info()))
686 work_queue.exceptions.put(sys.exc_info())
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000687 logging.info('_Worker.run(%s) done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000688
maruel@chromium.org3742c842010-09-09 19:27:14 +0000689 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000690 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000691 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000692 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000693 work_queue.ready_cond.release()