blob: 19a39a69d822783bf36f22f6b7ac3aca6841bb35 [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.org80cbe8b2010-08-13 13:53:07 +000026import threading
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000027import xml.dom.minidom
maruel@chromium.org167b9e62009-09-17 17:41:02 +000028import xml.parsers.expat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000029
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000030
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000031class CheckCallError(OSError):
32 """CheckCall() returned non-0."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000033 def __init__(self, command, cwd, retcode, stdout, stderr=None):
34 OSError.__init__(self, command, cwd, retcode, stdout, stderr)
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000035 self.command = command
36 self.cwd = cwd
37 self.retcode = retcode
38 self.stdout = stdout
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000039 self.stderr = stderr
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000040
41
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000042def CheckCall(command, cwd=None, print_error=True):
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000043 """Like subprocess.check_call() but returns stdout.
44
45 Works on python 2.4
46 """
maruel@chromium.org116704f2010-06-11 17:34:38 +000047 logging.debug('%s, cwd=%s' % (str(command), str(cwd)))
maruel@chromium.org18111352009-12-20 17:21:28 +000048 try:
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000049 stderr = None
50 if not print_error:
51 stderr = subprocess.PIPE
maruel@chromium.org18111352009-12-20 17:21:28 +000052 process = subprocess.Popen(command, cwd=cwd,
53 shell=sys.platform.startswith('win'),
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000054 stdout=subprocess.PIPE,
55 stderr=stderr)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000056 std_out, std_err = process.communicate()
maruel@chromium.org18111352009-12-20 17:21:28 +000057 except OSError, e:
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +000058 raise CheckCallError(command, cwd, e.errno, None)
maruel@chromium.org18111352009-12-20 17:21:28 +000059 if process.returncode:
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000060 raise CheckCallError(command, cwd, process.returncode, std_out, std_err)
61 return std_out, std_err
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000062
63
msb@chromium.orgac915bb2009-11-13 17:03:01 +000064def SplitUrlRevision(url):
65 """Splits url and returns a two-tuple: url, rev"""
66 if url.startswith('ssh:'):
67 # Make sure ssh://test@example.com/test.git@stable works
maruel@chromium.org116704f2010-06-11 17:34:38 +000068 regex = r'(ssh://(?:[\w]+@)?[-\w:\.]+/[-\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000069 components = re.search(regex, url).groups()
70 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000071 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000072 if len(components) == 1:
73 components += [None]
74 return tuple(components)
75
76
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000077def ParseXML(output):
78 try:
79 return xml.dom.minidom.parseString(output)
80 except xml.parsers.expat.ExpatError:
81 return None
82
83
84def GetNamedNodeText(node, node_name):
85 child_nodes = node.getElementsByTagName(node_name)
86 if not child_nodes:
87 return None
88 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
89 return child_nodes[0].firstChild.nodeValue
90
91
92def GetNodeNamedAttributeText(node, node_name, attribute_name):
93 child_nodes = node.getElementsByTagName(node_name)
94 if not child_nodes:
95 return None
96 assert len(child_nodes) == 1
97 return child_nodes[0].getAttribute(attribute_name)
98
99
100class Error(Exception):
101 """gclient exception class."""
102 pass
103
104
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000105def SyntaxErrorToError(filename, e):
106 """Raises a gclient_utils.Error exception with the human readable message"""
107 try:
108 # Try to construct a human readable error message
109 if filename:
110 error_message = 'There is a syntax error in %s\n' % filename
111 else:
112 error_message = 'There is a syntax error\n'
113 error_message += 'Line #%s, character %s: "%s"' % (
114 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
115 except:
116 # Something went wrong, re-raise the original exception
117 raise e
118 else:
119 raise Error(error_message)
120
121
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000122class PrintableObject(object):
123 def __str__(self):
124 output = ''
125 for i in dir(self):
126 if i.startswith('__'):
127 continue
128 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
129 return output
130
131
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000132def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000133 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000134 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000135 try:
136 content = f.read()
137 finally:
138 f.close()
139 return content
140
141
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000142def FileWrite(filename, content, mode='w'):
143 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000144 try:
145 f.write(content)
146 finally:
147 f.close()
148
149
150def RemoveDirectory(*path):
151 """Recursively removes a directory, even if it's marked read-only.
152
153 Remove the directory located at *path, if it exists.
154
155 shutil.rmtree() doesn't work on Windows if any of the files or directories
156 are read-only, which svn repositories and some .svn files are. We need to
157 be able to force the files to be writable (i.e., deletable) as we traverse
158 the tree.
159
160 Even with all this, Windows still sometimes fails to delete a file, citing
161 a permission error (maybe something to do with antivirus scans or disk
162 indexing). The best suggestion any of the user forums had was to wait a
163 bit and try again, so we do that too. It's hand-waving, but sometimes it
164 works. :/
165
166 On POSIX systems, things are a little bit simpler. The modes of the files
167 to be deleted doesn't matter, only the modes of the directories containing
168 them are significant. As the directory tree is traversed, each directory
169 has its mode set appropriately before descending into it. This should
170 result in the entire tree being removed, with the possible exception of
171 *path itself, because nothing attempts to change the mode of its parent.
172 Doing so would be hazardous, as it's not a directory slated for removal.
173 In the ordinary case, this is not a problem: for our purposes, the user
174 will never lack write permission on *path's parent.
175 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000176 logging.debug(path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000177 file_path = os.path.join(*path)
178 if not os.path.exists(file_path):
179 return
180
181 if os.path.islink(file_path) or not os.path.isdir(file_path):
maruel@chromium.org116704f2010-06-11 17:34:38 +0000182 raise Error('RemoveDirectory asked to remove non-directory %s' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000183
184 has_win32api = False
185 if sys.platform == 'win32':
186 has_win32api = True
187 # Some people don't have the APIs installed. In that case we'll do without.
188 try:
189 win32api = __import__('win32api')
190 win32con = __import__('win32con')
191 except ImportError:
192 has_win32api = False
193 else:
194 # On POSIX systems, we need the x-bit set on the directory to access it,
195 # the r-bit to see its contents, and the w-bit to remove files from it.
196 # The actual modes of the files within the directory is irrelevant.
197 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
198 for fn in os.listdir(file_path):
199 fullpath = os.path.join(file_path, fn)
200
201 # If fullpath is a symbolic link that points to a directory, isdir will
202 # be True, but we don't want to descend into that as a directory, we just
203 # want to remove the link. Check islink and treat links as ordinary files
204 # would be treated regardless of what they reference.
205 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
206 if sys.platform == 'win32':
207 os.chmod(fullpath, stat.S_IWRITE)
208 if has_win32api:
209 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
210 try:
211 os.remove(fullpath)
212 except OSError, e:
213 if e.errno != errno.EACCES or sys.platform != 'win32':
214 raise
215 print 'Failed to delete %s: trying again' % fullpath
216 time.sleep(0.1)
217 os.remove(fullpath)
218 else:
219 RemoveDirectory(fullpath)
220
221 if sys.platform == 'win32':
222 os.chmod(file_path, stat.S_IWRITE)
223 if has_win32api:
224 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
225 try:
226 os.rmdir(file_path)
227 except OSError, e:
228 if e.errno != errno.EACCES or sys.platform != 'win32':
229 raise
230 print 'Failed to remove %s: trying again' % file_path
231 time.sleep(0.1)
232 os.rmdir(file_path)
233
234
235def SubprocessCall(command, in_directory, fail_status=None):
236 """Runs command, a list, in directory in_directory.
237
238 This function wraps SubprocessCallAndFilter, but does not perform the
239 filtering functions. See that function for a more complete usage
240 description.
241 """
242 # Call subprocess and capture nothing:
243 SubprocessCallAndFilter(command, in_directory, True, True, fail_status)
244
245
246def SubprocessCallAndFilter(command,
247 in_directory,
248 print_messages,
249 print_stdout,
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000250 fail_status=None, filter_fn=None):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000251 """Runs command, a list, in directory in_directory.
252
253 If print_messages is true, a message indicating what is being done
dpranke@google.com22e29d42009-10-28 00:48:26 +0000254 is printed to stdout. If print_messages is false, the message is printed
255 only if we actually need to print something else as well, so you can
256 get the context of the output. If print_messages is false and print_stdout
257 is false, no output at all is generated.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000258
259 Also, if print_stdout is true, the command's stdout is also forwarded
260 to stdout.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000261
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000262 If a filter_fn function is specified, it is expected to take a single
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000263 string argument, and it will be called with each line of the
264 subprocess's output. Each line has had the trailing newline character
265 trimmed.
266
267 If the command fails, as indicated by a nonzero exit status, gclient will
268 exit with an exit status of fail_status. If fail_status is None (the
269 default), gclient will raise an Error exception.
270 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000271 logging.debug(command)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000272 if print_messages:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000273 print('\n________ running \'%s\' in \'%s\''
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000274 % (' '.join(command), in_directory))
275
276 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
277 # executable, but shell=True makes subprocess on Linux fail when it's called
278 # with a list because it only tries to execute the first item in the list.
279 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000280 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE,
dpranke@google.com5cc6c572009-11-06 20:04:56 +0000281 stderr=subprocess.STDOUT)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000282
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000283 # Do a flush of sys.stdout before we begin reading from the subprocess's
284 # stdout.
285 last_flushed_at = time.time()
286 sys.stdout.flush()
287
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000288 # Also, we need to forward stdout to prevent weird re-ordering of output.
289 # This has to be done on a per byte basis to make sure it is not buffered:
290 # normally buffering is done for each line, but if svn requests input, no
291 # end-of-line character is output after the prompt and it would not show up.
292 in_byte = kid.stdout.read(1)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000293 in_line = ''
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000294 while in_byte:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000295 if in_byte != '\r':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000296 if print_stdout:
dpranke@google.com22e29d42009-10-28 00:48:26 +0000297 if not print_messages:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000298 print('\n________ running \'%s\' in \'%s\''
dpranke@google.com22e29d42009-10-28 00:48:26 +0000299 % (' '.join(command), in_directory))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000300 print_messages = True
dpranke@google.com9e890f92009-10-28 01:32:29 +0000301 sys.stdout.write(in_byte)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000302 if in_byte != '\n':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000303 in_line += in_byte
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000304 if in_byte == '\n':
305 if filter_fn:
306 filter_fn(in_line)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000307 in_line = ''
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000308 # Flush at least 10 seconds between line writes. We wait at least 10
309 # seconds to avoid overloading the reader that called us with output,
310 # which can slow busy readers down.
311 if (time.time() - last_flushed_at) > 10:
312 last_flushed_at = time.time()
313 sys.stdout.flush()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000314 in_byte = kid.stdout.read(1)
315 rv = kid.wait()
316
317 if rv:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000318 msg = 'failed to run command: %s' % ' '.join(command)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000319
320 if fail_status != None:
321 print >>sys.stderr, msg
322 sys.exit(fail_status)
323
324 raise Error(msg)
325
326
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000327def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000328 """Tries to find the gclient root."""
329 path = os.path.realpath(from_dir)
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000330 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000331 next = os.path.split(path)
332 if not next[1]:
333 return None
334 path = next[0]
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000335 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000336 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000337
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000338
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000339def PathDifference(root, subpath):
340 """Returns the difference subpath minus root."""
341 root = os.path.realpath(root)
342 subpath = os.path.realpath(subpath)
343 if not subpath.startswith(root):
344 return None
345 # If the root does not have a trailing \ or /, we add it so the returned
346 # path starts immediately after the seperator regardless of whether it is
347 # provided.
348 root = os.path.join(root, '')
349 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000350
351
352def FindFileUpwards(filename, path=None):
353 """Search upwards from the a directory (default: current) to find a file."""
354 if not path:
355 path = os.getcwd()
356 path = os.path.realpath(path)
357 while True:
358 file_path = os.path.join(path, filename)
359 if os.path.isfile(file_path):
360 return file_path
361 (new_path, _) = os.path.split(path)
362 if new_path == path:
363 return None
364 path = new_path
365
366
367def GetGClientRootAndEntries(path=None):
368 """Returns the gclient root and the dict of entries."""
369 config_file = '.gclient_entries'
370 config_path = FindFileUpwards(config_file, path)
371
372 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000373 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000374 return None
375
376 env = {}
377 execfile(config_path, env)
378 config_dir = os.path.dirname(config_path)
379 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000380
381
382class WorkItem(object):
383 """One work item."""
384 # A list of string, each being a WorkItem name.
385 requirements = []
386 # A unique string representing this work item.
387 name = None
388
389 def run(self):
390 pass
391
392
393class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000394 """Runs a set of WorkItem that have interdependencies and were WorkItem are
395 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000396
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000397 In gclient's case, Dependencies sometime needs to be run out of order due to
398 From() keyword. This class manages that all the required dependencies are run
399 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000400
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000401 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000402 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000403 def __init__(self, jobs, progress):
404 """jobs specifies the number of concurrent tasks to allow. progress is a
405 Progress instance."""
406 # Set when a thread is done or a new item is enqueued.
407 self.ready_cond = threading.Condition()
408 # Maximum number of concurrent tasks.
409 self.jobs = jobs
410 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000411 self.queued = []
412 # List of strings representing each Dependency.name that was run.
413 self.ran = []
414 # List of items currently running.
415 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000416 # Exceptions thrown if any.
417 self.exceptions = []
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000418 self.progress = progress
419 if self.progress:
420 self.progress.update()
421
422 def enqueue(self, d):
423 """Enqueue one Dependency to be executed later once its requirements are
424 satisfied.
425 """
426 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000427 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000428 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000429 self.queued.append(d)
430 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000431 logging.debug('enqueued(%s)' % d.name)
432 if self.progress:
433 self.progress._total = total + 1
434 self.progress.update(0)
435 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000436 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000437 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000438
439 def flush(self, *args, **kwargs):
440 """Runs all enqueued items until all are executed."""
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000441 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000442 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000443 while True:
444 # Check for task to run first, then wait.
445 while True:
446 if self.exceptions:
447 # Systematically flush the queue when there is an exception logged
448 # in.
449 self.queued = []
450 # Flush threads that have terminated.
451 self.running = [t for t in self.running if t.isAlive()]
452 if not self.queued and not self.running:
453 break
454 if self.jobs == len(self.running):
455 break
456 for i in xrange(len(self.queued)):
457 # Verify its requirements.
458 for r in self.queued[i].requirements:
459 if not r in self.ran:
460 # Requirement not met.
461 break
462 else:
463 # Start one work item: all its requirements are satisfied.
464 d = self.queued.pop(i)
465 new_thread = self._Worker(self, d, args=args, kwargs=kwargs)
466 if self.jobs > 1:
467 # Start the thread.
468 self.running.append(new_thread)
469 new_thread.start()
470 else:
471 # Run the 'thread' inside the main thread.
472 new_thread.run()
473 break
474 else:
475 # Couldn't find an item that could run. Break out the outher loop.
476 break
477 if not self.queued and not self.running:
478 break
479 # We need to poll here otherwise Ctrl-C isn't processed.
480 self.ready_cond.wait(10)
481 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000482 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000483 self.ready_cond.release()
484 assert not self.running, 'Now guaranteed to be single-threaded'
485 if self.exceptions:
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000486 # To get back the stack location correctly, the raise a, b, c form must be
487 # used, passing a tuple as the first argument doesn't work.
488 e = self.exceptions.pop(0)
489 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000490 if self.progress:
491 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000492
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000493 class _Worker(threading.Thread):
494 """One thread to execute one WorkItem."""
495 def __init__(self, parent, item, args=(), kwargs=None):
496 threading.Thread.__init__(self, name=item.name or 'Worker')
497 self.args = args
498 self.kwargs = kwargs or {}
499 self.item = item
500 self.parent = parent
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000501
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000502 def run(self):
503 """Runs in its own thread."""
504 logging.debug('running(%s)' % self.item.name)
505 exception = None
506 try:
507 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000508 except Exception:
509 # Catch exception location.
510 exception = sys.exc_info()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000511
512 # This assumes the following code won't throw an exception. Bad.
513 self.parent.ready_cond.acquire()
514 try:
515 if exception:
516 self.parent.exceptions.append(exception)
517 if self.parent.progress:
518 self.parent.progress.update(1)
519 assert not self.item.name in self.parent.ran
520 if not self.item.name in self.parent.ran:
521 self.parent.ran.append(self.item.name)
522 finally:
523 self.parent.ready_cond.notifyAll()
524 self.parent.ready_cond.release()