blob: 8c75ecefac399a9be895ff63d6c40cff6caee76b [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
254def SubprocessCall(command, in_directory, fail_status=None):
255 """Runs command, a list, in directory in_directory.
256
257 This function wraps SubprocessCallAndFilter, but does not perform the
258 filtering functions. See that function for a more complete usage
259 description.
260 """
261 # Call subprocess and capture nothing:
262 SubprocessCallAndFilter(command, in_directory, True, True, fail_status)
263
264
265def SubprocessCallAndFilter(command,
266 in_directory,
267 print_messages,
268 print_stdout,
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000269 fail_status=None,
270 filter_fn=None,
271 stdout=None):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000272 """Runs command, a list, in directory in_directory.
273
274 If print_messages is true, a message indicating what is being done
dpranke@google.com22e29d42009-10-28 00:48:26 +0000275 is printed to stdout. If print_messages is false, the message is printed
276 only if we actually need to print something else as well, so you can
277 get the context of the output. If print_messages is false and print_stdout
278 is false, no output at all is generated.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000279
280 Also, if print_stdout is true, the command's stdout is also forwarded
281 to stdout.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000282
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000283 If a filter_fn function is specified, it is expected to take a single
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000284 string argument, and it will be called with each line of the
285 subprocess's output. Each line has had the trailing newline character
286 trimmed.
287
288 If the command fails, as indicated by a nonzero exit status, gclient will
289 exit with an exit status of fail_status. If fail_status is None (the
290 default), gclient will raise an Error exception.
291 """
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000292 stdout = stdout or sys.stdout
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000293 logging.debug(command)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000294 if print_messages:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000295 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000296 % (' '.join(command), in_directory))
297
maruel@chromium.org3a292682010-08-23 18:54:55 +0000298 kid = Popen(command, bufsize=0, cwd=in_directory,
299 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000300
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000301 # Do a flush of sys.stdout before we begin reading from the subprocess's
302 # stdout.
303 last_flushed_at = time.time()
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000304 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000305
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000306 # Also, we need to forward stdout to prevent weird re-ordering of output.
307 # This has to be done on a per byte basis to make sure it is not buffered:
308 # normally buffering is done for each line, but if svn requests input, no
309 # end-of-line character is output after the prompt and it would not show up.
310 in_byte = kid.stdout.read(1)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000311 in_line = ''
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000312 while in_byte:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000313 if in_byte != '\r':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000314 if print_stdout:
dpranke@google.com22e29d42009-10-28 00:48:26 +0000315 if not print_messages:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000316 stdout.write('\n________ running \'%s\' in \'%s\'\n'
dpranke@google.com22e29d42009-10-28 00:48:26 +0000317 % (' '.join(command), in_directory))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000318 print_messages = True
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000319 stdout.write(in_byte)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000320 if in_byte != '\n':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000321 in_line += in_byte
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000322 if in_byte == '\n':
323 if filter_fn:
324 filter_fn(in_line)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000325 in_line = ''
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000326 # Flush at least 10 seconds between line writes. We wait at least 10
327 # seconds to avoid overloading the reader that called us with output,
328 # which can slow busy readers down.
329 if (time.time() - last_flushed_at) > 10:
330 last_flushed_at = time.time()
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000331 stdout.flush()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000332 in_byte = kid.stdout.read(1)
maruel@chromium.orga488c7d2010-08-22 03:49:18 +0000333 # Flush the rest of buffered output. This is only an issue with files not
334 # ending with a \n.
335 if len(in_line) and filter_fn:
336 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000337 rv = kid.wait()
338
339 if rv:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000340 msg = 'failed to run command: %s' % ' '.join(command)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000341 if fail_status != None:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000342 sys.stderr.write(msg + '\n')
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000343 sys.exit(fail_status)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000344 raise Error(msg)
345
346
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000347def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000348 """Tries to find the gclient root."""
349 path = os.path.realpath(from_dir)
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000350 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000351 split_path = os.path.split(path)
352 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000353 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000354 path = split_path[0]
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000355 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000356 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000357
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000358
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000359def PathDifference(root, subpath):
360 """Returns the difference subpath minus root."""
361 root = os.path.realpath(root)
362 subpath = os.path.realpath(subpath)
363 if not subpath.startswith(root):
364 return None
365 # If the root does not have a trailing \ or /, we add it so the returned
366 # path starts immediately after the seperator regardless of whether it is
367 # provided.
368 root = os.path.join(root, '')
369 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000370
371
372def FindFileUpwards(filename, path=None):
373 """Search upwards from the a directory (default: current) to find a file."""
374 if not path:
375 path = os.getcwd()
376 path = os.path.realpath(path)
377 while True:
378 file_path = os.path.join(path, filename)
379 if os.path.isfile(file_path):
380 return file_path
381 (new_path, _) = os.path.split(path)
382 if new_path == path:
383 return None
384 path = new_path
385
386
387def GetGClientRootAndEntries(path=None):
388 """Returns the gclient root and the dict of entries."""
389 config_file = '.gclient_entries'
390 config_path = FindFileUpwards(config_file, path)
391
392 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000393 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000394 return None
395
396 env = {}
397 execfile(config_path, env)
398 config_dir = os.path.dirname(config_path)
399 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000400
401
402class WorkItem(object):
403 """One work item."""
404 # A list of string, each being a WorkItem name.
405 requirements = []
406 # A unique string representing this work item.
407 name = None
408
409 def run(self):
410 pass
411
412
413class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000414 """Runs a set of WorkItem that have interdependencies and were WorkItem are
415 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000416
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000417 In gclient's case, Dependencies sometime needs to be run out of order due to
418 From() keyword. This class manages that all the required dependencies are run
419 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000420
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000421 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000422 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000423 def __init__(self, jobs, progress):
424 """jobs specifies the number of concurrent tasks to allow. progress is a
425 Progress instance."""
426 # Set when a thread is done or a new item is enqueued.
427 self.ready_cond = threading.Condition()
428 # Maximum number of concurrent tasks.
429 self.jobs = jobs
430 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000431 self.queued = []
432 # List of strings representing each Dependency.name that was run.
433 self.ran = []
434 # List of items currently running.
435 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000436 # Exceptions thrown if any.
437 self.exceptions = []
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000438 self.progress = progress
439 if self.progress:
440 self.progress.update()
441
442 def enqueue(self, d):
443 """Enqueue one Dependency to be executed later once its requirements are
444 satisfied.
445 """
446 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000447 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000448 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000449 self.queued.append(d)
450 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000451 logging.debug('enqueued(%s)' % d.name)
452 if self.progress:
453 self.progress._total = total + 1
454 self.progress.update(0)
455 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000456 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000457 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000458
459 def flush(self, *args, **kwargs):
460 """Runs all enqueued items until all are executed."""
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000461 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000462 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000463 while True:
464 # Check for task to run first, then wait.
465 while True:
466 if self.exceptions:
467 # Systematically flush the queue when there is an exception logged
468 # in.
469 self.queued = []
470 # Flush threads that have terminated.
471 self.running = [t for t in self.running if t.isAlive()]
472 if not self.queued and not self.running:
473 break
474 if self.jobs == len(self.running):
475 break
476 for i in xrange(len(self.queued)):
477 # Verify its requirements.
478 for r in self.queued[i].requirements:
479 if not r in self.ran:
480 # Requirement not met.
481 break
482 else:
483 # Start one work item: all its requirements are satisfied.
484 d = self.queued.pop(i)
485 new_thread = self._Worker(self, d, args=args, kwargs=kwargs)
486 if self.jobs > 1:
487 # Start the thread.
488 self.running.append(new_thread)
489 new_thread.start()
490 else:
491 # Run the 'thread' inside the main thread.
492 new_thread.run()
493 break
494 else:
495 # Couldn't find an item that could run. Break out the outher loop.
496 break
497 if not self.queued and not self.running:
498 break
499 # We need to poll here otherwise Ctrl-C isn't processed.
500 self.ready_cond.wait(10)
501 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000502 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000503 self.ready_cond.release()
504 assert not self.running, 'Now guaranteed to be single-threaded'
505 if self.exceptions:
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000506 # To get back the stack location correctly, the raise a, b, c form must be
507 # used, passing a tuple as the first argument doesn't work.
508 e = self.exceptions.pop(0)
509 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000510 if self.progress:
511 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000512
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000513 class _Worker(threading.Thread):
514 """One thread to execute one WorkItem."""
515 def __init__(self, parent, item, args=(), kwargs=None):
516 threading.Thread.__init__(self, name=item.name or 'Worker')
517 self.args = args
518 self.kwargs = kwargs or {}
519 self.item = item
520 self.parent = parent
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000521
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000522 def run(self):
523 """Runs in its own thread."""
524 logging.debug('running(%s)' % self.item.name)
525 exception = None
526 try:
527 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000528 except Exception:
529 # Catch exception location.
530 exception = sys.exc_info()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000531
532 # This assumes the following code won't throw an exception. Bad.
533 self.parent.ready_cond.acquire()
534 try:
535 if exception:
536 self.parent.exceptions.append(exception)
537 if self.parent.progress:
538 self.parent.progress.update(1)
539 assert not self.item.name in self.parent.ran
540 if not self.item.name in self.parent.ran:
541 self.parent.ran.append(self.item.name)
542 finally:
543 self.parent.ready_cond.notifyAll()
544 self.parent.ready_cond.release()