blob: d8bbf023ee1a0b6f57bf8d585df07d6545a180da [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.orga1693be2010-09-03 19:09:35 +000076 return subprocess.Popen(args, **kwargs)
maruel@chromium.org3a292682010-08-23 18:54:55 +000077
78
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000079def CheckCall(command, cwd=None, print_error=True):
maruel@chromium.org3a292682010-08-23 18:54:55 +000080 """Similar subprocess.check_call() but redirects stdout and
81 returns (stdout, stderr).
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000082
83 Works on python 2.4
84 """
maruel@chromium.org18111352009-12-20 17:21:28 +000085 try:
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000086 stderr = None
87 if not print_error:
88 stderr = subprocess.PIPE
maruel@chromium.org3a292682010-08-23 18:54:55 +000089 process = Popen(command, cwd=cwd, stdout=subprocess.PIPE, stderr=stderr)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000090 std_out, std_err = process.communicate()
maruel@chromium.org18111352009-12-20 17:21:28 +000091 except OSError, e:
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +000092 raise CheckCallError(command, cwd, e.errno, None)
maruel@chromium.org18111352009-12-20 17:21:28 +000093 if process.returncode:
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000094 raise CheckCallError(command, cwd, process.returncode, std_out, std_err)
95 return std_out, std_err
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000096
97
msb@chromium.orgac915bb2009-11-13 17:03:01 +000098def SplitUrlRevision(url):
99 """Splits url and returns a two-tuple: url, rev"""
100 if url.startswith('ssh:'):
101 # Make sure ssh://test@example.com/test.git@stable works
maruel@chromium.org116704f2010-06-11 17:34:38 +0000102 regex = r'(ssh://(?:[\w]+@)?[-\w:\.]+/[-\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000103 components = re.search(regex, url).groups()
104 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000105 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000106 if len(components) == 1:
107 components += [None]
108 return tuple(components)
109
110
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000111def ParseXML(output):
112 try:
113 return xml.dom.minidom.parseString(output)
114 except xml.parsers.expat.ExpatError:
115 return None
116
117
118def GetNamedNodeText(node, node_name):
119 child_nodes = node.getElementsByTagName(node_name)
120 if not child_nodes:
121 return None
122 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
123 return child_nodes[0].firstChild.nodeValue
124
125
126def GetNodeNamedAttributeText(node, node_name, attribute_name):
127 child_nodes = node.getElementsByTagName(node_name)
128 if not child_nodes:
129 return None
130 assert len(child_nodes) == 1
131 return child_nodes[0].getAttribute(attribute_name)
132
133
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000134def SyntaxErrorToError(filename, e):
135 """Raises a gclient_utils.Error exception with the human readable message"""
136 try:
137 # Try to construct a human readable error message
138 if filename:
139 error_message = 'There is a syntax error in %s\n' % filename
140 else:
141 error_message = 'There is a syntax error\n'
142 error_message += 'Line #%s, character %s: "%s"' % (
143 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
144 except:
145 # Something went wrong, re-raise the original exception
146 raise e
147 else:
148 raise Error(error_message)
149
150
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000151class PrintableObject(object):
152 def __str__(self):
153 output = ''
154 for i in dir(self):
155 if i.startswith('__'):
156 continue
157 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
158 return output
159
160
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000161def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000162 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000163 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000164 try:
165 content = f.read()
166 finally:
167 f.close()
168 return content
169
170
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000171def FileWrite(filename, content, mode='w'):
172 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000173 try:
174 f.write(content)
175 finally:
176 f.close()
177
178
179def RemoveDirectory(*path):
180 """Recursively removes a directory, even if it's marked read-only.
181
182 Remove the directory located at *path, if it exists.
183
184 shutil.rmtree() doesn't work on Windows if any of the files or directories
185 are read-only, which svn repositories and some .svn files are. We need to
186 be able to force the files to be writable (i.e., deletable) as we traverse
187 the tree.
188
189 Even with all this, Windows still sometimes fails to delete a file, citing
190 a permission error (maybe something to do with antivirus scans or disk
191 indexing). The best suggestion any of the user forums had was to wait a
192 bit and try again, so we do that too. It's hand-waving, but sometimes it
193 works. :/
194
195 On POSIX systems, things are a little bit simpler. The modes of the files
196 to be deleted doesn't matter, only the modes of the directories containing
197 them are significant. As the directory tree is traversed, each directory
198 has its mode set appropriately before descending into it. This should
199 result in the entire tree being removed, with the possible exception of
200 *path itself, because nothing attempts to change the mode of its parent.
201 Doing so would be hazardous, as it's not a directory slated for removal.
202 In the ordinary case, this is not a problem: for our purposes, the user
203 will never lack write permission on *path's parent.
204 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000205 logging.debug(path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000206 file_path = os.path.join(*path)
207 if not os.path.exists(file_path):
208 return
209
210 if os.path.islink(file_path) or not os.path.isdir(file_path):
maruel@chromium.org116704f2010-06-11 17:34:38 +0000211 raise Error('RemoveDirectory asked to remove non-directory %s' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000212
213 has_win32api = False
214 if sys.platform == 'win32':
215 has_win32api = True
216 # Some people don't have the APIs installed. In that case we'll do without.
217 try:
218 win32api = __import__('win32api')
219 win32con = __import__('win32con')
220 except ImportError:
221 has_win32api = False
222 else:
223 # On POSIX systems, we need the x-bit set on the directory to access it,
224 # the r-bit to see its contents, and the w-bit to remove files from it.
225 # The actual modes of the files within the directory is irrelevant.
226 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
227 for fn in os.listdir(file_path):
228 fullpath = os.path.join(file_path, fn)
229
230 # If fullpath is a symbolic link that points to a directory, isdir will
231 # be True, but we don't want to descend into that as a directory, we just
232 # want to remove the link. Check islink and treat links as ordinary files
233 # would be treated regardless of what they reference.
234 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
235 if sys.platform == 'win32':
236 os.chmod(fullpath, stat.S_IWRITE)
237 if has_win32api:
238 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
239 try:
240 os.remove(fullpath)
241 except OSError, e:
242 if e.errno != errno.EACCES or sys.platform != 'win32':
243 raise
244 print 'Failed to delete %s: trying again' % fullpath
245 time.sleep(0.1)
246 os.remove(fullpath)
247 else:
248 RemoveDirectory(fullpath)
249
250 if sys.platform == 'win32':
251 os.chmod(file_path, stat.S_IWRITE)
252 if has_win32api:
253 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
254 try:
255 os.rmdir(file_path)
256 except OSError, e:
257 if e.errno != errno.EACCES or sys.platform != 'win32':
258 raise
259 print 'Failed to remove %s: trying again' % file_path
260 time.sleep(0.1)
261 os.rmdir(file_path)
262
263
maruel@chromium.org17d01792010-09-01 18:07:10 +0000264def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
265 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000266
maruel@chromium.org17d01792010-09-01 18:07:10 +0000267 If |always| is True, a message indicating what is being done
268 is printed to stdout all the time even if not output is generated. Otherwise
269 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000270 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000271 stdout = kwargs.get('stdout', None) or sys.stdout
272 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000273 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000274 % (' '.join(args), kwargs.get('cwd', '.')))
275 else:
276 filter_fn = kwargs.get('filter_fn', None)
277 def filter_msg(line):
278 if line is None:
279 stdout.write('\n________ running \'%s\' in \'%s\'\n'
280 % (' '.join(args), kwargs.get('cwd', '.')))
281 elif filter_fn:
282 filter_fn(line)
283 kwargs['filter_fn'] = filter_msg
284 kwargs['call_filter_on_first_line'] = True
285 # Obviously.
286 kwargs['print_stdout'] = True
287 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000288
maruel@chromium.org17d01792010-09-01 18:07:10 +0000289
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000290class StdoutAutoFlush(object):
291 """Automatically flush after N seconds."""
292 def __init__(self, stdout, delay=10):
293 self.lock = threading.Lock()
294 self.stdout = stdout
295 self.delay = delay
296 self.last_flushed_at = time.time()
297 self.stdout.flush()
298
299 def write(self, out):
300 """Thread-safe."""
301 self.stdout.write(out)
302 should_flush = False
maruel@chromium.org9c531262010-09-08 13:41:13 +0000303 self.lock.acquire()
304 try:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000305 if (time.time() - self.last_flushed_at) > self.delay:
306 should_flush = True
307 self.last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000308 finally:
309 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000310 if should_flush:
311 self.stdout.flush()
312
313 def flush(self):
314 self.stdout.flush()
315
316
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000317class StdoutAnnotated(object):
318 """Prepends every line with a string."""
319 def __init__(self, prepend, stdout):
320 self.prepend = prepend
321 self.buf = ''
322 self.stdout = stdout
323
324 def write(self, out):
325 self.buf += out
326 while '\n' in self.buf:
327 line, self.buf = self.buf.split('\n', 1)
328 self.stdout.write(self.prepend + line + '\n')
329
330 def flush(self):
331 pass
332
333 def full_flush(self):
334 if self.buf:
335 self.stdout.write(self.prepend + self.buf)
336 self.stdout.flush()
337 self.buf = ''
338
339
maruel@chromium.org17d01792010-09-01 18:07:10 +0000340def CheckCallAndFilter(args, stdout=None, filter_fn=None,
341 print_stdout=None, call_filter_on_first_line=False,
342 **kwargs):
343 """Runs a command and calls back a filter function if needed.
344
345 Accepts all subprocess.Popen() parameters plus:
346 print_stdout: If True, the command's stdout is forwarded to stdout.
347 filter_fn: A function taking a single string argument called with each line
348 of the subprocess's output. Each line has the trailing newline
349 character trimmed.
350 stdout: Can be any bufferable output.
351
352 stderr is always redirected to stdout.
353 """
354 assert print_stdout or filter_fn
355 stdout = stdout or sys.stdout
356 filter_fn = filter_fn or (lambda x: None)
357 assert not 'stderr' in kwargs
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000358 kid = Popen(args, bufsize=0,
359 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
360 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000361
maruel@chromium.org17d01792010-09-01 18:07:10 +0000362 # Do a flush of stdout before we begin reading from the subprocess's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000363 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000364
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000365 # Also, we need to forward stdout to prevent weird re-ordering of output.
366 # This has to be done on a per byte basis to make sure it is not buffered:
367 # normally buffering is done for each line, but if svn requests input, no
368 # end-of-line character is output after the prompt and it would not show up.
369 in_byte = kid.stdout.read(1)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000370 if in_byte:
371 if call_filter_on_first_line:
372 filter_fn(None)
373 in_line = ''
374 while in_byte:
375 if in_byte != '\r':
376 if print_stdout:
377 stdout.write(in_byte)
378 if in_byte != '\n':
379 in_line += in_byte
380 else:
381 filter_fn(in_line)
382 in_line = ''
maruel@chromium.org17d01792010-09-01 18:07:10 +0000383 in_byte = kid.stdout.read(1)
384 # Flush the rest of buffered output. This is only an issue with
385 # stdout/stderr not ending with a \n.
386 if len(in_line):
387 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000388 rv = kid.wait()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000389 if rv:
maruel@chromium.org7b194c12010-09-07 20:57:09 +0000390 raise CheckCallError(args, kwargs.get('cwd', None), rv, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000391 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000392
393
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000394def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000395 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000396 real_from_dir = os.path.realpath(from_dir)
397 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000398 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000399 split_path = os.path.split(path)
400 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000401 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000402 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000403
404 # If we did not find the file in the current directory, make sure we are in a
405 # sub directory that is controlled by this configuration.
406 if path != real_from_dir:
407 entries_filename = os.path.join(path, filename + '_entries')
408 if not os.path.exists(entries_filename):
409 # If .gclient_entries does not exist, a previous call to gclient sync
410 # might have failed. In that case, we cannot verify that the .gclient
411 # is the one we want to use. In order to not to cause too much trouble,
412 # just issue a warning and return the path anyway.
413 print >>sys.stderr, ("%s file in parent directory %s might not be the "
414 "file you want to use" % (filename, path))
415 return path
416 scope = {}
417 try:
418 exec(FileRead(entries_filename), scope)
419 except SyntaxError, e:
420 SyntaxErrorToError(filename, e)
421 all_directories = scope['entries'].keys()
422 path_to_check = real_from_dir[len(path)+1:]
423 while path_to_check:
424 if path_to_check in all_directories:
425 return path
426 path_to_check = os.path.dirname(path_to_check)
427 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000428
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000429 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000430 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000431
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000432
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000433def PathDifference(root, subpath):
434 """Returns the difference subpath minus root."""
435 root = os.path.realpath(root)
436 subpath = os.path.realpath(subpath)
437 if not subpath.startswith(root):
438 return None
439 # If the root does not have a trailing \ or /, we add it so the returned
440 # path starts immediately after the seperator regardless of whether it is
441 # provided.
442 root = os.path.join(root, '')
443 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000444
445
446def FindFileUpwards(filename, path=None):
447 """Search upwards from the a directory (default: current) to find a file."""
448 if not path:
449 path = os.getcwd()
450 path = os.path.realpath(path)
451 while True:
452 file_path = os.path.join(path, filename)
453 if os.path.isfile(file_path):
454 return file_path
455 (new_path, _) = os.path.split(path)
456 if new_path == path:
457 return None
458 path = new_path
459
460
461def GetGClientRootAndEntries(path=None):
462 """Returns the gclient root and the dict of entries."""
463 config_file = '.gclient_entries'
464 config_path = FindFileUpwards(config_file, path)
465
466 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000467 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000468 return None
469
470 env = {}
471 execfile(config_path, env)
472 config_dir = os.path.dirname(config_path)
473 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000474
475
476class WorkItem(object):
477 """One work item."""
478 # A list of string, each being a WorkItem name.
479 requirements = []
480 # A unique string representing this work item.
481 name = None
482
maruel@chromium.org3742c842010-09-09 19:27:14 +0000483 def run(self, work_queue, options):
484 """work_queue and options are passed as keyword arguments so they should be
485 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000486 pass
487
488
489class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000490 """Runs a set of WorkItem that have interdependencies and were WorkItem are
491 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000492
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000493 In gclient's case, Dependencies sometime needs to be run out of order due to
494 From() keyword. This class manages that all the required dependencies are run
495 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000496
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000497 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000498 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000499 def __init__(self, jobs, progress):
500 """jobs specifies the number of concurrent tasks to allow. progress is a
501 Progress instance."""
502 # Set when a thread is done or a new item is enqueued.
503 self.ready_cond = threading.Condition()
504 # Maximum number of concurrent tasks.
505 self.jobs = jobs
506 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000507 self.queued = []
508 # List of strings representing each Dependency.name that was run.
509 self.ran = []
510 # List of items currently running.
511 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000512 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000513 self.exceptions = Queue.Queue()
514 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000515 self.progress = progress
516 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000517 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000518
519 def enqueue(self, d):
520 """Enqueue one Dependency to be executed later once its requirements are
521 satisfied.
522 """
523 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000524 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000525 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000526 self.queued.append(d)
527 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000528 logging.debug('enqueued(%s)' % d.name)
529 if self.progress:
530 self.progress._total = total + 1
531 self.progress.update(0)
532 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000533 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000534 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000535
536 def flush(self, *args, **kwargs):
537 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000538 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000539 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000540 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000541 while True:
542 # Check for task to run first, then wait.
543 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000544 if not self.exceptions.empty():
545 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000546 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000547 self._flush_terminated_threads()
548 if (not self.queued and not self.running or
549 self.jobs == len(self.running)):
550 # No more worker threads or can't queue anything.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000551 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000552
553 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000554 for i in xrange(len(self.queued)):
555 # Verify its requirements.
556 for r in self.queued[i].requirements:
557 if not r in self.ran:
558 # Requirement not met.
559 break
560 else:
561 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000562 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000563 break
564 else:
565 # Couldn't find an item that could run. Break out the outher loop.
566 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000567
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000568 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000569 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000570 break
571 # We need to poll here otherwise Ctrl-C isn't processed.
572 self.ready_cond.wait(10)
573 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000574 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000575 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000576
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000577 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000578 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000579 # To get back the stack location correctly, the raise a, b, c form must be
580 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000581 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000582 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000583 if self.progress:
584 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000585
maruel@chromium.org3742c842010-09-09 19:27:14 +0000586 def _flush_terminated_threads(self):
587 """Flush threads that have terminated."""
588 running = self.running
589 self.running = []
590 for t in running:
591 if t.isAlive():
592 self.running.append(t)
593 else:
594 t.join()
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000595 t.kwargs['options'].stdout.full_flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000596 if self.progress:
597 self.progress.update(1)
598 assert not t.name in self.ran
599 if not t.name in self.ran:
600 self.ran.append(t.name)
601
602 def _run_one_task(self, task_item, args, kwargs):
603 if self.jobs > 1:
604 # Start the thread.
605 index = len(self.ran) + len(self.running) + 1
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000606 # Copy 'options' and add annotated stdout.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000607 task_kwargs = kwargs.copy()
608 task_kwargs['options'] = copy.copy(task_kwargs['options'])
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000609 task_kwargs['options'].stdout = StdoutAnnotated(
610 '%d>' % index, task_kwargs['options'].stdout)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000611 new_thread = self._Worker(task_item, args, task_kwargs)
612 self.running.append(new_thread)
613 new_thread.start()
614 else:
615 # Run the 'thread' inside the main thread. Don't try to catch any
616 # exception.
617 task_item.run(*args, **kwargs)
618 self.ran.append(task_item.name)
619 if self.progress:
620 self.progress.update(1)
621
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000622 class _Worker(threading.Thread):
623 """One thread to execute one WorkItem."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000624 def __init__(self, item, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000625 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org3742c842010-09-09 19:27:14 +0000626 logging.info(item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000627 self.item = item
maruel@chromium.org3742c842010-09-09 19:27:14 +0000628 self.args = args
629 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000630
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000631 def run(self):
632 """Runs in its own thread."""
633 logging.debug('running(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000634 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000635 try:
636 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000637 except Exception:
638 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000639 logging.info('Caught exception in thread %s' % self.item.name)
640 logging.info(str(sys.exc_info()))
641 work_queue.exceptions.put(sys.exc_info())
642 logging.info('Task %s done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000643
maruel@chromium.org3742c842010-09-09 19:27:14 +0000644 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000645 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000646 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000647 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000648 work_queue.ready_cond.release()