blob: aeaebc271d19ee6cb9d6c5a28f5e2fbfd8e9ef49 [file] [log] [blame]
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00001# Copyright 2009 Google Inc. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000015"""Generic utils."""
16
maruel@chromium.org3742c842010-09-09 19:27:14 +000017import copy
maruel@chromium.org167b9e62009-09-17 17:41:02 +000018import errno
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000019import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000020import os
maruel@chromium.org3742c842010-09-09 19:27:14 +000021import Queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000022import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000023import stat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000024import subprocess
25import sys
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000026import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000027import time
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000028import xml.dom.minidom
maruel@chromium.org167b9e62009-09-17 17:41:02 +000029import xml.parsers.expat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000030
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000031
maruel@chromium.org66c83e62010-09-07 14:18:45 +000032class Error(Exception):
33 """gclient exception class."""
34 pass
35
36
37class CheckCallError(OSError, Error):
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000038 """CheckCall() returned non-0."""
maruel@chromium.org66c83e62010-09-07 14:18:45 +000039 def __init__(self, command, cwd, returncode, stdout, stderr=None):
40 OSError.__init__(self, command, cwd, returncode, stdout, stderr)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +000041 Error.__init__(self, command)
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000042 self.command = command
43 self.cwd = cwd
maruel@chromium.org66c83e62010-09-07 14:18:45 +000044 self.returncode = returncode
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000045 self.stdout = stdout
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000046 self.stderr = stderr
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000047
maruel@chromium.org7b194c12010-09-07 20:57:09 +000048 def __str__(self):
49 out = ' '.join(self.command)
50 if self.cwd:
51 out += ' in ' + self.cwd
52 if self.returncode is not None:
53 out += ' returned %d' % self.returncode
54 if self.stdout is not None:
55 out += '\nstdout: %s\n' % self.stdout
56 if self.stderr is not None:
57 out += '\nstderr: %s\n' % self.stderr
58 return out
59
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000060
maruel@chromium.orga1693be2010-09-03 19:09:35 +000061def Popen(args, **kwargs):
maruel@chromium.org3a292682010-08-23 18:54:55 +000062 """Calls subprocess.Popen() with hacks to work around certain behaviors.
63
64 Ensure English outpout for svn and make it work reliably on Windows.
65 """
maruel@chromium.orga1693be2010-09-03 19:09:35 +000066 logging.debug(u'%s, cwd=%s' % (u' '.join(args), kwargs.get('cwd', '')))
maruel@chromium.org3a292682010-08-23 18:54:55 +000067 if not 'env' in kwargs:
maruel@chromium.org3a292682010-08-23 18:54:55 +000068 # It's easier to parse the stdout if it is always in English.
69 kwargs['env'] = os.environ.copy()
70 kwargs['env']['LANGUAGE'] = 'en'
71 if not 'shell' in kwargs:
maruel@chromium.org3a292682010-08-23 18:54:55 +000072 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
73 # executable, but shell=True makes subprocess on Linux fail when it's called
74 # with a list because it only tries to execute the first item in the list.
75 kwargs['shell'] = (sys.platform=='win32')
maruel@chromium.org8aba5f72010-09-16 19:48:59 +000076 try:
77 return subprocess.Popen(args, **kwargs)
78 except OSError, e:
79 if e.errno == errno.EAGAIN and sys.platform == 'cygwin':
80 raise Error(
81 'Visit '
82 'http://code.google.com/p/chromium/wiki/CygwinDllRemappingFailure to '
83 'learn how to fix this error; you need to rebase your cygwin dlls')
84 raise
maruel@chromium.org3a292682010-08-23 18:54:55 +000085
86
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000087def CheckCall(command, cwd=None, print_error=True):
maruel@chromium.org3a292682010-08-23 18:54:55 +000088 """Similar subprocess.check_call() but redirects stdout and
89 returns (stdout, stderr).
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000090
91 Works on python 2.4
92 """
maruel@chromium.org18111352009-12-20 17:21:28 +000093 try:
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000094 stderr = None
95 if not print_error:
96 stderr = subprocess.PIPE
maruel@chromium.org3a292682010-08-23 18:54:55 +000097 process = Popen(command, cwd=cwd, stdout=subprocess.PIPE, stderr=stderr)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000098 std_out, std_err = process.communicate()
maruel@chromium.org18111352009-12-20 17:21:28 +000099 except OSError, e:
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000100 raise CheckCallError(command, cwd, e.errno, None)
maruel@chromium.org18111352009-12-20 17:21:28 +0000101 if process.returncode:
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000102 raise CheckCallError(command, cwd, process.returncode, std_out, std_err)
103 return std_out, std_err
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +0000104
105
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000106def SplitUrlRevision(url):
107 """Splits url and returns a two-tuple: url, rev"""
108 if url.startswith('ssh:'):
109 # Make sure ssh://test@example.com/test.git@stable works
maruel@chromium.org116704f2010-06-11 17:34:38 +0000110 regex = r'(ssh://(?:[\w]+@)?[-\w:\.]+/[-\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000111 components = re.search(regex, url).groups()
112 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000113 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000114 if len(components) == 1:
115 components += [None]
116 return tuple(components)
117
118
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000119def ParseXML(output):
120 try:
121 return xml.dom.minidom.parseString(output)
122 except xml.parsers.expat.ExpatError:
123 return None
124
125
126def GetNamedNodeText(node, node_name):
127 child_nodes = node.getElementsByTagName(node_name)
128 if not child_nodes:
129 return None
130 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
131 return child_nodes[0].firstChild.nodeValue
132
133
134def GetNodeNamedAttributeText(node, node_name, attribute_name):
135 child_nodes = node.getElementsByTagName(node_name)
136 if not child_nodes:
137 return None
138 assert len(child_nodes) == 1
139 return child_nodes[0].getAttribute(attribute_name)
140
141
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000142def SyntaxErrorToError(filename, e):
143 """Raises a gclient_utils.Error exception with the human readable message"""
144 try:
145 # Try to construct a human readable error message
146 if filename:
147 error_message = 'There is a syntax error in %s\n' % filename
148 else:
149 error_message = 'There is a syntax error\n'
150 error_message += 'Line #%s, character %s: "%s"' % (
151 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
152 except:
153 # Something went wrong, re-raise the original exception
154 raise e
155 else:
156 raise Error(error_message)
157
158
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000159class PrintableObject(object):
160 def __str__(self):
161 output = ''
162 for i in dir(self):
163 if i.startswith('__'):
164 continue
165 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
166 return output
167
168
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000169def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000170 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000171 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000172 try:
173 content = f.read()
174 finally:
175 f.close()
176 return content
177
178
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000179def FileWrite(filename, content, mode='w'):
180 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000181 try:
182 f.write(content)
183 finally:
184 f.close()
185
186
187def RemoveDirectory(*path):
188 """Recursively removes a directory, even if it's marked read-only.
189
190 Remove the directory located at *path, if it exists.
191
192 shutil.rmtree() doesn't work on Windows if any of the files or directories
193 are read-only, which svn repositories and some .svn files are. We need to
194 be able to force the files to be writable (i.e., deletable) as we traverse
195 the tree.
196
197 Even with all this, Windows still sometimes fails to delete a file, citing
198 a permission error (maybe something to do with antivirus scans or disk
199 indexing). The best suggestion any of the user forums had was to wait a
200 bit and try again, so we do that too. It's hand-waving, but sometimes it
201 works. :/
202
203 On POSIX systems, things are a little bit simpler. The modes of the files
204 to be deleted doesn't matter, only the modes of the directories containing
205 them are significant. As the directory tree is traversed, each directory
206 has its mode set appropriately before descending into it. This should
207 result in the entire tree being removed, with the possible exception of
208 *path itself, because nothing attempts to change the mode of its parent.
209 Doing so would be hazardous, as it's not a directory slated for removal.
210 In the ordinary case, this is not a problem: for our purposes, the user
211 will never lack write permission on *path's parent.
212 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000213 logging.debug(path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000214 file_path = os.path.join(*path)
215 if not os.path.exists(file_path):
216 return
217
218 if os.path.islink(file_path) or not os.path.isdir(file_path):
maruel@chromium.org116704f2010-06-11 17:34:38 +0000219 raise Error('RemoveDirectory asked to remove non-directory %s' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000220
221 has_win32api = False
222 if sys.platform == 'win32':
223 has_win32api = True
224 # Some people don't have the APIs installed. In that case we'll do without.
225 try:
226 win32api = __import__('win32api')
227 win32con = __import__('win32con')
228 except ImportError:
229 has_win32api = False
230 else:
231 # On POSIX systems, we need the x-bit set on the directory to access it,
232 # the r-bit to see its contents, and the w-bit to remove files from it.
233 # The actual modes of the files within the directory is irrelevant.
234 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
235 for fn in os.listdir(file_path):
236 fullpath = os.path.join(file_path, fn)
237
238 # If fullpath is a symbolic link that points to a directory, isdir will
239 # be True, but we don't want to descend into that as a directory, we just
240 # want to remove the link. Check islink and treat links as ordinary files
241 # would be treated regardless of what they reference.
242 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
243 if sys.platform == 'win32':
244 os.chmod(fullpath, stat.S_IWRITE)
245 if has_win32api:
246 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
247 try:
248 os.remove(fullpath)
249 except OSError, e:
250 if e.errno != errno.EACCES or sys.platform != 'win32':
251 raise
252 print 'Failed to delete %s: trying again' % fullpath
253 time.sleep(0.1)
254 os.remove(fullpath)
255 else:
256 RemoveDirectory(fullpath)
257
258 if sys.platform == 'win32':
259 os.chmod(file_path, stat.S_IWRITE)
260 if has_win32api:
261 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
262 try:
263 os.rmdir(file_path)
264 except OSError, e:
265 if e.errno != errno.EACCES or sys.platform != 'win32':
266 raise
267 print 'Failed to remove %s: trying again' % file_path
268 time.sleep(0.1)
269 os.rmdir(file_path)
270
271
maruel@chromium.org17d01792010-09-01 18:07:10 +0000272def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
273 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000274
maruel@chromium.org17d01792010-09-01 18:07:10 +0000275 If |always| is True, a message indicating what is being done
276 is printed to stdout all the time even if not output is generated. Otherwise
277 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000278 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000279 stdout = kwargs.get('stdout', None) or sys.stdout
280 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000281 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000282 % (' '.join(args), kwargs.get('cwd', '.')))
283 else:
284 filter_fn = kwargs.get('filter_fn', None)
285 def filter_msg(line):
286 if line is None:
287 stdout.write('\n________ running \'%s\' in \'%s\'\n'
288 % (' '.join(args), kwargs.get('cwd', '.')))
289 elif filter_fn:
290 filter_fn(line)
291 kwargs['filter_fn'] = filter_msg
292 kwargs['call_filter_on_first_line'] = True
293 # Obviously.
294 kwargs['print_stdout'] = True
295 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000296
maruel@chromium.org17d01792010-09-01 18:07:10 +0000297
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000298def SoftClone(obj):
299 """Clones an object. copy.copy() doesn't work on 'file' objects."""
300 class NewObject(object): pass
301 new_obj = NewObject()
302 for member in dir(obj):
303 if member.startswith('_'):
304 continue
305 setattr(new_obj, member, getattr(obj, member))
306 return new_obj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000307
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000308
309def MakeFileAutoFlush(fileobj, delay=10):
310 """Creates a file object clone to automatically flush after N seconds."""
311 if hasattr(fileobj, 'last_flushed_at'):
312 # Already patched. Just update delay.
313 fileobj.delay = delay
314 return fileobj
315
316 new_fileobj = SoftClone(fileobj)
317 new_fileobj.lock = threading.Lock()
318 new_fileobj.last_flushed_at = time.time()
319 new_fileobj.delay = delay
320 new_fileobj.old_auto_flush_write = fileobj.write
321 # Silence pylint.
322 new_fileobj.flush = fileobj.flush
323
324 def auto_flush_write(out):
325 new_fileobj.old_auto_flush_write(out)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000326 should_flush = False
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000327 new_fileobj.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000328 try:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000329 if (new_fileobj.delay and
330 (time.time() - new_fileobj.last_flushed_at) > new_fileobj.delay):
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000331 should_flush = True
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000332 new_fileobj.last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000333 finally:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000334 new_fileobj.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000335 if should_flush:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000336 new_fileobj.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000337
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000338 new_fileobj.write = auto_flush_write
339 return new_fileobj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000340
341
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000342class StdoutAnnotated(object):
343 """Prepends every line with a string."""
344 def __init__(self, prepend, stdout):
345 self.prepend = prepend
346 self.buf = ''
347 self.stdout = stdout
348
349 def write(self, out):
350 self.buf += out
351 while '\n' in self.buf:
352 line, self.buf = self.buf.split('\n', 1)
353 self.stdout.write(self.prepend + line + '\n')
354
355 def flush(self):
356 pass
357
358 def full_flush(self):
359 if self.buf:
360 self.stdout.write(self.prepend + self.buf)
361 self.stdout.flush()
362 self.buf = ''
363
364
maruel@chromium.org17d01792010-09-01 18:07:10 +0000365def CheckCallAndFilter(args, stdout=None, filter_fn=None,
366 print_stdout=None, call_filter_on_first_line=False,
367 **kwargs):
368 """Runs a command and calls back a filter function if needed.
369
370 Accepts all subprocess.Popen() parameters plus:
371 print_stdout: If True, the command's stdout is forwarded to stdout.
372 filter_fn: A function taking a single string argument called with each line
373 of the subprocess's output. Each line has the trailing newline
374 character trimmed.
375 stdout: Can be any bufferable output.
376
377 stderr is always redirected to stdout.
378 """
379 assert print_stdout or filter_fn
380 stdout = stdout or sys.stdout
381 filter_fn = filter_fn or (lambda x: None)
382 assert not 'stderr' in kwargs
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000383 kid = Popen(args, bufsize=0,
384 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
385 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000386
maruel@chromium.org17d01792010-09-01 18:07:10 +0000387 # Do a flush of stdout before we begin reading from the subprocess's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000388 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000389
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000390 # Also, we need to forward stdout to prevent weird re-ordering of output.
391 # This has to be done on a per byte basis to make sure it is not buffered:
392 # normally buffering is done for each line, but if svn requests input, no
393 # end-of-line character is output after the prompt and it would not show up.
394 in_byte = kid.stdout.read(1)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000395 if in_byte:
396 if call_filter_on_first_line:
397 filter_fn(None)
398 in_line = ''
399 while in_byte:
400 if in_byte != '\r':
401 if print_stdout:
402 stdout.write(in_byte)
403 if in_byte != '\n':
404 in_line += in_byte
405 else:
406 filter_fn(in_line)
407 in_line = ''
maruel@chromium.org17d01792010-09-01 18:07:10 +0000408 in_byte = kid.stdout.read(1)
409 # Flush the rest of buffered output. This is only an issue with
410 # stdout/stderr not ending with a \n.
411 if len(in_line):
412 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000413 rv = kid.wait()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000414 if rv:
maruel@chromium.org7b194c12010-09-07 20:57:09 +0000415 raise CheckCallError(args, kwargs.get('cwd', None), rv, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000416 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000417
418
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000419def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000420 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000421 real_from_dir = os.path.realpath(from_dir)
422 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000423 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000424 split_path = os.path.split(path)
425 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000426 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000427 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000428
429 # If we did not find the file in the current directory, make sure we are in a
430 # sub directory that is controlled by this configuration.
431 if path != real_from_dir:
432 entries_filename = os.path.join(path, filename + '_entries')
433 if not os.path.exists(entries_filename):
434 # If .gclient_entries does not exist, a previous call to gclient sync
435 # might have failed. In that case, we cannot verify that the .gclient
436 # is the one we want to use. In order to not to cause too much trouble,
437 # just issue a warning and return the path anyway.
438 print >>sys.stderr, ("%s file in parent directory %s might not be the "
439 "file you want to use" % (filename, path))
440 return path
441 scope = {}
442 try:
443 exec(FileRead(entries_filename), scope)
444 except SyntaxError, e:
445 SyntaxErrorToError(filename, e)
446 all_directories = scope['entries'].keys()
447 path_to_check = real_from_dir[len(path)+1:]
448 while path_to_check:
449 if path_to_check in all_directories:
450 return path
451 path_to_check = os.path.dirname(path_to_check)
452 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000453
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000454 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000455 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000456
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000457
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000458def PathDifference(root, subpath):
459 """Returns the difference subpath minus root."""
460 root = os.path.realpath(root)
461 subpath = os.path.realpath(subpath)
462 if not subpath.startswith(root):
463 return None
464 # If the root does not have a trailing \ or /, we add it so the returned
465 # path starts immediately after the seperator regardless of whether it is
466 # provided.
467 root = os.path.join(root, '')
468 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000469
470
471def FindFileUpwards(filename, path=None):
472 """Search upwards from the a directory (default: current) to find a file."""
473 if not path:
474 path = os.getcwd()
475 path = os.path.realpath(path)
476 while True:
477 file_path = os.path.join(path, filename)
478 if os.path.isfile(file_path):
479 return file_path
480 (new_path, _) = os.path.split(path)
481 if new_path == path:
482 return None
483 path = new_path
484
485
486def GetGClientRootAndEntries(path=None):
487 """Returns the gclient root and the dict of entries."""
488 config_file = '.gclient_entries'
489 config_path = FindFileUpwards(config_file, path)
490
491 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000492 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000493 return None
494
495 env = {}
496 execfile(config_path, env)
497 config_dir = os.path.dirname(config_path)
498 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000499
500
501class WorkItem(object):
502 """One work item."""
503 # A list of string, each being a WorkItem name.
504 requirements = []
505 # A unique string representing this work item.
506 name = None
507
maruel@chromium.org3742c842010-09-09 19:27:14 +0000508 def run(self, work_queue, options):
509 """work_queue and options are passed as keyword arguments so they should be
510 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000511 pass
512
513
514class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000515 """Runs a set of WorkItem that have interdependencies and were WorkItem are
516 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000517
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000518 In gclient's case, Dependencies sometime needs to be run out of order due to
519 From() keyword. This class manages that all the required dependencies are run
520 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000521
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000522 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000523 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000524 def __init__(self, jobs, progress):
525 """jobs specifies the number of concurrent tasks to allow. progress is a
526 Progress instance."""
527 # Set when a thread is done or a new item is enqueued.
528 self.ready_cond = threading.Condition()
529 # Maximum number of concurrent tasks.
530 self.jobs = jobs
531 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000532 self.queued = []
533 # List of strings representing each Dependency.name that was run.
534 self.ran = []
535 # List of items currently running.
536 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000537 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000538 self.exceptions = Queue.Queue()
539 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000540 self.progress = progress
541 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000542 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000543
544 def enqueue(self, d):
545 """Enqueue one Dependency to be executed later once its requirements are
546 satisfied.
547 """
548 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000549 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000550 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000551 self.queued.append(d)
552 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000553 logging.debug('enqueued(%s)' % d.name)
554 if self.progress:
555 self.progress._total = total + 1
556 self.progress.update(0)
557 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000558 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000559 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000560
561 def flush(self, *args, **kwargs):
562 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000563 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000564 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000565 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000566 while True:
567 # Check for task to run first, then wait.
568 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000569 if not self.exceptions.empty():
570 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000571 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000572 self._flush_terminated_threads()
573 if (not self.queued and not self.running or
574 self.jobs == len(self.running)):
575 # No more worker threads or can't queue anything.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000576 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000577
578 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000579 for i in xrange(len(self.queued)):
580 # Verify its requirements.
581 for r in self.queued[i].requirements:
582 if not r in self.ran:
583 # Requirement not met.
584 break
585 else:
586 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000587 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000588 break
589 else:
590 # Couldn't find an item that could run. Break out the outher loop.
591 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000592
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000593 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000594 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000595 break
596 # We need to poll here otherwise Ctrl-C isn't processed.
597 self.ready_cond.wait(10)
598 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000599 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000600 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000601
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000602 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000603 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000604 # To get back the stack location correctly, the raise a, b, c form must be
605 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000606 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000607 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000608 if self.progress:
609 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000610
maruel@chromium.org3742c842010-09-09 19:27:14 +0000611 def _flush_terminated_threads(self):
612 """Flush threads that have terminated."""
613 running = self.running
614 self.running = []
615 for t in running:
616 if t.isAlive():
617 self.running.append(t)
618 else:
619 t.join()
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000620 t.kwargs['options'].stdout.full_flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000621 if self.progress:
622 self.progress.update(1)
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000623 assert not t.item.name in self.ran
624 if not t.item.name in self.ran:
625 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000626
627 def _run_one_task(self, task_item, args, kwargs):
628 if self.jobs > 1:
629 # Start the thread.
630 index = len(self.ran) + len(self.running) + 1
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000631 # Copy 'options' and add annotated stdout.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000632 task_kwargs = kwargs.copy()
633 task_kwargs['options'] = copy.copy(task_kwargs['options'])
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000634 task_kwargs['options'].stdout = StdoutAnnotated(
635 '%d>' % index, task_kwargs['options'].stdout)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000636 new_thread = self._Worker(task_item, args, task_kwargs)
637 self.running.append(new_thread)
638 new_thread.start()
639 else:
640 # Run the 'thread' inside the main thread. Don't try to catch any
641 # exception.
642 task_item.run(*args, **kwargs)
643 self.ran.append(task_item.name)
644 if self.progress:
645 self.progress.update(1)
646
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000647 class _Worker(threading.Thread):
648 """One thread to execute one WorkItem."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000649 def __init__(self, item, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000650 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org3742c842010-09-09 19:27:14 +0000651 logging.info(item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000652 self.item = item
maruel@chromium.org3742c842010-09-09 19:27:14 +0000653 self.args = args
654 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000655
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000656 def run(self):
657 """Runs in its own thread."""
658 logging.debug('running(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000659 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000660 try:
661 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000662 except Exception:
663 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000664 logging.info('Caught exception in thread %s' % self.item.name)
665 logging.info(str(sys.exc_info()))
666 work_queue.exceptions.put(sys.exc_info())
667 logging.info('Task %s done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000668
maruel@chromium.org3742c842010-09-09 19:27:14 +0000669 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000670 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000671 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000672 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000673 work_queue.ready_cond.release()