blob: e0c9e8e276c2532c14b2669b6796641aacd24034 [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.org167b9e62009-09-17 17:41:02 +000017import errno
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000018import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000019import os
msb@chromium.orgac915bb2009-11-13 17:03:01 +000020import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000021import stat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000022import subprocess
23import sys
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000024import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000025import time
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000026import xml.dom.minidom
maruel@chromium.org167b9e62009-09-17 17:41:02 +000027import xml.parsers.expat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000028
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000029
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000030class CheckCallError(OSError):
31 """CheckCall() returned non-0."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000032 def __init__(self, command, cwd, retcode, stdout, stderr=None):
33 OSError.__init__(self, command, cwd, retcode, stdout, stderr)
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000034 self.command = command
35 self.cwd = cwd
36 self.retcode = retcode
37 self.stdout = stdout
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000038 self.stderr = stderr
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000039
40
maruel@chromium.org3a292682010-08-23 18:54:55 +000041def Popen(*args, **kwargs):
42 """Calls subprocess.Popen() with hacks to work around certain behaviors.
43
44 Ensure English outpout for svn and make it work reliably on Windows.
45 """
46 copied = False
47 if not 'env' in kwargs:
48 copied = True
49 kwargs = kwargs.copy()
50 # It's easier to parse the stdout if it is always in English.
51 kwargs['env'] = os.environ.copy()
52 kwargs['env']['LANGUAGE'] = 'en'
53 if not 'shell' in kwargs:
54 if not copied:
55 kwargs = kwargs.copy()
56 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
57 # executable, but shell=True makes subprocess on Linux fail when it's called
58 # with a list because it only tries to execute the first item in the list.
59 kwargs['shell'] = (sys.platform=='win32')
60 return subprocess.Popen(*args, **kwargs)
61
62
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000063def CheckCall(command, cwd=None, print_error=True):
maruel@chromium.org3a292682010-08-23 18:54:55 +000064 """Similar subprocess.check_call() but redirects stdout and
65 returns (stdout, stderr).
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000066
67 Works on python 2.4
68 """
maruel@chromium.org116704f2010-06-11 17:34:38 +000069 logging.debug('%s, cwd=%s' % (str(command), str(cwd)))
maruel@chromium.org18111352009-12-20 17:21:28 +000070 try:
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000071 stderr = None
72 if not print_error:
73 stderr = subprocess.PIPE
maruel@chromium.org3a292682010-08-23 18:54:55 +000074 process = Popen(command, cwd=cwd, stdout=subprocess.PIPE, stderr=stderr)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000075 std_out, std_err = process.communicate()
maruel@chromium.org18111352009-12-20 17:21:28 +000076 except OSError, e:
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +000077 raise CheckCallError(command, cwd, e.errno, None)
maruel@chromium.org18111352009-12-20 17:21:28 +000078 if process.returncode:
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000079 raise CheckCallError(command, cwd, process.returncode, std_out, std_err)
80 return std_out, std_err
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000081
82
msb@chromium.orgac915bb2009-11-13 17:03:01 +000083def SplitUrlRevision(url):
84 """Splits url and returns a two-tuple: url, rev"""
85 if url.startswith('ssh:'):
86 # Make sure ssh://test@example.com/test.git@stable works
maruel@chromium.org116704f2010-06-11 17:34:38 +000087 regex = r'(ssh://(?:[\w]+@)?[-\w:\.]+/[-\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000088 components = re.search(regex, url).groups()
89 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000090 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000091 if len(components) == 1:
92 components += [None]
93 return tuple(components)
94
95
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000096def ParseXML(output):
97 try:
98 return xml.dom.minidom.parseString(output)
99 except xml.parsers.expat.ExpatError:
100 return None
101
102
103def GetNamedNodeText(node, node_name):
104 child_nodes = node.getElementsByTagName(node_name)
105 if not child_nodes:
106 return None
107 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
108 return child_nodes[0].firstChild.nodeValue
109
110
111def GetNodeNamedAttributeText(node, node_name, attribute_name):
112 child_nodes = node.getElementsByTagName(node_name)
113 if not child_nodes:
114 return None
115 assert len(child_nodes) == 1
116 return child_nodes[0].getAttribute(attribute_name)
117
118
119class Error(Exception):
120 """gclient exception class."""
121 pass
122
123
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000124def SyntaxErrorToError(filename, e):
125 """Raises a gclient_utils.Error exception with the human readable message"""
126 try:
127 # Try to construct a human readable error message
128 if filename:
129 error_message = 'There is a syntax error in %s\n' % filename
130 else:
131 error_message = 'There is a syntax error\n'
132 error_message += 'Line #%s, character %s: "%s"' % (
133 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
134 except:
135 # Something went wrong, re-raise the original exception
136 raise e
137 else:
138 raise Error(error_message)
139
140
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000141class PrintableObject(object):
142 def __str__(self):
143 output = ''
144 for i in dir(self):
145 if i.startswith('__'):
146 continue
147 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
148 return output
149
150
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000151def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000152 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000153 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000154 try:
155 content = f.read()
156 finally:
157 f.close()
158 return content
159
160
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000161def FileWrite(filename, content, mode='w'):
162 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000163 try:
164 f.write(content)
165 finally:
166 f.close()
167
168
169def RemoveDirectory(*path):
170 """Recursively removes a directory, even if it's marked read-only.
171
172 Remove the directory located at *path, if it exists.
173
174 shutil.rmtree() doesn't work on Windows if any of the files or directories
175 are read-only, which svn repositories and some .svn files are. We need to
176 be able to force the files to be writable (i.e., deletable) as we traverse
177 the tree.
178
179 Even with all this, Windows still sometimes fails to delete a file, citing
180 a permission error (maybe something to do with antivirus scans or disk
181 indexing). The best suggestion any of the user forums had was to wait a
182 bit and try again, so we do that too. It's hand-waving, but sometimes it
183 works. :/
184
185 On POSIX systems, things are a little bit simpler. The modes of the files
186 to be deleted doesn't matter, only the modes of the directories containing
187 them are significant. As the directory tree is traversed, each directory
188 has its mode set appropriately before descending into it. This should
189 result in the entire tree being removed, with the possible exception of
190 *path itself, because nothing attempts to change the mode of its parent.
191 Doing so would be hazardous, as it's not a directory slated for removal.
192 In the ordinary case, this is not a problem: for our purposes, the user
193 will never lack write permission on *path's parent.
194 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000195 logging.debug(path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000196 file_path = os.path.join(*path)
197 if not os.path.exists(file_path):
198 return
199
200 if os.path.islink(file_path) or not os.path.isdir(file_path):
maruel@chromium.org116704f2010-06-11 17:34:38 +0000201 raise Error('RemoveDirectory asked to remove non-directory %s' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000202
203 has_win32api = False
204 if sys.platform == 'win32':
205 has_win32api = True
206 # Some people don't have the APIs installed. In that case we'll do without.
207 try:
208 win32api = __import__('win32api')
209 win32con = __import__('win32con')
210 except ImportError:
211 has_win32api = False
212 else:
213 # On POSIX systems, we need the x-bit set on the directory to access it,
214 # the r-bit to see its contents, and the w-bit to remove files from it.
215 # The actual modes of the files within the directory is irrelevant.
216 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
217 for fn in os.listdir(file_path):
218 fullpath = os.path.join(file_path, fn)
219
220 # If fullpath is a symbolic link that points to a directory, isdir will
221 # be True, but we don't want to descend into that as a directory, we just
222 # want to remove the link. Check islink and treat links as ordinary files
223 # would be treated regardless of what they reference.
224 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
225 if sys.platform == 'win32':
226 os.chmod(fullpath, stat.S_IWRITE)
227 if has_win32api:
228 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
229 try:
230 os.remove(fullpath)
231 except OSError, e:
232 if e.errno != errno.EACCES or sys.platform != 'win32':
233 raise
234 print 'Failed to delete %s: trying again' % fullpath
235 time.sleep(0.1)
236 os.remove(fullpath)
237 else:
238 RemoveDirectory(fullpath)
239
240 if sys.platform == 'win32':
241 os.chmod(file_path, stat.S_IWRITE)
242 if has_win32api:
243 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
244 try:
245 os.rmdir(file_path)
246 except OSError, e:
247 if e.errno != errno.EACCES or sys.platform != 'win32':
248 raise
249 print 'Failed to remove %s: trying again' % file_path
250 time.sleep(0.1)
251 os.rmdir(file_path)
252
253
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000254def SubprocessCall(args, **kwargs):
255 """Wraps SubprocessCallAndFilter() with different default arguments.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000256
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000257 Calls subprocess and capture nothing."""
258 kwargs['print_messages'] = True
259 kwargs['print_stdout'] = True
260 return SubprocessCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000261
262
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000263def SubprocessCallAndFilter(args, **kwargs):
264 """Runs a command and prints a header line if appropriate.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000265
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000266 If |print_messages| is True, a message indicating what is being done
267 is printed to stdout. Otherwise the message is printed only if the call
268 generated any ouput. If both |print_messages| and |print_stdout| are False,
269 no output at all is generated.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000270
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000271 If |print_stdout| is True, the command's stdout is also forwarded to stdout.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000272
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000273 If |filter_fn| function is specified, it is expected to take a single
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000274 string argument, and it will be called with each line of the
275 subprocess's output. Each line has had the trailing newline character
276 trimmed.
277
278 If the command fails, as indicated by a nonzero exit status, gclient will
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000279 exit with an exit status of fail_status. If fail_status is None (the
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000280 default), gclient will raise an Error exception.
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000281
282 Other subprocess.Popen parameters can be specified.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000283 """
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000284 stdout = kwargs.pop('stdout', sys.stdout) or sys.stdout
285 assert not 'stderr' in kwargs
286 filter_fn = kwargs.pop('filter_fn', None)
287 print_messages = kwargs.pop('print_messages', False)
288 print_stdout = kwargs.pop('print_stdout', False)
289 fail_status = kwargs.pop('fail_status', None)
290
291 logging.debug(args)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000292 if print_messages:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000293 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000294 % (' '.join(args), kwargs['cwd']))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000295
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000296 kid = Popen(args, bufsize=0,
297 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
298 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000299
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000300 # Do a flush of sys.stdout before we begin reading from the subprocess's
301 # stdout.
302 last_flushed_at = time.time()
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000303 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000304
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000305 # Also, we need to forward stdout to prevent weird re-ordering of output.
306 # This has to be done on a per byte basis to make sure it is not buffered:
307 # normally buffering is done for each line, but if svn requests input, no
308 # end-of-line character is output after the prompt and it would not show up.
309 in_byte = kid.stdout.read(1)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000310 in_line = ''
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000311 while in_byte:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000312 if in_byte != '\r':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000313 if print_stdout:
dpranke@google.com22e29d42009-10-28 00:48:26 +0000314 if not print_messages:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000315 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000316 % (' '.join(args), kwargs['cwd']))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000317 print_messages = True
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000318 stdout.write(in_byte)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000319 if in_byte != '\n':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000320 in_line += in_byte
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000321 if in_byte == '\n':
322 if filter_fn:
323 filter_fn(in_line)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000324 in_line = ''
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000325 # Flush at least 10 seconds between line writes. We wait at least 10
326 # seconds to avoid overloading the reader that called us with output,
327 # which can slow busy readers down.
328 if (time.time() - last_flushed_at) > 10:
329 last_flushed_at = time.time()
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000330 stdout.flush()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000331 in_byte = kid.stdout.read(1)
maruel@chromium.orga488c7d2010-08-22 03:49:18 +0000332 # Flush the rest of buffered output. This is only an issue with files not
333 # ending with a \n.
334 if len(in_line) and filter_fn:
335 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000336 rv = kid.wait()
337
338 if rv:
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000339 msg = 'failed to run command: %s' % ' '.join(args)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000340 if fail_status != None:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000341 sys.stderr.write(msg + '\n')
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000342 sys.exit(fail_status)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000343 raise Error(msg)
344
345
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000346def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000347 """Tries to find the gclient root."""
348 path = os.path.realpath(from_dir)
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000349 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000350 split_path = os.path.split(path)
351 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000352 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000353 path = split_path[0]
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000354 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000355 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000356
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000357
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000358def PathDifference(root, subpath):
359 """Returns the difference subpath minus root."""
360 root = os.path.realpath(root)
361 subpath = os.path.realpath(subpath)
362 if not subpath.startswith(root):
363 return None
364 # If the root does not have a trailing \ or /, we add it so the returned
365 # path starts immediately after the seperator regardless of whether it is
366 # provided.
367 root = os.path.join(root, '')
368 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000369
370
371def FindFileUpwards(filename, path=None):
372 """Search upwards from the a directory (default: current) to find a file."""
373 if not path:
374 path = os.getcwd()
375 path = os.path.realpath(path)
376 while True:
377 file_path = os.path.join(path, filename)
378 if os.path.isfile(file_path):
379 return file_path
380 (new_path, _) = os.path.split(path)
381 if new_path == path:
382 return None
383 path = new_path
384
385
386def GetGClientRootAndEntries(path=None):
387 """Returns the gclient root and the dict of entries."""
388 config_file = '.gclient_entries'
389 config_path = FindFileUpwards(config_file, path)
390
391 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000392 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000393 return None
394
395 env = {}
396 execfile(config_path, env)
397 config_dir = os.path.dirname(config_path)
398 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000399
400
401class WorkItem(object):
402 """One work item."""
403 # A list of string, each being a WorkItem name.
404 requirements = []
405 # A unique string representing this work item.
406 name = None
407
408 def run(self):
409 pass
410
411
412class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000413 """Runs a set of WorkItem that have interdependencies and were WorkItem are
414 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000415
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000416 In gclient's case, Dependencies sometime needs to be run out of order due to
417 From() keyword. This class manages that all the required dependencies are run
418 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000419
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000420 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000421 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000422 def __init__(self, jobs, progress):
423 """jobs specifies the number of concurrent tasks to allow. progress is a
424 Progress instance."""
425 # Set when a thread is done or a new item is enqueued.
426 self.ready_cond = threading.Condition()
427 # Maximum number of concurrent tasks.
428 self.jobs = jobs
429 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000430 self.queued = []
431 # List of strings representing each Dependency.name that was run.
432 self.ran = []
433 # List of items currently running.
434 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000435 # Exceptions thrown if any.
436 self.exceptions = []
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000437 self.progress = progress
438 if self.progress:
439 self.progress.update()
440
441 def enqueue(self, d):
442 """Enqueue one Dependency to be executed later once its requirements are
443 satisfied.
444 """
445 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000446 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000447 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000448 self.queued.append(d)
449 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000450 logging.debug('enqueued(%s)' % d.name)
451 if self.progress:
452 self.progress._total = total + 1
453 self.progress.update(0)
454 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000455 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000456 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000457
458 def flush(self, *args, **kwargs):
459 """Runs all enqueued items until all are executed."""
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000460 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000461 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000462 while True:
463 # Check for task to run first, then wait.
464 while True:
465 if self.exceptions:
466 # Systematically flush the queue when there is an exception logged
467 # in.
468 self.queued = []
469 # Flush threads that have terminated.
470 self.running = [t for t in self.running if t.isAlive()]
471 if not self.queued and not self.running:
472 break
473 if self.jobs == len(self.running):
474 break
475 for i in xrange(len(self.queued)):
476 # Verify its requirements.
477 for r in self.queued[i].requirements:
478 if not r in self.ran:
479 # Requirement not met.
480 break
481 else:
482 # Start one work item: all its requirements are satisfied.
483 d = self.queued.pop(i)
484 new_thread = self._Worker(self, d, args=args, kwargs=kwargs)
485 if self.jobs > 1:
486 # Start the thread.
487 self.running.append(new_thread)
488 new_thread.start()
489 else:
490 # Run the 'thread' inside the main thread.
491 new_thread.run()
492 break
493 else:
494 # Couldn't find an item that could run. Break out the outher loop.
495 break
496 if not self.queued and not self.running:
497 break
498 # We need to poll here otherwise Ctrl-C isn't processed.
499 self.ready_cond.wait(10)
500 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000501 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000502 self.ready_cond.release()
503 assert not self.running, 'Now guaranteed to be single-threaded'
504 if self.exceptions:
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000505 # To get back the stack location correctly, the raise a, b, c form must be
506 # used, passing a tuple as the first argument doesn't work.
507 e = self.exceptions.pop(0)
508 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000509 if self.progress:
510 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000511
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000512 class _Worker(threading.Thread):
513 """One thread to execute one WorkItem."""
514 def __init__(self, parent, item, args=(), kwargs=None):
515 threading.Thread.__init__(self, name=item.name or 'Worker')
516 self.args = args
517 self.kwargs = kwargs or {}
518 self.item = item
519 self.parent = parent
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000520
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000521 def run(self):
522 """Runs in its own thread."""
523 logging.debug('running(%s)' % self.item.name)
524 exception = None
525 try:
526 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000527 except Exception:
528 # Catch exception location.
529 exception = sys.exc_info()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000530
531 # This assumes the following code won't throw an exception. Bad.
532 self.parent.ready_cond.acquire()
533 try:
534 if exception:
535 self.parent.exceptions.append(exception)
536 if self.parent.progress:
537 self.parent.progress.update(1)
538 assert not self.item.name in self.parent.ran
539 if not self.item.name in self.parent.ran:
540 self.parent.ran.append(self.item.name)
541 finally:
542 self.parent.ready_cond.notifyAll()
543 self.parent.ready_cond.release()