blob: 733d43fca236727fb5afb5f584f9ced53c2a5543 [file] [log] [blame]
maruel@chromium.org06617272010-11-04 13:50:50 +00001# Copyright (c) 2010 The Chromium Authors. All rights reserved.
2# 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 subprocess
14import sys
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000015import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000016import time
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000017import xml.dom.minidom
maruel@chromium.org167b9e62009-09-17 17:41:02 +000018import xml.parsers.expat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000019
maruel@chromium.org93ef4102011-04-01 20:37:02 +000020import subprocess2
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000021
maruel@chromium.org93ef4102011-04-01 20:37:02 +000022# Keep an alias for now.
23Popen = subprocess2.Popen
maruel@chromium.org06617272010-11-04 13:50:50 +000024
25
maruel@chromium.org66c83e62010-09-07 14:18:45 +000026class Error(Exception):
27 """gclient exception class."""
28 pass
29
30
31class CheckCallError(OSError, Error):
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000032 """CheckCall() returned non-0."""
maruel@chromium.org66c83e62010-09-07 14:18:45 +000033 def __init__(self, command, cwd, returncode, stdout, stderr=None):
34 OSError.__init__(self, command, cwd, returncode, stdout, stderr)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +000035 Error.__init__(self, command)
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000036 self.command = command
37 self.cwd = cwd
maruel@chromium.org66c83e62010-09-07 14:18:45 +000038 self.returncode = returncode
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000039 self.stdout = stdout
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000040 self.stderr = stderr
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000041
maruel@chromium.org7b194c12010-09-07 20:57:09 +000042 def __str__(self):
43 out = ' '.join(self.command)
44 if self.cwd:
45 out += ' in ' + self.cwd
46 if self.returncode is not None:
47 out += ' returned %d' % self.returncode
48 if self.stdout is not None:
49 out += '\nstdout: %s\n' % self.stdout
50 if self.stderr is not None:
51 out += '\nstderr: %s\n' % self.stderr
52 return out
53
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000054
maruel@chromium.orgac610232010-10-13 14:01:31 +000055def CheckCall(command, print_error=True, **kwargs):
maruel@chromium.org3a292682010-08-23 18:54:55 +000056 """Similar subprocess.check_call() but redirects stdout and
57 returns (stdout, stderr).
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000058
59 Works on python 2.4
60 """
maruel@chromium.org18111352009-12-20 17:21:28 +000061 try:
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000062 stderr = None
63 if not print_error:
64 stderr = subprocess.PIPE
maruel@chromium.orgac610232010-10-13 14:01:31 +000065 process = Popen(command, stdout=subprocess.PIPE, stderr=stderr, **kwargs)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000066 std_out, std_err = process.communicate()
maruel@chromium.org18111352009-12-20 17:21:28 +000067 except OSError, e:
maruel@chromium.orgac610232010-10-13 14:01:31 +000068 raise CheckCallError(command, kwargs.get('cwd', None), e.errno, None)
maruel@chromium.org18111352009-12-20 17:21:28 +000069 if process.returncode:
maruel@chromium.orgac610232010-10-13 14:01:31 +000070 raise CheckCallError(command, kwargs.get('cwd', None), process.returncode,
71 std_out, std_err)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000072 return std_out, std_err
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000073
74
msb@chromium.orgac915bb2009-11-13 17:03:01 +000075def SplitUrlRevision(url):
76 """Splits url and returns a two-tuple: url, rev"""
77 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000078 # Make sure ssh://user-name@example.com/~/test.git@stable works
79 regex = r'(ssh://(?:[-\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000080 components = re.search(regex, url).groups()
81 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000082 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000083 if len(components) == 1:
84 components += [None]
85 return tuple(components)
86
87
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000088def ParseXML(output):
89 try:
90 return xml.dom.minidom.parseString(output)
91 except xml.parsers.expat.ExpatError:
92 return None
93
94
95def GetNamedNodeText(node, node_name):
96 child_nodes = node.getElementsByTagName(node_name)
97 if not child_nodes:
98 return None
99 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
100 return child_nodes[0].firstChild.nodeValue
101
102
103def GetNodeNamedAttributeText(node, node_name, attribute_name):
104 child_nodes = node.getElementsByTagName(node_name)
105 if not child_nodes:
106 return None
107 assert len(child_nodes) == 1
108 return child_nodes[0].getAttribute(attribute_name)
109
110
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000111def SyntaxErrorToError(filename, e):
112 """Raises a gclient_utils.Error exception with the human readable message"""
113 try:
114 # Try to construct a human readable error message
115 if filename:
116 error_message = 'There is a syntax error in %s\n' % filename
117 else:
118 error_message = 'There is a syntax error\n'
119 error_message += 'Line #%s, character %s: "%s"' % (
120 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
121 except:
122 # Something went wrong, re-raise the original exception
123 raise e
124 else:
125 raise Error(error_message)
126
127
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000128class PrintableObject(object):
129 def __str__(self):
130 output = ''
131 for i in dir(self):
132 if i.startswith('__'):
133 continue
134 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
135 return output
136
137
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000138def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000139 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000140 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000141 try:
142 content = f.read()
143 finally:
144 f.close()
145 return content
146
147
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000148def FileWrite(filename, content, mode='w'):
149 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000150 try:
151 f.write(content)
152 finally:
153 f.close()
154
155
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000156def rmtree(path):
157 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000158
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000159 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000160
161 shutil.rmtree() doesn't work on Windows if any of the files or directories
162 are read-only, which svn repositories and some .svn files are. We need to
163 be able to force the files to be writable (i.e., deletable) as we traverse
164 the tree.
165
166 Even with all this, Windows still sometimes fails to delete a file, citing
167 a permission error (maybe something to do with antivirus scans or disk
168 indexing). The best suggestion any of the user forums had was to wait a
169 bit and try again, so we do that too. It's hand-waving, but sometimes it
170 works. :/
171
172 On POSIX systems, things are a little bit simpler. The modes of the files
173 to be deleted doesn't matter, only the modes of the directories containing
174 them are significant. As the directory tree is traversed, each directory
175 has its mode set appropriately before descending into it. This should
176 result in the entire tree being removed, with the possible exception of
177 *path itself, because nothing attempts to change the mode of its parent.
178 Doing so would be hazardous, as it's not a directory slated for removal.
179 In the ordinary case, this is not a problem: for our purposes, the user
180 will never lack write permission on *path's parent.
181 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000182 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000183 return
184
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000185 if os.path.islink(path) or not os.path.isdir(path):
186 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000187
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000188 if sys.platform == 'win32':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000189 # Some people don't have the APIs installed. In that case we'll do without.
maruel@chromium.org1edee692011-03-12 19:39:13 +0000190 win32api = None
191 win32con = None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000192 try:
maruel@chromium.org1edee692011-03-12 19:39:13 +0000193 # Unable to import 'XX'
194 # pylint: disable=F0401
195 import win32api, win32con
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000196 except ImportError:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000197 pass
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000198 else:
199 # On POSIX systems, we need the x-bit set on the directory to access it,
200 # the r-bit to see its contents, and the w-bit to remove files from it.
201 # The actual modes of the files within the directory is irrelevant.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000202 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000203
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000204 def remove(func, subpath):
205 if sys.platform == 'win32':
206 os.chmod(subpath, stat.S_IWRITE)
207 if win32api and win32con:
208 win32api.SetFileAttributes(subpath, win32con.FILE_ATTRIBUTE_NORMAL)
209 try:
210 func(subpath)
211 except OSError, e:
212 if e.errno != errno.EACCES or sys.platform != 'win32':
213 raise
214 # Failed to delete, try again after a 100ms sleep.
215 time.sleep(0.1)
216 func(subpath)
217
218 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000219 # If fullpath is a symbolic link that points to a directory, isdir will
220 # be True, but we don't want to descend into that as a directory, we just
221 # want to remove the link. Check islink and treat links as ordinary files
222 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000223 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000224 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000225 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000226 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000227 # Recurse.
228 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000229
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000230 remove(os.rmdir, path)
231
232# TODO(maruel): Rename the references.
233RemoveDirectory = rmtree
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000234
235
maruel@chromium.org17d01792010-09-01 18:07:10 +0000236def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
237 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000238
maruel@chromium.org17d01792010-09-01 18:07:10 +0000239 If |always| is True, a message indicating what is being done
240 is printed to stdout all the time even if not output is generated. Otherwise
241 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000242 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000243 stdout = kwargs.get('stdout', None) or sys.stdout
244 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000245 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000246 % (' '.join(args), kwargs.get('cwd', '.')))
247 else:
248 filter_fn = kwargs.get('filter_fn', None)
249 def filter_msg(line):
250 if line is None:
251 stdout.write('\n________ running \'%s\' in \'%s\'\n'
252 % (' '.join(args), kwargs.get('cwd', '.')))
253 elif filter_fn:
254 filter_fn(line)
255 kwargs['filter_fn'] = filter_msg
256 kwargs['call_filter_on_first_line'] = True
257 # Obviously.
258 kwargs['print_stdout'] = True
259 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000260
maruel@chromium.org17d01792010-09-01 18:07:10 +0000261
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000262def SoftClone(obj):
263 """Clones an object. copy.copy() doesn't work on 'file' objects."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000264 if obj.__class__.__name__ == 'SoftCloned':
265 return obj
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000266 class SoftCloned(object):
267 pass
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000268 new_obj = SoftCloned()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000269 for member in dir(obj):
270 if member.startswith('_'):
271 continue
272 setattr(new_obj, member, getattr(obj, member))
273 return new_obj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000274
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000275
276def MakeFileAutoFlush(fileobj, delay=10):
277 """Creates a file object clone to automatically flush after N seconds."""
278 if hasattr(fileobj, 'last_flushed_at'):
279 # Already patched. Just update delay.
280 fileobj.delay = delay
281 return fileobj
282
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000283 # Attribute 'XXX' defined outside __init__
284 # pylint: disable=W0201
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000285 new_fileobj = SoftClone(fileobj)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000286 if not hasattr(new_fileobj, 'lock'):
287 new_fileobj.lock = threading.Lock()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000288 new_fileobj.last_flushed_at = time.time()
289 new_fileobj.delay = delay
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000290 new_fileobj.old_auto_flush_write = new_fileobj.write
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000291 # Silence pylint.
292 new_fileobj.flush = fileobj.flush
293
294 def auto_flush_write(out):
295 new_fileobj.old_auto_flush_write(out)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000296 should_flush = False
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000297 new_fileobj.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000298 try:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000299 if (new_fileobj.delay and
300 (time.time() - new_fileobj.last_flushed_at) > new_fileobj.delay):
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000301 should_flush = True
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000302 new_fileobj.last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000303 finally:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000304 new_fileobj.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000305 if should_flush:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000306 new_fileobj.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000307
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000308 new_fileobj.write = auto_flush_write
309 return new_fileobj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000310
311
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000312def MakeFileAnnotated(fileobj):
313 """Creates a file object clone to automatically prepends every line in worker
314 threads with a NN> prefix."""
315 if hasattr(fileobj, 'output_buffers'):
316 # Already patched.
317 return fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000318
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000319 # Attribute 'XXX' defined outside __init__
320 # pylint: disable=W0201
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000321 new_fileobj = SoftClone(fileobj)
322 if not hasattr(new_fileobj, 'lock'):
323 new_fileobj.lock = threading.Lock()
324 new_fileobj.output_buffers = {}
325 new_fileobj.old_annotated_write = new_fileobj.write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000326
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000327 def annotated_write(out):
328 index = getattr(threading.currentThread(), 'index', None)
329 if index is None:
330 # Undexed threads aren't buffered.
331 new_fileobj.old_annotated_write(out)
332 return
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000333
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000334 new_fileobj.lock.acquire()
335 try:
336 # Use a dummy array to hold the string so the code can be lockless.
337 # Strings are immutable, requiring to keep a lock for the whole dictionary
338 # otherwise. Using an array is faster than using a dummy object.
339 if not index in new_fileobj.output_buffers:
340 obj = new_fileobj.output_buffers[index] = ['']
341 else:
342 obj = new_fileobj.output_buffers[index]
343 finally:
344 new_fileobj.lock.release()
345
346 # Continue lockless.
347 obj[0] += out
348 while '\n' in obj[0]:
349 line, remaining = obj[0].split('\n', 1)
350 new_fileobj.old_annotated_write('%d>%s\n' % (index, line))
351 obj[0] = remaining
352
353 def full_flush():
354 """Flush buffered output."""
355 orphans = []
356 new_fileobj.lock.acquire()
357 try:
358 # Detect threads no longer existing.
359 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000360 indexes = filter(None, indexes)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000361 for index in new_fileobj.output_buffers:
362 if not index in indexes:
363 orphans.append((index, new_fileobj.output_buffers[index][0]))
364 for orphan in orphans:
365 del new_fileobj.output_buffers[orphan[0]]
366 finally:
367 new_fileobj.lock.release()
368
369 # Don't keep the lock while writting. Will append \n when it shouldn't.
370 for orphan in orphans:
371 new_fileobj.old_annotated_write('%d>%s\n' % (orphan[0], orphan[1]))
372
373 new_fileobj.write = annotated_write
374 new_fileobj.full_flush = full_flush
375 return new_fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000376
377
maruel@chromium.org17d01792010-09-01 18:07:10 +0000378def CheckCallAndFilter(args, stdout=None, filter_fn=None,
379 print_stdout=None, call_filter_on_first_line=False,
380 **kwargs):
381 """Runs a command and calls back a filter function if needed.
382
383 Accepts all subprocess.Popen() parameters plus:
384 print_stdout: If True, the command's stdout is forwarded to stdout.
385 filter_fn: A function taking a single string argument called with each line
386 of the subprocess's output. Each line has the trailing newline
387 character trimmed.
388 stdout: Can be any bufferable output.
389
390 stderr is always redirected to stdout.
391 """
392 assert print_stdout or filter_fn
393 stdout = stdout or sys.stdout
394 filter_fn = filter_fn or (lambda x: None)
395 assert not 'stderr' in kwargs
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000396 kid = Popen(args, bufsize=0,
397 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
398 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000399
maruel@chromium.org17d01792010-09-01 18:07:10 +0000400 # Do a flush of stdout before we begin reading from the subprocess's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000401 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000402
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000403 # Also, we need to forward stdout to prevent weird re-ordering of output.
404 # This has to be done on a per byte basis to make sure it is not buffered:
405 # normally buffering is done for each line, but if svn requests input, no
406 # end-of-line character is output after the prompt and it would not show up.
407 in_byte = kid.stdout.read(1)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000408 if in_byte:
409 if call_filter_on_first_line:
410 filter_fn(None)
411 in_line = ''
412 while in_byte:
413 if in_byte != '\r':
414 if print_stdout:
415 stdout.write(in_byte)
416 if in_byte != '\n':
417 in_line += in_byte
418 else:
419 filter_fn(in_line)
420 in_line = ''
maruel@chromium.org17d01792010-09-01 18:07:10 +0000421 in_byte = kid.stdout.read(1)
422 # Flush the rest of buffered output. This is only an issue with
423 # stdout/stderr not ending with a \n.
424 if len(in_line):
425 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000426 rv = kid.wait()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000427 if rv:
maruel@chromium.org7b194c12010-09-07 20:57:09 +0000428 raise CheckCallError(args, kwargs.get('cwd', None), rv, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000429 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000430
431
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000432def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000433 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000434 real_from_dir = os.path.realpath(from_dir)
435 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000436 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000437 split_path = os.path.split(path)
438 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000439 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000440 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000441
442 # If we did not find the file in the current directory, make sure we are in a
443 # sub directory that is controlled by this configuration.
444 if path != real_from_dir:
445 entries_filename = os.path.join(path, filename + '_entries')
446 if not os.path.exists(entries_filename):
447 # If .gclient_entries does not exist, a previous call to gclient sync
448 # might have failed. In that case, we cannot verify that the .gclient
449 # is the one we want to use. In order to not to cause too much trouble,
450 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000451 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000452 "file you want to use" % (filename, path))
453 return path
454 scope = {}
455 try:
456 exec(FileRead(entries_filename), scope)
457 except SyntaxError, e:
458 SyntaxErrorToError(filename, e)
459 all_directories = scope['entries'].keys()
460 path_to_check = real_from_dir[len(path)+1:]
461 while path_to_check:
462 if path_to_check in all_directories:
463 return path
464 path_to_check = os.path.dirname(path_to_check)
465 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000466
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000467 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000468 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000469
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000470
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000471def PathDifference(root, subpath):
472 """Returns the difference subpath minus root."""
473 root = os.path.realpath(root)
474 subpath = os.path.realpath(subpath)
475 if not subpath.startswith(root):
476 return None
477 # If the root does not have a trailing \ or /, we add it so the returned
478 # path starts immediately after the seperator regardless of whether it is
479 # provided.
480 root = os.path.join(root, '')
481 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000482
483
484def FindFileUpwards(filename, path=None):
485 """Search upwards from the a directory (default: current) to find a file."""
486 if not path:
487 path = os.getcwd()
488 path = os.path.realpath(path)
489 while True:
490 file_path = os.path.join(path, filename)
491 if os.path.isfile(file_path):
492 return file_path
493 (new_path, _) = os.path.split(path)
494 if new_path == path:
495 return None
496 path = new_path
497
498
499def GetGClientRootAndEntries(path=None):
500 """Returns the gclient root and the dict of entries."""
501 config_file = '.gclient_entries'
502 config_path = FindFileUpwards(config_file, path)
503
504 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000505 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000506 return None
507
508 env = {}
509 execfile(config_path, env)
510 config_dir = os.path.dirname(config_path)
511 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000512
513
514class WorkItem(object):
515 """One work item."""
516 # A list of string, each being a WorkItem name.
517 requirements = []
518 # A unique string representing this work item.
519 name = None
520
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000521 def run(self, work_queue):
522 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000523 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000524 pass
525
526
527class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000528 """Runs a set of WorkItem that have interdependencies and were WorkItem are
529 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000530
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000531 In gclient's case, Dependencies sometime needs to be run out of order due to
532 From() keyword. This class manages that all the required dependencies are run
533 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000534
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000535 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000536 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000537 def __init__(self, jobs, progress):
538 """jobs specifies the number of concurrent tasks to allow. progress is a
539 Progress instance."""
540 # Set when a thread is done or a new item is enqueued.
541 self.ready_cond = threading.Condition()
542 # Maximum number of concurrent tasks.
543 self.jobs = jobs
544 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000545 self.queued = []
546 # List of strings representing each Dependency.name that was run.
547 self.ran = []
548 # List of items currently running.
549 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000550 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000551 self.exceptions = Queue.Queue()
552 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000553 self.progress = progress
554 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000555 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000556
557 def enqueue(self, d):
558 """Enqueue one Dependency to be executed later once its requirements are
559 satisfied.
560 """
561 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000562 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000563 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000564 self.queued.append(d)
565 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000566 logging.debug('enqueued(%s)' % d.name)
567 if self.progress:
568 self.progress._total = total + 1
569 self.progress.update(0)
570 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000571 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000572 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000573
574 def flush(self, *args, **kwargs):
575 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000576 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000577 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000578 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000579 while True:
580 # Check for task to run first, then wait.
581 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000582 if not self.exceptions.empty():
583 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000584 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000585 self._flush_terminated_threads()
586 if (not self.queued and not self.running or
587 self.jobs == len(self.running)):
588 # No more worker threads or can't queue anything.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000589 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000590
591 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000592 for i in xrange(len(self.queued)):
593 # Verify its requirements.
594 for r in self.queued[i].requirements:
595 if not r in self.ran:
596 # Requirement not met.
597 break
598 else:
599 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000600 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000601 break
602 else:
603 # Couldn't find an item that could run. Break out the outher loop.
604 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000605
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000606 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000607 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000608 break
609 # We need to poll here otherwise Ctrl-C isn't processed.
610 self.ready_cond.wait(10)
611 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000612 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000613 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000614
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000615 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000616 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000617 # To get back the stack location correctly, the raise a, b, c form must be
618 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000619 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000620 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000621 if self.progress:
622 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000623
maruel@chromium.org3742c842010-09-09 19:27:14 +0000624 def _flush_terminated_threads(self):
625 """Flush threads that have terminated."""
626 running = self.running
627 self.running = []
628 for t in running:
629 if t.isAlive():
630 self.running.append(t)
631 else:
632 t.join()
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000633 sys.stdout.full_flush() # pylint: disable=E1101
maruel@chromium.org3742c842010-09-09 19:27:14 +0000634 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000635 self.progress.update(1, t.item.name)
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000636 assert not t.item.name in self.ran
637 if not t.item.name in self.ran:
638 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000639
640 def _run_one_task(self, task_item, args, kwargs):
641 if self.jobs > 1:
642 # Start the thread.
643 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000644 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000645 self.running.append(new_thread)
646 new_thread.start()
647 else:
648 # Run the 'thread' inside the main thread. Don't try to catch any
649 # exception.
650 task_item.run(*args, **kwargs)
651 self.ran.append(task_item.name)
652 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000653 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000654
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000655 class _Worker(threading.Thread):
656 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000657 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000658 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org3742c842010-09-09 19:27:14 +0000659 logging.info(item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000660 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000661 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000662 self.args = args
663 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000664
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000665 def run(self):
666 """Runs in its own thread."""
667 logging.debug('running(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000668 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000669 try:
670 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000671 except Exception:
672 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000673 logging.info('Caught exception in thread %s' % self.item.name)
674 logging.info(str(sys.exc_info()))
675 work_queue.exceptions.put(sys.exc_info())
676 logging.info('Task %s done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000677
maruel@chromium.org3742c842010-09-09 19:27:14 +0000678 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000679 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000680 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000681 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000682 work_queue.ready_cond.release()