blob: 5ab1ed509a25b112f01c962471c683f6614c801f [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.org5f3eee32009-09-17 00:34:30 +000020
maruel@chromium.org06617272010-11-04 13:50:50 +000021def hack_subprocess():
22 """subprocess functions may throw exceptions when used in multiple threads.
23
24 See http://bugs.python.org/issue1731717 for more information.
25 """
26 subprocess._cleanup = lambda: None
27
28
maruel@chromium.org66c83e62010-09-07 14:18:45 +000029class Error(Exception):
30 """gclient exception class."""
31 pass
32
33
34class CheckCallError(OSError, Error):
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000035 """CheckCall() returned non-0."""
maruel@chromium.org66c83e62010-09-07 14:18:45 +000036 def __init__(self, command, cwd, returncode, stdout, stderr=None):
37 OSError.__init__(self, command, cwd, returncode, stdout, stderr)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +000038 Error.__init__(self, command)
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000039 self.command = command
40 self.cwd = cwd
maruel@chromium.org66c83e62010-09-07 14:18:45 +000041 self.returncode = returncode
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000042 self.stdout = stdout
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000043 self.stderr = stderr
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000044
maruel@chromium.org7b194c12010-09-07 20:57:09 +000045 def __str__(self):
46 out = ' '.join(self.command)
47 if self.cwd:
48 out += ' in ' + self.cwd
49 if self.returncode is not None:
50 out += ' returned %d' % self.returncode
51 if self.stdout is not None:
52 out += '\nstdout: %s\n' % self.stdout
53 if self.stderr is not None:
54 out += '\nstderr: %s\n' % self.stderr
55 return out
56
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000057
maruel@chromium.orga1693be2010-09-03 19:09:35 +000058def Popen(args, **kwargs):
maruel@chromium.org3a292682010-08-23 18:54:55 +000059 """Calls subprocess.Popen() with hacks to work around certain behaviors.
60
61 Ensure English outpout for svn and make it work reliably on Windows.
62 """
maruel@chromium.orga1693be2010-09-03 19:09:35 +000063 logging.debug(u'%s, cwd=%s' % (u' '.join(args), kwargs.get('cwd', '')))
maruel@chromium.org3a292682010-08-23 18:54:55 +000064 if not 'env' in kwargs:
maruel@chromium.org3a292682010-08-23 18:54:55 +000065 # It's easier to parse the stdout if it is always in English.
66 kwargs['env'] = os.environ.copy()
67 kwargs['env']['LANGUAGE'] = 'en'
68 if not 'shell' in kwargs:
maruel@chromium.org3a292682010-08-23 18:54:55 +000069 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
70 # executable, but shell=True makes subprocess on Linux fail when it's called
71 # with a list because it only tries to execute the first item in the list.
72 kwargs['shell'] = (sys.platform=='win32')
maruel@chromium.org8aba5f72010-09-16 19:48:59 +000073 try:
74 return subprocess.Popen(args, **kwargs)
75 except OSError, e:
76 if e.errno == errno.EAGAIN and sys.platform == 'cygwin':
77 raise Error(
78 'Visit '
79 'http://code.google.com/p/chromium/wiki/CygwinDllRemappingFailure to '
80 'learn how to fix this error; you need to rebase your cygwin dlls')
81 raise
maruel@chromium.org3a292682010-08-23 18:54:55 +000082
83
maruel@chromium.orgac610232010-10-13 14:01:31 +000084def CheckCall(command, print_error=True, **kwargs):
maruel@chromium.org3a292682010-08-23 18:54:55 +000085 """Similar subprocess.check_call() but redirects stdout and
86 returns (stdout, stderr).
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000087
88 Works on python 2.4
89 """
maruel@chromium.org18111352009-12-20 17:21:28 +000090 try:
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000091 stderr = None
92 if not print_error:
93 stderr = subprocess.PIPE
maruel@chromium.orgac610232010-10-13 14:01:31 +000094 process = Popen(command, stdout=subprocess.PIPE, stderr=stderr, **kwargs)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000095 std_out, std_err = process.communicate()
maruel@chromium.org18111352009-12-20 17:21:28 +000096 except OSError, e:
maruel@chromium.orgac610232010-10-13 14:01:31 +000097 raise CheckCallError(command, kwargs.get('cwd', None), e.errno, None)
maruel@chromium.org18111352009-12-20 17:21:28 +000098 if process.returncode:
maruel@chromium.orgac610232010-10-13 14:01:31 +000099 raise CheckCallError(command, kwargs.get('cwd', None), process.returncode,
100 std_out, std_err)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000101 return std_out, std_err
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +0000102
103
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000104def SplitUrlRevision(url):
105 """Splits url and returns a two-tuple: url, rev"""
106 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +0000107 # Make sure ssh://user-name@example.com/~/test.git@stable works
108 regex = r'(ssh://(?:[-\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000109 components = re.search(regex, url).groups()
110 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000111 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000112 if len(components) == 1:
113 components += [None]
114 return tuple(components)
115
116
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000117def ParseXML(output):
118 try:
119 return xml.dom.minidom.parseString(output)
120 except xml.parsers.expat.ExpatError:
121 return None
122
123
124def GetNamedNodeText(node, node_name):
125 child_nodes = node.getElementsByTagName(node_name)
126 if not child_nodes:
127 return None
128 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
129 return child_nodes[0].firstChild.nodeValue
130
131
132def GetNodeNamedAttributeText(node, node_name, attribute_name):
133 child_nodes = node.getElementsByTagName(node_name)
134 if not child_nodes:
135 return None
136 assert len(child_nodes) == 1
137 return child_nodes[0].getAttribute(attribute_name)
138
139
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000140def SyntaxErrorToError(filename, e):
141 """Raises a gclient_utils.Error exception with the human readable message"""
142 try:
143 # Try to construct a human readable error message
144 if filename:
145 error_message = 'There is a syntax error in %s\n' % filename
146 else:
147 error_message = 'There is a syntax error\n'
148 error_message += 'Line #%s, character %s: "%s"' % (
149 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
150 except:
151 # Something went wrong, re-raise the original exception
152 raise e
153 else:
154 raise Error(error_message)
155
156
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000157class PrintableObject(object):
158 def __str__(self):
159 output = ''
160 for i in dir(self):
161 if i.startswith('__'):
162 continue
163 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
164 return output
165
166
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000167def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000168 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000169 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000170 try:
171 content = f.read()
172 finally:
173 f.close()
174 return content
175
176
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000177def FileWrite(filename, content, mode='w'):
178 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000179 try:
180 f.write(content)
181 finally:
182 f.close()
183
184
185def RemoveDirectory(*path):
186 """Recursively removes a directory, even if it's marked read-only.
187
188 Remove the directory located at *path, if it exists.
189
190 shutil.rmtree() doesn't work on Windows if any of the files or directories
191 are read-only, which svn repositories and some .svn files are. We need to
192 be able to force the files to be writable (i.e., deletable) as we traverse
193 the tree.
194
195 Even with all this, Windows still sometimes fails to delete a file, citing
196 a permission error (maybe something to do with antivirus scans or disk
197 indexing). The best suggestion any of the user forums had was to wait a
198 bit and try again, so we do that too. It's hand-waving, but sometimes it
199 works. :/
200
201 On POSIX systems, things are a little bit simpler. The modes of the files
202 to be deleted doesn't matter, only the modes of the directories containing
203 them are significant. As the directory tree is traversed, each directory
204 has its mode set appropriately before descending into it. This should
205 result in the entire tree being removed, with the possible exception of
206 *path itself, because nothing attempts to change the mode of its parent.
207 Doing so would be hazardous, as it's not a directory slated for removal.
208 In the ordinary case, this is not a problem: for our purposes, the user
209 will never lack write permission on *path's parent.
210 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000211 logging.debug(path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000212 file_path = os.path.join(*path)
213 if not os.path.exists(file_path):
214 return
215
216 if os.path.islink(file_path) or not os.path.isdir(file_path):
maruel@chromium.org116704f2010-06-11 17:34:38 +0000217 raise Error('RemoveDirectory asked to remove non-directory %s' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000218
219 has_win32api = False
220 if sys.platform == 'win32':
221 has_win32api = True
222 # Some people don't have the APIs installed. In that case we'll do without.
223 try:
224 win32api = __import__('win32api')
225 win32con = __import__('win32con')
226 except ImportError:
227 has_win32api = False
228 else:
229 # On POSIX systems, we need the x-bit set on the directory to access it,
230 # the r-bit to see its contents, and the w-bit to remove files from it.
231 # The actual modes of the files within the directory is irrelevant.
232 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
233 for fn in os.listdir(file_path):
234 fullpath = os.path.join(file_path, fn)
235
236 # If fullpath is a symbolic link that points to a directory, isdir will
237 # be True, but we don't want to descend into that as a directory, we just
238 # want to remove the link. Check islink and treat links as ordinary files
239 # would be treated regardless of what they reference.
240 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
241 if sys.platform == 'win32':
242 os.chmod(fullpath, stat.S_IWRITE)
243 if has_win32api:
244 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
245 try:
246 os.remove(fullpath)
247 except OSError, e:
248 if e.errno != errno.EACCES or sys.platform != 'win32':
249 raise
250 print 'Failed to delete %s: trying again' % fullpath
251 time.sleep(0.1)
252 os.remove(fullpath)
253 else:
254 RemoveDirectory(fullpath)
255
256 if sys.platform == 'win32':
257 os.chmod(file_path, stat.S_IWRITE)
258 if has_win32api:
259 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
260 try:
261 os.rmdir(file_path)
262 except OSError, e:
263 if e.errno != errno.EACCES or sys.platform != 'win32':
264 raise
265 print 'Failed to remove %s: trying again' % file_path
266 time.sleep(0.1)
267 os.rmdir(file_path)
268
269
maruel@chromium.org17d01792010-09-01 18:07:10 +0000270def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
271 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000272
maruel@chromium.org17d01792010-09-01 18:07:10 +0000273 If |always| is True, a message indicating what is being done
274 is printed to stdout all the time even if not output is generated. Otherwise
275 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000276 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000277 stdout = kwargs.get('stdout', None) or sys.stdout
278 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000279 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000280 % (' '.join(args), kwargs.get('cwd', '.')))
281 else:
282 filter_fn = kwargs.get('filter_fn', None)
283 def filter_msg(line):
284 if line is None:
285 stdout.write('\n________ running \'%s\' in \'%s\'\n'
286 % (' '.join(args), kwargs.get('cwd', '.')))
287 elif filter_fn:
288 filter_fn(line)
289 kwargs['filter_fn'] = filter_msg
290 kwargs['call_filter_on_first_line'] = True
291 # Obviously.
292 kwargs['print_stdout'] = True
293 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000294
maruel@chromium.org17d01792010-09-01 18:07:10 +0000295
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000296def SoftClone(obj):
297 """Clones an object. copy.copy() doesn't work on 'file' objects."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000298 if obj.__class__.__name__ == 'SoftCloned':
299 return obj
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000300 class SoftCloned(object):
301 pass
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000302 new_obj = SoftCloned()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000303 for member in dir(obj):
304 if member.startswith('_'):
305 continue
306 setattr(new_obj, member, getattr(obj, member))
307 return new_obj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000308
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000309
310def MakeFileAutoFlush(fileobj, delay=10):
311 """Creates a file object clone to automatically flush after N seconds."""
312 if hasattr(fileobj, 'last_flushed_at'):
313 # Already patched. Just update delay.
314 fileobj.delay = delay
315 return fileobj
316
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000317 # Attribute 'XXX' defined outside __init__
318 # pylint: disable=W0201
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000319 new_fileobj = SoftClone(fileobj)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000320 if not hasattr(new_fileobj, 'lock'):
321 new_fileobj.lock = threading.Lock()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000322 new_fileobj.last_flushed_at = time.time()
323 new_fileobj.delay = delay
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000324 new_fileobj.old_auto_flush_write = new_fileobj.write
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000325 # Silence pylint.
326 new_fileobj.flush = fileobj.flush
327
328 def auto_flush_write(out):
329 new_fileobj.old_auto_flush_write(out)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000330 should_flush = False
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000331 new_fileobj.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000332 try:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000333 if (new_fileobj.delay and
334 (time.time() - new_fileobj.last_flushed_at) > new_fileobj.delay):
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000335 should_flush = True
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000336 new_fileobj.last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000337 finally:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000338 new_fileobj.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000339 if should_flush:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000340 new_fileobj.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000341
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000342 new_fileobj.write = auto_flush_write
343 return new_fileobj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000344
345
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000346def MakeFileAnnotated(fileobj):
347 """Creates a file object clone to automatically prepends every line in worker
348 threads with a NN> prefix."""
349 if hasattr(fileobj, 'output_buffers'):
350 # Already patched.
351 return fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000352
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000353 # Attribute 'XXX' defined outside __init__
354 # pylint: disable=W0201
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000355 new_fileobj = SoftClone(fileobj)
356 if not hasattr(new_fileobj, 'lock'):
357 new_fileobj.lock = threading.Lock()
358 new_fileobj.output_buffers = {}
359 new_fileobj.old_annotated_write = new_fileobj.write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000360
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000361 def annotated_write(out):
362 index = getattr(threading.currentThread(), 'index', None)
363 if index is None:
364 # Undexed threads aren't buffered.
365 new_fileobj.old_annotated_write(out)
366 return
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000367
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000368 new_fileobj.lock.acquire()
369 try:
370 # Use a dummy array to hold the string so the code can be lockless.
371 # Strings are immutable, requiring to keep a lock for the whole dictionary
372 # otherwise. Using an array is faster than using a dummy object.
373 if not index in new_fileobj.output_buffers:
374 obj = new_fileobj.output_buffers[index] = ['']
375 else:
376 obj = new_fileobj.output_buffers[index]
377 finally:
378 new_fileobj.lock.release()
379
380 # Continue lockless.
381 obj[0] += out
382 while '\n' in obj[0]:
383 line, remaining = obj[0].split('\n', 1)
384 new_fileobj.old_annotated_write('%d>%s\n' % (index, line))
385 obj[0] = remaining
386
387 def full_flush():
388 """Flush buffered output."""
389 orphans = []
390 new_fileobj.lock.acquire()
391 try:
392 # Detect threads no longer existing.
393 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000394 indexes = filter(None, indexes)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000395 for index in new_fileobj.output_buffers:
396 if not index in indexes:
397 orphans.append((index, new_fileobj.output_buffers[index][0]))
398 for orphan in orphans:
399 del new_fileobj.output_buffers[orphan[0]]
400 finally:
401 new_fileobj.lock.release()
402
403 # Don't keep the lock while writting. Will append \n when it shouldn't.
404 for orphan in orphans:
405 new_fileobj.old_annotated_write('%d>%s\n' % (orphan[0], orphan[1]))
406
407 new_fileobj.write = annotated_write
408 new_fileobj.full_flush = full_flush
409 return new_fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000410
411
maruel@chromium.org17d01792010-09-01 18:07:10 +0000412def CheckCallAndFilter(args, stdout=None, filter_fn=None,
413 print_stdout=None, call_filter_on_first_line=False,
414 **kwargs):
415 """Runs a command and calls back a filter function if needed.
416
417 Accepts all subprocess.Popen() parameters plus:
418 print_stdout: If True, the command's stdout is forwarded to stdout.
419 filter_fn: A function taking a single string argument called with each line
420 of the subprocess's output. Each line has the trailing newline
421 character trimmed.
422 stdout: Can be any bufferable output.
423
424 stderr is always redirected to stdout.
425 """
426 assert print_stdout or filter_fn
427 stdout = stdout or sys.stdout
428 filter_fn = filter_fn or (lambda x: None)
429 assert not 'stderr' in kwargs
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000430 kid = Popen(args, bufsize=0,
431 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
432 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000433
maruel@chromium.org17d01792010-09-01 18:07:10 +0000434 # Do a flush of stdout before we begin reading from the subprocess's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000435 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000436
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000437 # Also, we need to forward stdout to prevent weird re-ordering of output.
438 # This has to be done on a per byte basis to make sure it is not buffered:
439 # normally buffering is done for each line, but if svn requests input, no
440 # end-of-line character is output after the prompt and it would not show up.
441 in_byte = kid.stdout.read(1)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000442 if in_byte:
443 if call_filter_on_first_line:
444 filter_fn(None)
445 in_line = ''
446 while in_byte:
447 if in_byte != '\r':
448 if print_stdout:
449 stdout.write(in_byte)
450 if in_byte != '\n':
451 in_line += in_byte
452 else:
453 filter_fn(in_line)
454 in_line = ''
maruel@chromium.org17d01792010-09-01 18:07:10 +0000455 in_byte = kid.stdout.read(1)
456 # Flush the rest of buffered output. This is only an issue with
457 # stdout/stderr not ending with a \n.
458 if len(in_line):
459 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000460 rv = kid.wait()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000461 if rv:
maruel@chromium.org7b194c12010-09-07 20:57:09 +0000462 raise CheckCallError(args, kwargs.get('cwd', None), rv, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000463 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000464
465
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000466def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000467 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000468 real_from_dir = os.path.realpath(from_dir)
469 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000470 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000471 split_path = os.path.split(path)
472 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000473 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000474 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000475
476 # If we did not find the file in the current directory, make sure we are in a
477 # sub directory that is controlled by this configuration.
478 if path != real_from_dir:
479 entries_filename = os.path.join(path, filename + '_entries')
480 if not os.path.exists(entries_filename):
481 # If .gclient_entries does not exist, a previous call to gclient sync
482 # might have failed. In that case, we cannot verify that the .gclient
483 # is the one we want to use. In order to not to cause too much trouble,
484 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000485 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000486 "file you want to use" % (filename, path))
487 return path
488 scope = {}
489 try:
490 exec(FileRead(entries_filename), scope)
491 except SyntaxError, e:
492 SyntaxErrorToError(filename, e)
493 all_directories = scope['entries'].keys()
494 path_to_check = real_from_dir[len(path)+1:]
495 while path_to_check:
496 if path_to_check in all_directories:
497 return path
498 path_to_check = os.path.dirname(path_to_check)
499 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000500
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000501 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000502 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000503
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000504
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000505def PathDifference(root, subpath):
506 """Returns the difference subpath minus root."""
507 root = os.path.realpath(root)
508 subpath = os.path.realpath(subpath)
509 if not subpath.startswith(root):
510 return None
511 # If the root does not have a trailing \ or /, we add it so the returned
512 # path starts immediately after the seperator regardless of whether it is
513 # provided.
514 root = os.path.join(root, '')
515 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000516
517
518def FindFileUpwards(filename, path=None):
519 """Search upwards from the a directory (default: current) to find a file."""
520 if not path:
521 path = os.getcwd()
522 path = os.path.realpath(path)
523 while True:
524 file_path = os.path.join(path, filename)
525 if os.path.isfile(file_path):
526 return file_path
527 (new_path, _) = os.path.split(path)
528 if new_path == path:
529 return None
530 path = new_path
531
532
533def GetGClientRootAndEntries(path=None):
534 """Returns the gclient root and the dict of entries."""
535 config_file = '.gclient_entries'
536 config_path = FindFileUpwards(config_file, path)
537
538 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000539 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000540 return None
541
542 env = {}
543 execfile(config_path, env)
544 config_dir = os.path.dirname(config_path)
545 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000546
547
548class WorkItem(object):
549 """One work item."""
550 # A list of string, each being a WorkItem name.
551 requirements = []
552 # A unique string representing this work item.
553 name = None
554
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000555 def run(self, work_queue):
556 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000557 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000558 pass
559
560
561class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000562 """Runs a set of WorkItem that have interdependencies and were WorkItem are
563 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000564
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000565 In gclient's case, Dependencies sometime needs to be run out of order due to
566 From() keyword. This class manages that all the required dependencies are run
567 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000568
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000569 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000570 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000571 def __init__(self, jobs, progress):
572 """jobs specifies the number of concurrent tasks to allow. progress is a
573 Progress instance."""
maruel@chromium.org06617272010-11-04 13:50:50 +0000574 hack_subprocess()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000575 # Set when a thread is done or a new item is enqueued.
576 self.ready_cond = threading.Condition()
577 # Maximum number of concurrent tasks.
578 self.jobs = jobs
579 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000580 self.queued = []
581 # List of strings representing each Dependency.name that was run.
582 self.ran = []
583 # List of items currently running.
584 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000585 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000586 self.exceptions = Queue.Queue()
587 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000588 self.progress = progress
589 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000590 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000591
592 def enqueue(self, d):
593 """Enqueue one Dependency to be executed later once its requirements are
594 satisfied.
595 """
596 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000597 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000598 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000599 self.queued.append(d)
600 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000601 logging.debug('enqueued(%s)' % d.name)
602 if self.progress:
603 self.progress._total = total + 1
604 self.progress.update(0)
605 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000606 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000607 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000608
609 def flush(self, *args, **kwargs):
610 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000611 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000612 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000613 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000614 while True:
615 # Check for task to run first, then wait.
616 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000617 if not self.exceptions.empty():
618 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000619 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000620 self._flush_terminated_threads()
621 if (not self.queued and not self.running or
622 self.jobs == len(self.running)):
623 # No more worker threads or can't queue anything.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000624 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000625
626 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000627 for i in xrange(len(self.queued)):
628 # Verify its requirements.
629 for r in self.queued[i].requirements:
630 if not r in self.ran:
631 # Requirement not met.
632 break
633 else:
634 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000635 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000636 break
637 else:
638 # Couldn't find an item that could run. Break out the outher loop.
639 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000640
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000641 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000642 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000643 break
644 # We need to poll here otherwise Ctrl-C isn't processed.
645 self.ready_cond.wait(10)
646 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000647 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000648 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000649
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000650 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000651 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000652 # To get back the stack location correctly, the raise a, b, c form must be
653 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000654 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000655 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000656 if self.progress:
657 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000658
maruel@chromium.org3742c842010-09-09 19:27:14 +0000659 def _flush_terminated_threads(self):
660 """Flush threads that have terminated."""
661 running = self.running
662 self.running = []
663 for t in running:
664 if t.isAlive():
665 self.running.append(t)
666 else:
667 t.join()
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000668 sys.stdout.full_flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000669 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000670 self.progress.update(1, t.item.name)
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000671 assert not t.item.name in self.ran
672 if not t.item.name in self.ran:
673 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000674
675 def _run_one_task(self, task_item, args, kwargs):
676 if self.jobs > 1:
677 # Start the thread.
678 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000679 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000680 self.running.append(new_thread)
681 new_thread.start()
682 else:
683 # Run the 'thread' inside the main thread. Don't try to catch any
684 # exception.
685 task_item.run(*args, **kwargs)
686 self.ran.append(task_item.name)
687 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000688 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000689
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000690 class _Worker(threading.Thread):
691 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000692 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000693 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org3742c842010-09-09 19:27:14 +0000694 logging.info(item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000695 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000696 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000697 self.args = args
698 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000699
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000700 def run(self):
701 """Runs in its own thread."""
702 logging.debug('running(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000703 work_queue = self.kwargs['work_queue']
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000704 # It's necessary to catch all exceptions.
705 # pylint: disable=W0703
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000706 try:
707 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000708 except Exception:
709 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000710 logging.info('Caught exception in thread %s' % self.item.name)
711 logging.info(str(sys.exc_info()))
712 work_queue.exceptions.put(sys.exc_info())
713 logging.info('Task %s done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000714
maruel@chromium.org3742c842010-09-09 19:27:14 +0000715 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000716 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000717 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000718 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000719 work_queue.ready_cond.release()