blob: 1f4fdf22c5a5899023dfcad48a200b1204bade2f [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)
nsylvain@google.come939bb52011-06-01 22:59:15 +0000365 if line:
366 new_fileobj.old_annotated_write('%d>%s\n' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000367 obj[0] = remaining
368
369 def full_flush():
370 """Flush buffered output."""
371 orphans = []
372 new_fileobj.lock.acquire()
373 try:
374 # Detect threads no longer existing.
375 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000376 indexes = filter(None, indexes)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000377 for index in new_fileobj.output_buffers:
378 if not index in indexes:
379 orphans.append((index, new_fileobj.output_buffers[index][0]))
380 for orphan in orphans:
381 del new_fileobj.output_buffers[orphan[0]]
382 finally:
383 new_fileobj.lock.release()
384
385 # Don't keep the lock while writting. Will append \n when it shouldn't.
386 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000387 if orphan[1]:
388 new_fileobj.old_annotated_write('%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000389
390 new_fileobj.write = annotated_write
391 new_fileobj.full_flush = full_flush
392 return new_fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000393
394
maruel@chromium.org17d01792010-09-01 18:07:10 +0000395def CheckCallAndFilter(args, stdout=None, filter_fn=None,
396 print_stdout=None, call_filter_on_first_line=False,
397 **kwargs):
398 """Runs a command and calls back a filter function if needed.
399
400 Accepts all subprocess.Popen() parameters plus:
401 print_stdout: If True, the command's stdout is forwarded to stdout.
402 filter_fn: A function taking a single string argument called with each line
403 of the subprocess's output. Each line has the trailing newline
404 character trimmed.
405 stdout: Can be any bufferable output.
406
407 stderr is always redirected to stdout.
408 """
409 assert print_stdout or filter_fn
410 stdout = stdout or sys.stdout
411 filter_fn = filter_fn or (lambda x: None)
412 assert not 'stderr' in kwargs
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000413 kid = Popen(args, bufsize=0,
414 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
415 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000416
maruel@chromium.org17d01792010-09-01 18:07:10 +0000417 # Do a flush of stdout before we begin reading from the subprocess's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000418 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000419
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000420 # Also, we need to forward stdout to prevent weird re-ordering of output.
421 # This has to be done on a per byte basis to make sure it is not buffered:
422 # normally buffering is done for each line, but if svn requests input, no
423 # end-of-line character is output after the prompt and it would not show up.
424 in_byte = kid.stdout.read(1)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000425 if in_byte:
426 if call_filter_on_first_line:
427 filter_fn(None)
428 in_line = ''
429 while in_byte:
430 if in_byte != '\r':
431 if print_stdout:
432 stdout.write(in_byte)
433 if in_byte != '\n':
434 in_line += in_byte
435 else:
436 filter_fn(in_line)
437 in_line = ''
maruel@chromium.org17d01792010-09-01 18:07:10 +0000438 in_byte = kid.stdout.read(1)
439 # Flush the rest of buffered output. This is only an issue with
440 # stdout/stderr not ending with a \n.
441 if len(in_line):
442 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000443 rv = kid.wait()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000444 if rv:
maruel@chromium.org7b194c12010-09-07 20:57:09 +0000445 raise CheckCallError(args, kwargs.get('cwd', None), rv, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000446 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000447
448
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000449def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000450 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000451 real_from_dir = os.path.realpath(from_dir)
452 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000453 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000454 split_path = os.path.split(path)
455 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000456 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000457 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000458
459 # If we did not find the file in the current directory, make sure we are in a
460 # sub directory that is controlled by this configuration.
461 if path != real_from_dir:
462 entries_filename = os.path.join(path, filename + '_entries')
463 if not os.path.exists(entries_filename):
464 # If .gclient_entries does not exist, a previous call to gclient sync
465 # might have failed. In that case, we cannot verify that the .gclient
466 # is the one we want to use. In order to not to cause too much trouble,
467 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000468 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000469 "file you want to use" % (filename, path))
470 return path
471 scope = {}
472 try:
473 exec(FileRead(entries_filename), scope)
474 except SyntaxError, e:
475 SyntaxErrorToError(filename, e)
476 all_directories = scope['entries'].keys()
477 path_to_check = real_from_dir[len(path)+1:]
478 while path_to_check:
479 if path_to_check in all_directories:
480 return path
481 path_to_check = os.path.dirname(path_to_check)
482 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000483
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000484 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000485 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000486
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000487
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000488def PathDifference(root, subpath):
489 """Returns the difference subpath minus root."""
490 root = os.path.realpath(root)
491 subpath = os.path.realpath(subpath)
492 if not subpath.startswith(root):
493 return None
494 # If the root does not have a trailing \ or /, we add it so the returned
495 # path starts immediately after the seperator regardless of whether it is
496 # provided.
497 root = os.path.join(root, '')
498 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000499
500
501def FindFileUpwards(filename, path=None):
502 """Search upwards from the a directory (default: current) to find a file."""
503 if not path:
504 path = os.getcwd()
505 path = os.path.realpath(path)
506 while True:
507 file_path = os.path.join(path, filename)
508 if os.path.isfile(file_path):
509 return file_path
510 (new_path, _) = os.path.split(path)
511 if new_path == path:
512 return None
513 path = new_path
514
515
516def GetGClientRootAndEntries(path=None):
517 """Returns the gclient root and the dict of entries."""
518 config_file = '.gclient_entries'
519 config_path = FindFileUpwards(config_file, path)
520
521 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000522 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000523 return None
524
525 env = {}
526 execfile(config_path, env)
527 config_dir = os.path.dirname(config_path)
528 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000529
530
531class WorkItem(object):
532 """One work item."""
533 # A list of string, each being a WorkItem name.
534 requirements = []
535 # A unique string representing this work item.
536 name = None
537
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000538 def run(self, work_queue):
539 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000540 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000541 pass
542
543
544class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000545 """Runs a set of WorkItem that have interdependencies and were WorkItem are
546 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000547
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000548 In gclient's case, Dependencies sometime needs to be run out of order due to
549 From() keyword. This class manages that all the required dependencies are run
550 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000551
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000552 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000553 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000554 def __init__(self, jobs, progress):
555 """jobs specifies the number of concurrent tasks to allow. progress is a
556 Progress instance."""
maruel@chromium.org58ef2972011-04-01 21:00:11 +0000557 hack_subprocess()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000558 # Set when a thread is done or a new item is enqueued.
559 self.ready_cond = threading.Condition()
560 # Maximum number of concurrent tasks.
561 self.jobs = jobs
562 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000563 self.queued = []
564 # List of strings representing each Dependency.name that was run.
565 self.ran = []
566 # List of items currently running.
567 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000568 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000569 self.exceptions = Queue.Queue()
570 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000571 self.progress = progress
572 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000573 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000574
575 def enqueue(self, d):
576 """Enqueue one Dependency to be executed later once its requirements are
577 satisfied.
578 """
579 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000580 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000581 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000582 self.queued.append(d)
583 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000584 logging.debug('enqueued(%s)' % d.name)
585 if self.progress:
586 self.progress._total = total + 1
587 self.progress.update(0)
588 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000589 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000590 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000591
592 def flush(self, *args, **kwargs):
593 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000594 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000595 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000596 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000597 while True:
598 # Check for task to run first, then wait.
599 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000600 if not self.exceptions.empty():
601 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000602 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000603 self._flush_terminated_threads()
604 if (not self.queued and not self.running or
605 self.jobs == len(self.running)):
606 # No more worker threads or can't queue anything.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000607 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000608
609 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000610 for i in xrange(len(self.queued)):
611 # Verify its requirements.
612 for r in self.queued[i].requirements:
613 if not r in self.ran:
614 # Requirement not met.
615 break
616 else:
617 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000618 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000619 break
620 else:
621 # Couldn't find an item that could run. Break out the outher loop.
622 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000623
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000624 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000625 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000626 break
627 # We need to poll here otherwise Ctrl-C isn't processed.
628 self.ready_cond.wait(10)
629 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000630 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000631 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000632
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000633 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000634 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000635 # To get back the stack location correctly, the raise a, b, c form must be
636 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000637 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000638 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000639 if self.progress:
640 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000641
maruel@chromium.org3742c842010-09-09 19:27:14 +0000642 def _flush_terminated_threads(self):
643 """Flush threads that have terminated."""
644 running = self.running
645 self.running = []
646 for t in running:
647 if t.isAlive():
648 self.running.append(t)
649 else:
650 t.join()
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000651 sys.stdout.full_flush() # pylint: disable=E1101
maruel@chromium.org3742c842010-09-09 19:27:14 +0000652 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000653 self.progress.update(1, t.item.name)
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000654 assert not t.item.name in self.ran
655 if not t.item.name in self.ran:
656 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000657
658 def _run_one_task(self, task_item, args, kwargs):
659 if self.jobs > 1:
660 # Start the thread.
661 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000662 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000663 self.running.append(new_thread)
664 new_thread.start()
665 else:
666 # Run the 'thread' inside the main thread. Don't try to catch any
667 # exception.
668 task_item.run(*args, **kwargs)
669 self.ran.append(task_item.name)
670 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000671 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000672
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000673 class _Worker(threading.Thread):
674 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000675 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000676 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org3742c842010-09-09 19:27:14 +0000677 logging.info(item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000678 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000679 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000680 self.args = args
681 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000682
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000683 def run(self):
684 """Runs in its own thread."""
685 logging.debug('running(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000686 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000687 try:
688 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000689 except Exception:
690 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000691 logging.info('Caught exception in thread %s' % self.item.name)
692 logging.info(str(sys.exc_info()))
693 work_queue.exceptions.put(sys.exc_info())
694 logging.info('Task %s done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000695
maruel@chromium.org3742c842010-09-09 19:27:14 +0000696 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000697 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000698 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000699 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000700 work_queue.ready_cond.release()