blob: 97c8227c097370f2e933f759a5a268e752d72020 [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.
maruel@chromium.org1edee692011-03-12 19:39:13 +0000219 win32api = None
220 win32con = None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000221 try:
maruel@chromium.org1edee692011-03-12 19:39:13 +0000222 # Unable to import 'XX'
223 # pylint: disable=F0401
224 import win32api, win32con
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000225 except ImportError:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000226 pass
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000227 else:
228 # On POSIX systems, we need the x-bit set on the directory to access it,
229 # the r-bit to see its contents, and the w-bit to remove files from it.
230 # The actual modes of the files within the directory is irrelevant.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000231 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000232
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000233 def remove(func, subpath):
234 if sys.platform == 'win32':
235 os.chmod(subpath, stat.S_IWRITE)
236 if win32api and win32con:
237 win32api.SetFileAttributes(subpath, win32con.FILE_ATTRIBUTE_NORMAL)
238 try:
239 func(subpath)
240 except OSError, e:
241 if e.errno != errno.EACCES or sys.platform != 'win32':
242 raise
243 # Failed to delete, try again after a 100ms sleep.
244 time.sleep(0.1)
245 func(subpath)
246
247 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000248 # If fullpath is a symbolic link that points to a directory, isdir will
249 # be True, but we don't want to descend into that as a directory, we just
250 # want to remove the link. Check islink and treat links as ordinary files
251 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000252 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000253 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000254 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000255 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000256 # Recurse.
257 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000258
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000259 remove(os.rmdir, path)
260
261# TODO(maruel): Rename the references.
262RemoveDirectory = rmtree
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000263
264
maruel@chromium.org17d01792010-09-01 18:07:10 +0000265def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
266 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000267
maruel@chromium.org17d01792010-09-01 18:07:10 +0000268 If |always| is True, a message indicating what is being done
269 is printed to stdout all the time even if not output is generated. Otherwise
270 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000271 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000272 stdout = kwargs.get('stdout', None) or sys.stdout
273 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000274 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000275 % (' '.join(args), kwargs.get('cwd', '.')))
276 else:
277 filter_fn = kwargs.get('filter_fn', None)
278 def filter_msg(line):
279 if line is None:
280 stdout.write('\n________ running \'%s\' in \'%s\'\n'
281 % (' '.join(args), kwargs.get('cwd', '.')))
282 elif filter_fn:
283 filter_fn(line)
284 kwargs['filter_fn'] = filter_msg
285 kwargs['call_filter_on_first_line'] = True
286 # Obviously.
287 kwargs['print_stdout'] = True
288 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000289
maruel@chromium.org17d01792010-09-01 18:07:10 +0000290
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000291def SoftClone(obj):
292 """Clones an object. copy.copy() doesn't work on 'file' objects."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000293 if obj.__class__.__name__ == 'SoftCloned':
294 return obj
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000295 class SoftCloned(object):
296 pass
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000297 new_obj = SoftCloned()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000298 for member in dir(obj):
299 if member.startswith('_'):
300 continue
301 setattr(new_obj, member, getattr(obj, member))
302 return new_obj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000303
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000304
305def MakeFileAutoFlush(fileobj, delay=10):
306 """Creates a file object clone to automatically flush after N seconds."""
307 if hasattr(fileobj, 'last_flushed_at'):
308 # Already patched. Just update delay.
309 fileobj.delay = delay
310 return fileobj
311
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000312 # Attribute 'XXX' defined outside __init__
313 # pylint: disable=W0201
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000314 new_fileobj = SoftClone(fileobj)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000315 if not hasattr(new_fileobj, 'lock'):
316 new_fileobj.lock = threading.Lock()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000317 new_fileobj.last_flushed_at = time.time()
318 new_fileobj.delay = delay
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000319 new_fileobj.old_auto_flush_write = new_fileobj.write
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000320 # Silence pylint.
321 new_fileobj.flush = fileobj.flush
322
323 def auto_flush_write(out):
324 new_fileobj.old_auto_flush_write(out)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000325 should_flush = False
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000326 new_fileobj.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000327 try:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000328 if (new_fileobj.delay and
329 (time.time() - new_fileobj.last_flushed_at) > new_fileobj.delay):
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000330 should_flush = True
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000331 new_fileobj.last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000332 finally:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000333 new_fileobj.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000334 if should_flush:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000335 new_fileobj.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000336
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000337 new_fileobj.write = auto_flush_write
338 return new_fileobj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000339
340
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000341def MakeFileAnnotated(fileobj):
342 """Creates a file object clone to automatically prepends every line in worker
343 threads with a NN> prefix."""
344 if hasattr(fileobj, 'output_buffers'):
345 # Already patched.
346 return fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000347
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000348 # Attribute 'XXX' defined outside __init__
349 # pylint: disable=W0201
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000350 new_fileobj = SoftClone(fileobj)
351 if not hasattr(new_fileobj, 'lock'):
352 new_fileobj.lock = threading.Lock()
353 new_fileobj.output_buffers = {}
354 new_fileobj.old_annotated_write = new_fileobj.write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000355
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000356 def annotated_write(out):
357 index = getattr(threading.currentThread(), 'index', None)
358 if index is None:
359 # Undexed threads aren't buffered.
360 new_fileobj.old_annotated_write(out)
361 return
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000362
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000363 new_fileobj.lock.acquire()
364 try:
365 # Use a dummy array to hold the string so the code can be lockless.
366 # Strings are immutable, requiring to keep a lock for the whole dictionary
367 # otherwise. Using an array is faster than using a dummy object.
368 if not index in new_fileobj.output_buffers:
369 obj = new_fileobj.output_buffers[index] = ['']
370 else:
371 obj = new_fileobj.output_buffers[index]
372 finally:
373 new_fileobj.lock.release()
374
375 # Continue lockless.
376 obj[0] += out
377 while '\n' in obj[0]:
378 line, remaining = obj[0].split('\n', 1)
379 new_fileobj.old_annotated_write('%d>%s\n' % (index, line))
380 obj[0] = remaining
381
382 def full_flush():
383 """Flush buffered output."""
384 orphans = []
385 new_fileobj.lock.acquire()
386 try:
387 # Detect threads no longer existing.
388 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000389 indexes = filter(None, indexes)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000390 for index in new_fileobj.output_buffers:
391 if not index in indexes:
392 orphans.append((index, new_fileobj.output_buffers[index][0]))
393 for orphan in orphans:
394 del new_fileobj.output_buffers[orphan[0]]
395 finally:
396 new_fileobj.lock.release()
397
398 # Don't keep the lock while writting. Will append \n when it shouldn't.
399 for orphan in orphans:
400 new_fileobj.old_annotated_write('%d>%s\n' % (orphan[0], orphan[1]))
401
402 new_fileobj.write = annotated_write
403 new_fileobj.full_flush = full_flush
404 return new_fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000405
406
maruel@chromium.org17d01792010-09-01 18:07:10 +0000407def CheckCallAndFilter(args, stdout=None, filter_fn=None,
408 print_stdout=None, call_filter_on_first_line=False,
409 **kwargs):
410 """Runs a command and calls back a filter function if needed.
411
412 Accepts all subprocess.Popen() parameters plus:
413 print_stdout: If True, the command's stdout is forwarded to stdout.
414 filter_fn: A function taking a single string argument called with each line
415 of the subprocess's output. Each line has the trailing newline
416 character trimmed.
417 stdout: Can be any bufferable output.
418
419 stderr is always redirected to stdout.
420 """
421 assert print_stdout or filter_fn
422 stdout = stdout or sys.stdout
423 filter_fn = filter_fn or (lambda x: None)
424 assert not 'stderr' in kwargs
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000425 kid = Popen(args, bufsize=0,
426 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
427 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000428
maruel@chromium.org17d01792010-09-01 18:07:10 +0000429 # Do a flush of stdout before we begin reading from the subprocess's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000430 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000431
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000432 # Also, we need to forward stdout to prevent weird re-ordering of output.
433 # This has to be done on a per byte basis to make sure it is not buffered:
434 # normally buffering is done for each line, but if svn requests input, no
435 # end-of-line character is output after the prompt and it would not show up.
436 in_byte = kid.stdout.read(1)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000437 if in_byte:
438 if call_filter_on_first_line:
439 filter_fn(None)
440 in_line = ''
441 while in_byte:
442 if in_byte != '\r':
443 if print_stdout:
444 stdout.write(in_byte)
445 if in_byte != '\n':
446 in_line += in_byte
447 else:
448 filter_fn(in_line)
449 in_line = ''
maruel@chromium.org17d01792010-09-01 18:07:10 +0000450 in_byte = kid.stdout.read(1)
451 # Flush the rest of buffered output. This is only an issue with
452 # stdout/stderr not ending with a \n.
453 if len(in_line):
454 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000455 rv = kid.wait()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000456 if rv:
maruel@chromium.org7b194c12010-09-07 20:57:09 +0000457 raise CheckCallError(args, kwargs.get('cwd', None), rv, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000458 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000459
460
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000461def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000462 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000463 real_from_dir = os.path.realpath(from_dir)
464 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000465 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000466 split_path = os.path.split(path)
467 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000468 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000469 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000470
471 # If we did not find the file in the current directory, make sure we are in a
472 # sub directory that is controlled by this configuration.
473 if path != real_from_dir:
474 entries_filename = os.path.join(path, filename + '_entries')
475 if not os.path.exists(entries_filename):
476 # If .gclient_entries does not exist, a previous call to gclient sync
477 # might have failed. In that case, we cannot verify that the .gclient
478 # is the one we want to use. In order to not to cause too much trouble,
479 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000480 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000481 "file you want to use" % (filename, path))
482 return path
483 scope = {}
484 try:
485 exec(FileRead(entries_filename), scope)
486 except SyntaxError, e:
487 SyntaxErrorToError(filename, e)
488 all_directories = scope['entries'].keys()
489 path_to_check = real_from_dir[len(path)+1:]
490 while path_to_check:
491 if path_to_check in all_directories:
492 return path
493 path_to_check = os.path.dirname(path_to_check)
494 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000495
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000496 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000497 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000498
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000499
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000500def PathDifference(root, subpath):
501 """Returns the difference subpath minus root."""
502 root = os.path.realpath(root)
503 subpath = os.path.realpath(subpath)
504 if not subpath.startswith(root):
505 return None
506 # If the root does not have a trailing \ or /, we add it so the returned
507 # path starts immediately after the seperator regardless of whether it is
508 # provided.
509 root = os.path.join(root, '')
510 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000511
512
513def FindFileUpwards(filename, path=None):
514 """Search upwards from the a directory (default: current) to find a file."""
515 if not path:
516 path = os.getcwd()
517 path = os.path.realpath(path)
518 while True:
519 file_path = os.path.join(path, filename)
520 if os.path.isfile(file_path):
521 return file_path
522 (new_path, _) = os.path.split(path)
523 if new_path == path:
524 return None
525 path = new_path
526
527
528def GetGClientRootAndEntries(path=None):
529 """Returns the gclient root and the dict of entries."""
530 config_file = '.gclient_entries'
531 config_path = FindFileUpwards(config_file, path)
532
533 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000534 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000535 return None
536
537 env = {}
538 execfile(config_path, env)
539 config_dir = os.path.dirname(config_path)
540 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000541
542
543class WorkItem(object):
544 """One work item."""
545 # A list of string, each being a WorkItem name.
546 requirements = []
547 # A unique string representing this work item.
548 name = None
549
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000550 def run(self, work_queue):
551 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000552 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000553 pass
554
555
556class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000557 """Runs a set of WorkItem that have interdependencies and were WorkItem are
558 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000559
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000560 In gclient's case, Dependencies sometime needs to be run out of order due to
561 From() keyword. This class manages that all the required dependencies are run
562 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000563
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000564 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000565 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000566 def __init__(self, jobs, progress):
567 """jobs specifies the number of concurrent tasks to allow. progress is a
568 Progress instance."""
maruel@chromium.org06617272010-11-04 13:50:50 +0000569 hack_subprocess()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000570 # Set when a thread is done or a new item is enqueued.
571 self.ready_cond = threading.Condition()
572 # Maximum number of concurrent tasks.
573 self.jobs = jobs
574 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000575 self.queued = []
576 # List of strings representing each Dependency.name that was run.
577 self.ran = []
578 # List of items currently running.
579 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000580 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000581 self.exceptions = Queue.Queue()
582 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000583 self.progress = progress
584 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000585 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000586
587 def enqueue(self, d):
588 """Enqueue one Dependency to be executed later once its requirements are
589 satisfied.
590 """
591 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000592 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000593 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000594 self.queued.append(d)
595 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000596 logging.debug('enqueued(%s)' % d.name)
597 if self.progress:
598 self.progress._total = total + 1
599 self.progress.update(0)
600 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000601 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000602 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000603
604 def flush(self, *args, **kwargs):
605 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000606 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000607 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000608 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000609 while True:
610 # Check for task to run first, then wait.
611 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000612 if not self.exceptions.empty():
613 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000614 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000615 self._flush_terminated_threads()
616 if (not self.queued and not self.running or
617 self.jobs == len(self.running)):
618 # No more worker threads or can't queue anything.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000619 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000620
621 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000622 for i in xrange(len(self.queued)):
623 # Verify its requirements.
624 for r in self.queued[i].requirements:
625 if not r in self.ran:
626 # Requirement not met.
627 break
628 else:
629 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000630 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000631 break
632 else:
633 # Couldn't find an item that could run. Break out the outher loop.
634 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000635
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000636 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000637 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000638 break
639 # We need to poll here otherwise Ctrl-C isn't processed.
640 self.ready_cond.wait(10)
641 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000642 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000643 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000644
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000645 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000646 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000647 # To get back the stack location correctly, the raise a, b, c form must be
648 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000649 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000650 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000651 if self.progress:
652 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000653
maruel@chromium.org3742c842010-09-09 19:27:14 +0000654 def _flush_terminated_threads(self):
655 """Flush threads that have terminated."""
656 running = self.running
657 self.running = []
658 for t in running:
659 if t.isAlive():
660 self.running.append(t)
661 else:
662 t.join()
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000663 sys.stdout.full_flush() # pylint: disable=E1101
maruel@chromium.org3742c842010-09-09 19:27:14 +0000664 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000665 self.progress.update(1, t.item.name)
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000666 assert not t.item.name in self.ran
667 if not t.item.name in self.ran:
668 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000669
670 def _run_one_task(self, task_item, args, kwargs):
671 if self.jobs > 1:
672 # Start the thread.
673 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000674 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000675 self.running.append(new_thread)
676 new_thread.start()
677 else:
678 # Run the 'thread' inside the main thread. Don't try to catch any
679 # exception.
680 task_item.run(*args, **kwargs)
681 self.ran.append(task_item.name)
682 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000683 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000684
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000685 class _Worker(threading.Thread):
686 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000687 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000688 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org3742c842010-09-09 19:27:14 +0000689 logging.info(item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000690 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000691 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000692 self.args = args
693 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000694
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000695 def run(self):
696 """Runs in its own thread."""
697 logging.debug('running(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000698 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000699 try:
700 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000701 except Exception:
702 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000703 logging.info('Caught exception in thread %s' % self.item.name)
704 logging.info(str(sys.exc_info()))
705 work_queue.exceptions.put(sys.exc_info())
706 logging.info('Task %s done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000707
maruel@chromium.org3742c842010-09-09 19:27:14 +0000708 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000709 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000710 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000711 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000712 work_queue.ready_cond.release()