blob: 8e2d1c315e19807b8ade9c65ee3013b87dee6029 [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
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000185def rmtree(path):
186 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000187
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000188 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000189
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.orgf9040722011-03-09 14:47:51 +0000211 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000212 return
213
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000214 if os.path.islink(path) or not os.path.isdir(path):
215 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000216
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000217 if sys.platform == 'win32':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000218 # Some people don't have the APIs installed. In that case we'll do without.
219 try:
220 win32api = __import__('win32api')
221 win32con = __import__('win32con')
222 except ImportError:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000223 pass
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000224 else:
225 # On POSIX systems, we need the x-bit set on the directory to access it,
226 # the r-bit to see its contents, and the w-bit to remove files from it.
227 # The actual modes of the files within the directory is irrelevant.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000228 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000229
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000230 def remove(func, subpath):
231 if sys.platform == 'win32':
232 os.chmod(subpath, stat.S_IWRITE)
233 if win32api and win32con:
234 win32api.SetFileAttributes(subpath, win32con.FILE_ATTRIBUTE_NORMAL)
235 try:
236 func(subpath)
237 except OSError, e:
238 if e.errno != errno.EACCES or sys.platform != 'win32':
239 raise
240 # Failed to delete, try again after a 100ms sleep.
241 time.sleep(0.1)
242 func(subpath)
243
244 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000245 # If fullpath is a symbolic link that points to a directory, isdir will
246 # be True, but we don't want to descend into that as a directory, we just
247 # want to remove the link. Check islink and treat links as ordinary files
248 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000249 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000250 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000251 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000252 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000253 # Recurse.
254 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000255
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000256 remove(os.rmdir, path)
257
258# TODO(maruel): Rename the references.
259RemoveDirectory = rmtree
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000260
261
maruel@chromium.org17d01792010-09-01 18:07:10 +0000262def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
263 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000264
maruel@chromium.org17d01792010-09-01 18:07:10 +0000265 If |always| is True, a message indicating what is being done
266 is printed to stdout all the time even if not output is generated. Otherwise
267 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000268 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000269 stdout = kwargs.get('stdout', None) or sys.stdout
270 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000271 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000272 % (' '.join(args), kwargs.get('cwd', '.')))
273 else:
274 filter_fn = kwargs.get('filter_fn', None)
275 def filter_msg(line):
276 if line is None:
277 stdout.write('\n________ running \'%s\' in \'%s\'\n'
278 % (' '.join(args), kwargs.get('cwd', '.')))
279 elif filter_fn:
280 filter_fn(line)
281 kwargs['filter_fn'] = filter_msg
282 kwargs['call_filter_on_first_line'] = True
283 # Obviously.
284 kwargs['print_stdout'] = True
285 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000286
maruel@chromium.org17d01792010-09-01 18:07:10 +0000287
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000288def SoftClone(obj):
289 """Clones an object. copy.copy() doesn't work on 'file' objects."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000290 if obj.__class__.__name__ == 'SoftCloned':
291 return obj
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000292 class SoftCloned(object):
293 pass
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000294 new_obj = SoftCloned()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000295 for member in dir(obj):
296 if member.startswith('_'):
297 continue
298 setattr(new_obj, member, getattr(obj, member))
299 return new_obj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000300
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000301
302def MakeFileAutoFlush(fileobj, delay=10):
303 """Creates a file object clone to automatically flush after N seconds."""
304 if hasattr(fileobj, 'last_flushed_at'):
305 # Already patched. Just update delay.
306 fileobj.delay = delay
307 return fileobj
308
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000309 # Attribute 'XXX' defined outside __init__
310 # pylint: disable=W0201
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000311 new_fileobj = SoftClone(fileobj)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000312 if not hasattr(new_fileobj, 'lock'):
313 new_fileobj.lock = threading.Lock()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000314 new_fileobj.last_flushed_at = time.time()
315 new_fileobj.delay = delay
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000316 new_fileobj.old_auto_flush_write = new_fileobj.write
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000317 # Silence pylint.
318 new_fileobj.flush = fileobj.flush
319
320 def auto_flush_write(out):
321 new_fileobj.old_auto_flush_write(out)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000322 should_flush = False
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000323 new_fileobj.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000324 try:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000325 if (new_fileobj.delay and
326 (time.time() - new_fileobj.last_flushed_at) > new_fileobj.delay):
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000327 should_flush = True
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000328 new_fileobj.last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000329 finally:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000330 new_fileobj.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000331 if should_flush:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000332 new_fileobj.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000333
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000334 new_fileobj.write = auto_flush_write
335 return new_fileobj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000336
337
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000338def MakeFileAnnotated(fileobj):
339 """Creates a file object clone to automatically prepends every line in worker
340 threads with a NN> prefix."""
341 if hasattr(fileobj, 'output_buffers'):
342 # Already patched.
343 return fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000344
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000345 # Attribute 'XXX' defined outside __init__
346 # pylint: disable=W0201
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000347 new_fileobj = SoftClone(fileobj)
348 if not hasattr(new_fileobj, 'lock'):
349 new_fileobj.lock = threading.Lock()
350 new_fileobj.output_buffers = {}
351 new_fileobj.old_annotated_write = new_fileobj.write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000352
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000353 def annotated_write(out):
354 index = getattr(threading.currentThread(), 'index', None)
355 if index is None:
356 # Undexed threads aren't buffered.
357 new_fileobj.old_annotated_write(out)
358 return
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000359
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000360 new_fileobj.lock.acquire()
361 try:
362 # Use a dummy array to hold the string so the code can be lockless.
363 # Strings are immutable, requiring to keep a lock for the whole dictionary
364 # otherwise. Using an array is faster than using a dummy object.
365 if not index in new_fileobj.output_buffers:
366 obj = new_fileobj.output_buffers[index] = ['']
367 else:
368 obj = new_fileobj.output_buffers[index]
369 finally:
370 new_fileobj.lock.release()
371
372 # Continue lockless.
373 obj[0] += out
374 while '\n' in obj[0]:
375 line, remaining = obj[0].split('\n', 1)
376 new_fileobj.old_annotated_write('%d>%s\n' % (index, line))
377 obj[0] = remaining
378
379 def full_flush():
380 """Flush buffered output."""
381 orphans = []
382 new_fileobj.lock.acquire()
383 try:
384 # Detect threads no longer existing.
385 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000386 indexes = filter(None, indexes)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000387 for index in new_fileobj.output_buffers:
388 if not index in indexes:
389 orphans.append((index, new_fileobj.output_buffers[index][0]))
390 for orphan in orphans:
391 del new_fileobj.output_buffers[orphan[0]]
392 finally:
393 new_fileobj.lock.release()
394
395 # Don't keep the lock while writting. Will append \n when it shouldn't.
396 for orphan in orphans:
397 new_fileobj.old_annotated_write('%d>%s\n' % (orphan[0], orphan[1]))
398
399 new_fileobj.write = annotated_write
400 new_fileobj.full_flush = full_flush
401 return new_fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000402
403
maruel@chromium.org17d01792010-09-01 18:07:10 +0000404def CheckCallAndFilter(args, stdout=None, filter_fn=None,
405 print_stdout=None, call_filter_on_first_line=False,
406 **kwargs):
407 """Runs a command and calls back a filter function if needed.
408
409 Accepts all subprocess.Popen() parameters plus:
410 print_stdout: If True, the command's stdout is forwarded to stdout.
411 filter_fn: A function taking a single string argument called with each line
412 of the subprocess's output. Each line has the trailing newline
413 character trimmed.
414 stdout: Can be any bufferable output.
415
416 stderr is always redirected to stdout.
417 """
418 assert print_stdout or filter_fn
419 stdout = stdout or sys.stdout
420 filter_fn = filter_fn or (lambda x: None)
421 assert not 'stderr' in kwargs
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000422 kid = Popen(args, bufsize=0,
423 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
424 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000425
maruel@chromium.org17d01792010-09-01 18:07:10 +0000426 # Do a flush of stdout before we begin reading from the subprocess's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000427 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000428
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000429 # Also, we need to forward stdout to prevent weird re-ordering of output.
430 # This has to be done on a per byte basis to make sure it is not buffered:
431 # normally buffering is done for each line, but if svn requests input, no
432 # end-of-line character is output after the prompt and it would not show up.
433 in_byte = kid.stdout.read(1)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000434 if in_byte:
435 if call_filter_on_first_line:
436 filter_fn(None)
437 in_line = ''
438 while in_byte:
439 if in_byte != '\r':
440 if print_stdout:
441 stdout.write(in_byte)
442 if in_byte != '\n':
443 in_line += in_byte
444 else:
445 filter_fn(in_line)
446 in_line = ''
maruel@chromium.org17d01792010-09-01 18:07:10 +0000447 in_byte = kid.stdout.read(1)
448 # Flush the rest of buffered output. This is only an issue with
449 # stdout/stderr not ending with a \n.
450 if len(in_line):
451 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000452 rv = kid.wait()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000453 if rv:
maruel@chromium.org7b194c12010-09-07 20:57:09 +0000454 raise CheckCallError(args, kwargs.get('cwd', None), rv, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000455 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000456
457
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000458def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000459 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000460 real_from_dir = os.path.realpath(from_dir)
461 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000462 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000463 split_path = os.path.split(path)
464 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000465 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000466 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000467
468 # If we did not find the file in the current directory, make sure we are in a
469 # sub directory that is controlled by this configuration.
470 if path != real_from_dir:
471 entries_filename = os.path.join(path, filename + '_entries')
472 if not os.path.exists(entries_filename):
473 # If .gclient_entries does not exist, a previous call to gclient sync
474 # might have failed. In that case, we cannot verify that the .gclient
475 # is the one we want to use. In order to not to cause too much trouble,
476 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000477 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000478 "file you want to use" % (filename, path))
479 return path
480 scope = {}
481 try:
482 exec(FileRead(entries_filename), scope)
483 except SyntaxError, e:
484 SyntaxErrorToError(filename, e)
485 all_directories = scope['entries'].keys()
486 path_to_check = real_from_dir[len(path)+1:]
487 while path_to_check:
488 if path_to_check in all_directories:
489 return path
490 path_to_check = os.path.dirname(path_to_check)
491 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000492
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000493 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000494 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000495
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000496
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000497def PathDifference(root, subpath):
498 """Returns the difference subpath minus root."""
499 root = os.path.realpath(root)
500 subpath = os.path.realpath(subpath)
501 if not subpath.startswith(root):
502 return None
503 # If the root does not have a trailing \ or /, we add it so the returned
504 # path starts immediately after the seperator regardless of whether it is
505 # provided.
506 root = os.path.join(root, '')
507 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000508
509
510def FindFileUpwards(filename, path=None):
511 """Search upwards from the a directory (default: current) to find a file."""
512 if not path:
513 path = os.getcwd()
514 path = os.path.realpath(path)
515 while True:
516 file_path = os.path.join(path, filename)
517 if os.path.isfile(file_path):
518 return file_path
519 (new_path, _) = os.path.split(path)
520 if new_path == path:
521 return None
522 path = new_path
523
524
525def GetGClientRootAndEntries(path=None):
526 """Returns the gclient root and the dict of entries."""
527 config_file = '.gclient_entries'
528 config_path = FindFileUpwards(config_file, path)
529
530 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000531 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000532 return None
533
534 env = {}
535 execfile(config_path, env)
536 config_dir = os.path.dirname(config_path)
537 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000538
539
540class WorkItem(object):
541 """One work item."""
542 # A list of string, each being a WorkItem name.
543 requirements = []
544 # A unique string representing this work item.
545 name = None
546
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000547 def run(self, work_queue):
548 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000549 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000550 pass
551
552
553class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000554 """Runs a set of WorkItem that have interdependencies and were WorkItem are
555 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000556
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000557 In gclient's case, Dependencies sometime needs to be run out of order due to
558 From() keyword. This class manages that all the required dependencies are run
559 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000560
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000561 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000562 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000563 def __init__(self, jobs, progress):
564 """jobs specifies the number of concurrent tasks to allow. progress is a
565 Progress instance."""
maruel@chromium.org06617272010-11-04 13:50:50 +0000566 hack_subprocess()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000567 # Set when a thread is done or a new item is enqueued.
568 self.ready_cond = threading.Condition()
569 # Maximum number of concurrent tasks.
570 self.jobs = jobs
571 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000572 self.queued = []
573 # List of strings representing each Dependency.name that was run.
574 self.ran = []
575 # List of items currently running.
576 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000577 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000578 self.exceptions = Queue.Queue()
579 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000580 self.progress = progress
581 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000582 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000583
584 def enqueue(self, d):
585 """Enqueue one Dependency to be executed later once its requirements are
586 satisfied.
587 """
588 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000589 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000590 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000591 self.queued.append(d)
592 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000593 logging.debug('enqueued(%s)' % d.name)
594 if self.progress:
595 self.progress._total = total + 1
596 self.progress.update(0)
597 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000598 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000599 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000600
601 def flush(self, *args, **kwargs):
602 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000603 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000604 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000605 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000606 while True:
607 # Check for task to run first, then wait.
608 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000609 if not self.exceptions.empty():
610 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000611 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000612 self._flush_terminated_threads()
613 if (not self.queued and not self.running or
614 self.jobs == len(self.running)):
615 # No more worker threads or can't queue anything.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000616 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000617
618 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000619 for i in xrange(len(self.queued)):
620 # Verify its requirements.
621 for r in self.queued[i].requirements:
622 if not r in self.ran:
623 # Requirement not met.
624 break
625 else:
626 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000627 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000628 break
629 else:
630 # Couldn't find an item that could run. Break out the outher loop.
631 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000632
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000633 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000634 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000635 break
636 # We need to poll here otherwise Ctrl-C isn't processed.
637 self.ready_cond.wait(10)
638 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000639 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000640 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000641
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000642 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000643 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000644 # To get back the stack location correctly, the raise a, b, c form must be
645 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000646 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000647 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000648 if self.progress:
649 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000650
maruel@chromium.org3742c842010-09-09 19:27:14 +0000651 def _flush_terminated_threads(self):
652 """Flush threads that have terminated."""
653 running = self.running
654 self.running = []
655 for t in running:
656 if t.isAlive():
657 self.running.append(t)
658 else:
659 t.join()
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000660 sys.stdout.full_flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000661 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000662 self.progress.update(1, t.item.name)
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000663 assert not t.item.name in self.ran
664 if not t.item.name in self.ran:
665 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000666
667 def _run_one_task(self, task_item, args, kwargs):
668 if self.jobs > 1:
669 # Start the thread.
670 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000671 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000672 self.running.append(new_thread)
673 new_thread.start()
674 else:
675 # Run the 'thread' inside the main thread. Don't try to catch any
676 # exception.
677 task_item.run(*args, **kwargs)
678 self.ran.append(task_item.name)
679 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000680 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000681
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000682 class _Worker(threading.Thread):
683 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000684 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000685 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org3742c842010-09-09 19:27:14 +0000686 logging.info(item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000687 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000688 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000689 self.args = args
690 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000691
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000692 def run(self):
693 """Runs in its own thread."""
694 logging.debug('running(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000695 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000696 try:
697 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000698 except Exception:
699 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000700 logging.info('Caught exception in thread %s' % self.item.name)
701 logging.info(str(sys.exc_info()))
702 work_queue.exceptions.put(sys.exc_info())
703 logging.info('Task %s done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000704
maruel@chromium.org3742c842010-09-09 19:27:14 +0000705 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000706 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000707 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000708 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000709 work_queue.ready_cond.release()