blob: 81a1f667e63f3e45434b1073fecd452171970b20 [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 +000017
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000018
maruel@chromium.org58ef2972011-04-01 21:00:11 +000019def hack_subprocess():
20 """subprocess functions may throw exceptions when used in multiple threads.
21
22 See http://bugs.python.org/issue1731717 for more information.
23 """
24 subprocess._cleanup = lambda: None
maruel@chromium.org06617272010-11-04 13:50:50 +000025
26
maruel@chromium.org66c83e62010-09-07 14:18:45 +000027class Error(Exception):
28 """gclient exception class."""
29 pass
30
31
32class CheckCallError(OSError, Error):
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000033 """CheckCall() returned non-0."""
maruel@chromium.org66c83e62010-09-07 14:18:45 +000034 def __init__(self, command, cwd, returncode, stdout, stderr=None):
maruel@chromium.orgff3e4a82011-04-23 01:23:42 +000035 OSError.__init__(self, command, cwd, returncode)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +000036 Error.__init__(self, command)
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000037 self.command = command
38 self.cwd = cwd
maruel@chromium.org66c83e62010-09-07 14:18:45 +000039 self.returncode = returncode
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000040 self.stdout = stdout
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000041 self.stderr = stderr
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000042
maruel@chromium.org7b194c12010-09-07 20:57:09 +000043 def __str__(self):
44 out = ' '.join(self.command)
45 if self.cwd:
46 out += ' in ' + self.cwd
47 if self.returncode is not None:
48 out += ' returned %d' % self.returncode
49 if self.stdout is not None:
50 out += '\nstdout: %s\n' % self.stdout
51 if self.stderr is not None:
52 out += '\nstderr: %s\n' % self.stderr
53 return out
54
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000055
maruel@chromium.org58ef2972011-04-01 21:00:11 +000056def Popen(args, **kwargs):
57 """Calls subprocess.Popen() with hacks to work around certain behaviors.
58
59 Ensure English outpout for svn and make it work reliably on Windows.
60 """
61 logging.debug(u'%s, cwd=%s' % (u' '.join(args), kwargs.get('cwd', '')))
62 if not 'env' in kwargs:
63 # It's easier to parse the stdout if it is always in English.
64 kwargs['env'] = os.environ.copy()
65 kwargs['env']['LANGUAGE'] = 'en'
66 if not 'shell' in kwargs:
67 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
68 # executable, but shell=True makes subprocess on Linux fail when it's called
69 # with a list because it only tries to execute the first item in the list.
70 kwargs['shell'] = (sys.platform=='win32')
71 try:
72 return subprocess.Popen(args, **kwargs)
73 except OSError, e:
74 if e.errno == errno.EAGAIN and sys.platform == 'cygwin':
75 raise Error(
76 'Visit '
77 'http://code.google.com/p/chromium/wiki/CygwinDllRemappingFailure to '
78 'learn how to fix this error; you need to rebase your cygwin dlls')
79 raise
80
81
maruel@chromium.orgac610232010-10-13 14:01:31 +000082def CheckCall(command, print_error=True, **kwargs):
maruel@chromium.org3a292682010-08-23 18:54:55 +000083 """Similar subprocess.check_call() but redirects stdout and
84 returns (stdout, stderr).
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000085
86 Works on python 2.4
87 """
maruel@chromium.org18111352009-12-20 17:21:28 +000088 try:
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000089 stderr = None
90 if not print_error:
91 stderr = subprocess.PIPE
maruel@chromium.orgac610232010-10-13 14:01:31 +000092 process = Popen(command, stdout=subprocess.PIPE, stderr=stderr, **kwargs)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000093 std_out, std_err = process.communicate()
maruel@chromium.org18111352009-12-20 17:21:28 +000094 except OSError, e:
maruel@chromium.orgac610232010-10-13 14:01:31 +000095 raise CheckCallError(command, kwargs.get('cwd', None), e.errno, None)
maruel@chromium.org18111352009-12-20 17:21:28 +000096 if process.returncode:
maruel@chromium.orgac610232010-10-13 14:01:31 +000097 raise CheckCallError(command, kwargs.get('cwd', None), process.returncode,
98 std_out, std_err)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000099 return std_out, std_err
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +0000100
101
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000102def SplitUrlRevision(url):
103 """Splits url and returns a two-tuple: url, rev"""
104 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +0000105 # Make sure ssh://user-name@example.com/~/test.git@stable works
106 regex = r'(ssh://(?:[-\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000107 components = re.search(regex, url).groups()
108 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000109 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000110 if len(components) == 1:
111 components += [None]
112 return tuple(components)
113
114
floitsch@google.comeaab7842011-04-28 09:07:58 +0000115def IsDateRevision(revision):
116 """Returns true if the given revision is of the form "{ ... }"."""
117 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
118
119
120def MakeDateRevision(date):
121 """Returns a revision representing the latest revision before the given
122 date."""
123 return "{" + date + "}"
124
125
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000126def SyntaxErrorToError(filename, e):
127 """Raises a gclient_utils.Error exception with the human readable message"""
128 try:
129 # Try to construct a human readable error message
130 if filename:
131 error_message = 'There is a syntax error in %s\n' % filename
132 else:
133 error_message = 'There is a syntax error\n'
134 error_message += 'Line #%s, character %s: "%s"' % (
135 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
136 except:
137 # Something went wrong, re-raise the original exception
138 raise e
139 else:
140 raise Error(error_message)
141
142
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000143class PrintableObject(object):
144 def __str__(self):
145 output = ''
146 for i in dir(self):
147 if i.startswith('__'):
148 continue
149 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
150 return output
151
152
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000153def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000154 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000155 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000156 try:
157 content = f.read()
158 finally:
159 f.close()
160 return content
161
162
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000163def FileWrite(filename, content, mode='w'):
164 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000165 try:
166 f.write(content)
167 finally:
168 f.close()
169
170
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000171def rmtree(path):
172 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000173
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000174 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000175
176 shutil.rmtree() doesn't work on Windows if any of the files or directories
177 are read-only, which svn repositories and some .svn files are. We need to
178 be able to force the files to be writable (i.e., deletable) as we traverse
179 the tree.
180
181 Even with all this, Windows still sometimes fails to delete a file, citing
182 a permission error (maybe something to do with antivirus scans or disk
183 indexing). The best suggestion any of the user forums had was to wait a
184 bit and try again, so we do that too. It's hand-waving, but sometimes it
185 works. :/
186
187 On POSIX systems, things are a little bit simpler. The modes of the files
188 to be deleted doesn't matter, only the modes of the directories containing
189 them are significant. As the directory tree is traversed, each directory
190 has its mode set appropriately before descending into it. This should
191 result in the entire tree being removed, with the possible exception of
192 *path itself, because nothing attempts to change the mode of its parent.
193 Doing so would be hazardous, as it's not a directory slated for removal.
194 In the ordinary case, this is not a problem: for our purposes, the user
195 will never lack write permission on *path's parent.
196 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000197 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000198 return
199
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000200 if os.path.islink(path) or not os.path.isdir(path):
201 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000202
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000203 if sys.platform == 'win32':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000204 # Some people don't have the APIs installed. In that case we'll do without.
maruel@chromium.org1edee692011-03-12 19:39:13 +0000205 win32api = None
206 win32con = None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000207 try:
maruel@chromium.org1edee692011-03-12 19:39:13 +0000208 # Unable to import 'XX'
209 # pylint: disable=F0401
210 import win32api, win32con
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000211 except ImportError:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000212 pass
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000213 else:
214 # On POSIX systems, we need the x-bit set on the directory to access it,
215 # the r-bit to see its contents, and the w-bit to remove files from it.
216 # The actual modes of the files within the directory is irrelevant.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000217 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000218
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000219 def remove(func, subpath):
220 if sys.platform == 'win32':
221 os.chmod(subpath, stat.S_IWRITE)
222 if win32api and win32con:
223 win32api.SetFileAttributes(subpath, win32con.FILE_ATTRIBUTE_NORMAL)
224 try:
225 func(subpath)
226 except OSError, e:
227 if e.errno != errno.EACCES or sys.platform != 'win32':
228 raise
229 # Failed to delete, try again after a 100ms sleep.
230 time.sleep(0.1)
231 func(subpath)
232
233 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000234 # If fullpath is a symbolic link that points to a directory, isdir will
235 # be True, but we don't want to descend into that as a directory, we just
236 # want to remove the link. Check islink and treat links as ordinary files
237 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000238 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000239 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000240 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000241 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000242 # Recurse.
243 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000244
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000245 remove(os.rmdir, path)
246
247# TODO(maruel): Rename the references.
248RemoveDirectory = rmtree
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000249
250
maruel@chromium.org17d01792010-09-01 18:07:10 +0000251def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
252 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000253
maruel@chromium.org17d01792010-09-01 18:07:10 +0000254 If |always| is True, a message indicating what is being done
255 is printed to stdout all the time even if not output is generated. Otherwise
256 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000257 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000258 stdout = kwargs.get('stdout', None) or sys.stdout
259 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000260 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000261 % (' '.join(args), kwargs.get('cwd', '.')))
262 else:
263 filter_fn = kwargs.get('filter_fn', None)
264 def filter_msg(line):
265 if line is None:
266 stdout.write('\n________ running \'%s\' in \'%s\'\n'
267 % (' '.join(args), kwargs.get('cwd', '.')))
268 elif filter_fn:
269 filter_fn(line)
270 kwargs['filter_fn'] = filter_msg
271 kwargs['call_filter_on_first_line'] = True
272 # Obviously.
273 kwargs['print_stdout'] = True
274 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000275
maruel@chromium.org17d01792010-09-01 18:07:10 +0000276
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000277def SoftClone(obj):
278 """Clones an object. copy.copy() doesn't work on 'file' objects."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000279 if obj.__class__.__name__ == 'SoftCloned':
280 return obj
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000281 class SoftCloned(object):
282 pass
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000283 new_obj = SoftCloned()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000284 for member in dir(obj):
285 if member.startswith('_'):
286 continue
287 setattr(new_obj, member, getattr(obj, member))
288 return new_obj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000289
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000290
291def MakeFileAutoFlush(fileobj, delay=10):
292 """Creates a file object clone to automatically flush after N seconds."""
293 if hasattr(fileobj, 'last_flushed_at'):
294 # Already patched. Just update delay.
295 fileobj.delay = delay
296 return fileobj
297
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000298 # Attribute 'XXX' defined outside __init__
299 # pylint: disable=W0201
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000300 new_fileobj = SoftClone(fileobj)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000301 if not hasattr(new_fileobj, 'lock'):
302 new_fileobj.lock = threading.Lock()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000303 new_fileobj.last_flushed_at = time.time()
304 new_fileobj.delay = delay
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000305 new_fileobj.old_auto_flush_write = new_fileobj.write
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000306 # Silence pylint.
307 new_fileobj.flush = fileobj.flush
308
309 def auto_flush_write(out):
310 new_fileobj.old_auto_flush_write(out)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000311 should_flush = False
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000312 new_fileobj.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000313 try:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000314 if (new_fileobj.delay and
315 (time.time() - new_fileobj.last_flushed_at) > new_fileobj.delay):
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000316 should_flush = True
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000317 new_fileobj.last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000318 finally:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000319 new_fileobj.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000320 if should_flush:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000321 new_fileobj.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000322
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000323 new_fileobj.write = auto_flush_write
324 return new_fileobj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000325
326
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000327def MakeFileAnnotated(fileobj):
328 """Creates a file object clone to automatically prepends every line in worker
329 threads with a NN> prefix."""
330 if hasattr(fileobj, 'output_buffers'):
331 # Already patched.
332 return fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000333
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000334 # Attribute 'XXX' defined outside __init__
335 # pylint: disable=W0201
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000336 new_fileobj = SoftClone(fileobj)
337 if not hasattr(new_fileobj, 'lock'):
338 new_fileobj.lock = threading.Lock()
339 new_fileobj.output_buffers = {}
340 new_fileobj.old_annotated_write = new_fileobj.write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000341
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000342 def annotated_write(out):
343 index = getattr(threading.currentThread(), 'index', None)
344 if index is None:
345 # Undexed threads aren't buffered.
346 new_fileobj.old_annotated_write(out)
347 return
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000348
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000349 new_fileobj.lock.acquire()
350 try:
351 # Use a dummy array to hold the string so the code can be lockless.
352 # Strings are immutable, requiring to keep a lock for the whole dictionary
353 # otherwise. Using an array is faster than using a dummy object.
354 if not index in new_fileobj.output_buffers:
355 obj = new_fileobj.output_buffers[index] = ['']
356 else:
357 obj = new_fileobj.output_buffers[index]
358 finally:
359 new_fileobj.lock.release()
360
361 # Continue lockless.
362 obj[0] += out
363 while '\n' in obj[0]:
364 line, remaining = obj[0].split('\n', 1)
365 new_fileobj.old_annotated_write('%d>%s\n' % (index, line))
366 obj[0] = remaining
367
368 def full_flush():
369 """Flush buffered output."""
370 orphans = []
371 new_fileobj.lock.acquire()
372 try:
373 # Detect threads no longer existing.
374 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000375 indexes = filter(None, indexes)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000376 for index in new_fileobj.output_buffers:
377 if not index in indexes:
378 orphans.append((index, new_fileobj.output_buffers[index][0]))
379 for orphan in orphans:
380 del new_fileobj.output_buffers[orphan[0]]
381 finally:
382 new_fileobj.lock.release()
383
384 # Don't keep the lock while writting. Will append \n when it shouldn't.
385 for orphan in orphans:
386 new_fileobj.old_annotated_write('%d>%s\n' % (orphan[0], orphan[1]))
387
388 new_fileobj.write = annotated_write
389 new_fileobj.full_flush = full_flush
390 return new_fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000391
392
maruel@chromium.org17d01792010-09-01 18:07:10 +0000393def CheckCallAndFilter(args, stdout=None, filter_fn=None,
394 print_stdout=None, call_filter_on_first_line=False,
395 **kwargs):
396 """Runs a command and calls back a filter function if needed.
397
398 Accepts all subprocess.Popen() parameters plus:
399 print_stdout: If True, the command's stdout is forwarded to stdout.
400 filter_fn: A function taking a single string argument called with each line
401 of the subprocess's output. Each line has the trailing newline
402 character trimmed.
403 stdout: Can be any bufferable output.
404
405 stderr is always redirected to stdout.
406 """
407 assert print_stdout or filter_fn
408 stdout = stdout or sys.stdout
409 filter_fn = filter_fn or (lambda x: None)
410 assert not 'stderr' in kwargs
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000411 kid = Popen(args, bufsize=0,
412 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
413 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000414
maruel@chromium.org17d01792010-09-01 18:07:10 +0000415 # Do a flush of stdout before we begin reading from the subprocess's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000416 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000417
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000418 # Also, we need to forward stdout to prevent weird re-ordering of output.
419 # This has to be done on a per byte basis to make sure it is not buffered:
420 # normally buffering is done for each line, but if svn requests input, no
421 # end-of-line character is output after the prompt and it would not show up.
422 in_byte = kid.stdout.read(1)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000423 if in_byte:
424 if call_filter_on_first_line:
425 filter_fn(None)
426 in_line = ''
427 while in_byte:
428 if in_byte != '\r':
429 if print_stdout:
430 stdout.write(in_byte)
431 if in_byte != '\n':
432 in_line += in_byte
433 else:
434 filter_fn(in_line)
435 in_line = ''
maruel@chromium.org17d01792010-09-01 18:07:10 +0000436 in_byte = kid.stdout.read(1)
437 # Flush the rest of buffered output. This is only an issue with
438 # stdout/stderr not ending with a \n.
439 if len(in_line):
440 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000441 rv = kid.wait()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000442 if rv:
maruel@chromium.org7b194c12010-09-07 20:57:09 +0000443 raise CheckCallError(args, kwargs.get('cwd', None), rv, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000444 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000445
446
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000447def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000448 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000449 real_from_dir = os.path.realpath(from_dir)
450 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000451 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000452 split_path = os.path.split(path)
453 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000454 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000455 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000456
457 # If we did not find the file in the current directory, make sure we are in a
458 # sub directory that is controlled by this configuration.
459 if path != real_from_dir:
460 entries_filename = os.path.join(path, filename + '_entries')
461 if not os.path.exists(entries_filename):
462 # If .gclient_entries does not exist, a previous call to gclient sync
463 # might have failed. In that case, we cannot verify that the .gclient
464 # is the one we want to use. In order to not to cause too much trouble,
465 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000466 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000467 "file you want to use" % (filename, path))
468 return path
469 scope = {}
470 try:
471 exec(FileRead(entries_filename), scope)
472 except SyntaxError, e:
473 SyntaxErrorToError(filename, e)
474 all_directories = scope['entries'].keys()
475 path_to_check = real_from_dir[len(path)+1:]
476 while path_to_check:
477 if path_to_check in all_directories:
478 return path
479 path_to_check = os.path.dirname(path_to_check)
480 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000481
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000482 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000483 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000484
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000485
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000486def PathDifference(root, subpath):
487 """Returns the difference subpath minus root."""
488 root = os.path.realpath(root)
489 subpath = os.path.realpath(subpath)
490 if not subpath.startswith(root):
491 return None
492 # If the root does not have a trailing \ or /, we add it so the returned
493 # path starts immediately after the seperator regardless of whether it is
494 # provided.
495 root = os.path.join(root, '')
496 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000497
498
499def FindFileUpwards(filename, path=None):
500 """Search upwards from the a directory (default: current) to find a file."""
501 if not path:
502 path = os.getcwd()
503 path = os.path.realpath(path)
504 while True:
505 file_path = os.path.join(path, filename)
506 if os.path.isfile(file_path):
507 return file_path
508 (new_path, _) = os.path.split(path)
509 if new_path == path:
510 return None
511 path = new_path
512
513
514def GetGClientRootAndEntries(path=None):
515 """Returns the gclient root and the dict of entries."""
516 config_file = '.gclient_entries'
517 config_path = FindFileUpwards(config_file, path)
518
519 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000520 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000521 return None
522
523 env = {}
524 execfile(config_path, env)
525 config_dir = os.path.dirname(config_path)
526 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000527
528
529class WorkItem(object):
530 """One work item."""
531 # A list of string, each being a WorkItem name.
532 requirements = []
533 # A unique string representing this work item.
534 name = None
535
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000536 def run(self, work_queue):
537 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000538 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000539 pass
540
541
542class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000543 """Runs a set of WorkItem that have interdependencies and were WorkItem are
544 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000545
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000546 In gclient's case, Dependencies sometime needs to be run out of order due to
547 From() keyword. This class manages that all the required dependencies are run
548 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000549
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000550 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000551 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000552 def __init__(self, jobs, progress):
553 """jobs specifies the number of concurrent tasks to allow. progress is a
554 Progress instance."""
maruel@chromium.org58ef2972011-04-01 21:00:11 +0000555 hack_subprocess()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000556 # Set when a thread is done or a new item is enqueued.
557 self.ready_cond = threading.Condition()
558 # Maximum number of concurrent tasks.
559 self.jobs = jobs
560 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000561 self.queued = []
562 # List of strings representing each Dependency.name that was run.
563 self.ran = []
564 # List of items currently running.
565 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000566 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000567 self.exceptions = Queue.Queue()
568 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000569 self.progress = progress
570 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000571 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000572
573 def enqueue(self, d):
574 """Enqueue one Dependency to be executed later once its requirements are
575 satisfied.
576 """
577 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000578 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000579 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000580 self.queued.append(d)
581 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000582 logging.debug('enqueued(%s)' % d.name)
583 if self.progress:
584 self.progress._total = total + 1
585 self.progress.update(0)
586 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000587 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000588 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000589
590 def flush(self, *args, **kwargs):
591 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000592 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000593 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000594 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000595 while True:
596 # Check for task to run first, then wait.
597 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000598 if not self.exceptions.empty():
599 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000600 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000601 self._flush_terminated_threads()
602 if (not self.queued and not self.running or
603 self.jobs == len(self.running)):
604 # No more worker threads or can't queue anything.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000605 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000606
607 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000608 for i in xrange(len(self.queued)):
609 # Verify its requirements.
610 for r in self.queued[i].requirements:
611 if not r in self.ran:
612 # Requirement not met.
613 break
614 else:
615 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000616 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000617 break
618 else:
619 # Couldn't find an item that could run. Break out the outher loop.
620 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000621
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000622 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000623 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000624 break
625 # We need to poll here otherwise Ctrl-C isn't processed.
626 self.ready_cond.wait(10)
627 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000628 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000629 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000630
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000631 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000632 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000633 # To get back the stack location correctly, the raise a, b, c form must be
634 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000635 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000636 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000637 if self.progress:
638 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000639
maruel@chromium.org3742c842010-09-09 19:27:14 +0000640 def _flush_terminated_threads(self):
641 """Flush threads that have terminated."""
642 running = self.running
643 self.running = []
644 for t in running:
645 if t.isAlive():
646 self.running.append(t)
647 else:
648 t.join()
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000649 sys.stdout.full_flush() # pylint: disable=E1101
maruel@chromium.org3742c842010-09-09 19:27:14 +0000650 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000651 self.progress.update(1, t.item.name)
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000652 assert not t.item.name in self.ran
653 if not t.item.name in self.ran:
654 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000655
656 def _run_one_task(self, task_item, args, kwargs):
657 if self.jobs > 1:
658 # Start the thread.
659 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000660 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000661 self.running.append(new_thread)
662 new_thread.start()
663 else:
664 # Run the 'thread' inside the main thread. Don't try to catch any
665 # exception.
666 task_item.run(*args, **kwargs)
667 self.ran.append(task_item.name)
668 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000669 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000670
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000671 class _Worker(threading.Thread):
672 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000673 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000674 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org3742c842010-09-09 19:27:14 +0000675 logging.info(item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000676 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000677 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000678 self.args = args
679 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000680
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000681 def run(self):
682 """Runs in its own thread."""
683 logging.debug('running(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000684 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000685 try:
686 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000687 except Exception:
688 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000689 logging.info('Caught exception in thread %s' % self.item.name)
690 logging.info(str(sys.exc_info()))
691 work_queue.exceptions.put(sys.exc_info())
692 logging.info('Task %s done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000693
maruel@chromium.org3742c842010-09-09 19:27:14 +0000694 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000695 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000696 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000697 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000698 work_queue.ready_cond.release()