blob: 1efa036f6e99b970dea91e4d6d0c3b96e368e90f [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.orgad0b13d2010-08-20 18:42:22 +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.orgad0b13d2010-08-20 18:42:22 +000043 """Like subprocess.check_call() but returns stdout.
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000044
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.orgad0b13d2010-08-20 18:42:22 +000052 env = os.environ.copy()
53 env['LANGUAGE'] = 'en'
54 process = subprocess.Popen(command, cwd=cwd,
55 shell=sys.platform.startswith('win'),
56 stdout=subprocess.PIPE,
57 stderr=stderr,
58 env=env)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000059 std_out, std_err = process.communicate()
maruel@chromium.org18111352009-12-20 17:21:28 +000060 except OSError, e:
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +000061 raise CheckCallError(command, cwd, e.errno, None)
maruel@chromium.org18111352009-12-20 17:21:28 +000062 if process.returncode:
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000063 raise CheckCallError(command, cwd, process.returncode, std_out, std_err)
64 return std_out, std_err
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000065
66
msb@chromium.orgac915bb2009-11-13 17:03:01 +000067def SplitUrlRevision(url):
68 """Splits url and returns a two-tuple: url, rev"""
69 if url.startswith('ssh:'):
70 # Make sure ssh://test@example.com/test.git@stable works
maruel@chromium.org116704f2010-06-11 17:34:38 +000071 regex = r'(ssh://(?:[\w]+@)?[-\w:\.]+/[-\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000072 components = re.search(regex, url).groups()
73 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000074 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000075 if len(components) == 1:
76 components += [None]
77 return tuple(components)
78
79
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000080def ParseXML(output):
81 try:
82 return xml.dom.minidom.parseString(output)
83 except xml.parsers.expat.ExpatError:
84 return None
85
86
87def GetNamedNodeText(node, node_name):
88 child_nodes = node.getElementsByTagName(node_name)
89 if not child_nodes:
90 return None
91 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
92 return child_nodes[0].firstChild.nodeValue
93
94
95def GetNodeNamedAttributeText(node, node_name, attribute_name):
96 child_nodes = node.getElementsByTagName(node_name)
97 if not child_nodes:
98 return None
99 assert len(child_nodes) == 1
100 return child_nodes[0].getAttribute(attribute_name)
101
102
103class Error(Exception):
104 """gclient exception class."""
105 pass
106
107
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000108def SyntaxErrorToError(filename, e):
109 """Raises a gclient_utils.Error exception with the human readable message"""
110 try:
111 # Try to construct a human readable error message
112 if filename:
113 error_message = 'There is a syntax error in %s\n' % filename
114 else:
115 error_message = 'There is a syntax error\n'
116 error_message += 'Line #%s, character %s: "%s"' % (
117 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
118 except:
119 # Something went wrong, re-raise the original exception
120 raise e
121 else:
122 raise Error(error_message)
123
124
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000125class PrintableObject(object):
126 def __str__(self):
127 output = ''
128 for i in dir(self):
129 if i.startswith('__'):
130 continue
131 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
132 return output
133
134
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000135def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000136 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000137 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000138 try:
139 content = f.read()
140 finally:
141 f.close()
142 return content
143
144
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000145def FileWrite(filename, content, mode='w'):
146 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000147 try:
148 f.write(content)
149 finally:
150 f.close()
151
152
153def RemoveDirectory(*path):
154 """Recursively removes a directory, even if it's marked read-only.
155
156 Remove the directory located at *path, if it exists.
157
158 shutil.rmtree() doesn't work on Windows if any of the files or directories
159 are read-only, which svn repositories and some .svn files are. We need to
160 be able to force the files to be writable (i.e., deletable) as we traverse
161 the tree.
162
163 Even with all this, Windows still sometimes fails to delete a file, citing
164 a permission error (maybe something to do with antivirus scans or disk
165 indexing). The best suggestion any of the user forums had was to wait a
166 bit and try again, so we do that too. It's hand-waving, but sometimes it
167 works. :/
168
169 On POSIX systems, things are a little bit simpler. The modes of the files
170 to be deleted doesn't matter, only the modes of the directories containing
171 them are significant. As the directory tree is traversed, each directory
172 has its mode set appropriately before descending into it. This should
173 result in the entire tree being removed, with the possible exception of
174 *path itself, because nothing attempts to change the mode of its parent.
175 Doing so would be hazardous, as it's not a directory slated for removal.
176 In the ordinary case, this is not a problem: for our purposes, the user
177 will never lack write permission on *path's parent.
178 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000179 logging.debug(path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000180 file_path = os.path.join(*path)
181 if not os.path.exists(file_path):
182 return
183
184 if os.path.islink(file_path) or not os.path.isdir(file_path):
maruel@chromium.org116704f2010-06-11 17:34:38 +0000185 raise Error('RemoveDirectory asked to remove non-directory %s' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000186
187 has_win32api = False
188 if sys.platform == 'win32':
189 has_win32api = True
190 # Some people don't have the APIs installed. In that case we'll do without.
191 try:
192 win32api = __import__('win32api')
193 win32con = __import__('win32con')
194 except ImportError:
195 has_win32api = False
196 else:
197 # On POSIX systems, we need the x-bit set on the directory to access it,
198 # the r-bit to see its contents, and the w-bit to remove files from it.
199 # The actual modes of the files within the directory is irrelevant.
200 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
201 for fn in os.listdir(file_path):
202 fullpath = os.path.join(file_path, fn)
203
204 # If fullpath is a symbolic link that points to a directory, isdir will
205 # be True, but we don't want to descend into that as a directory, we just
206 # want to remove the link. Check islink and treat links as ordinary files
207 # would be treated regardless of what they reference.
208 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
209 if sys.platform == 'win32':
210 os.chmod(fullpath, stat.S_IWRITE)
211 if has_win32api:
212 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
213 try:
214 os.remove(fullpath)
215 except OSError, e:
216 if e.errno != errno.EACCES or sys.platform != 'win32':
217 raise
218 print 'Failed to delete %s: trying again' % fullpath
219 time.sleep(0.1)
220 os.remove(fullpath)
221 else:
222 RemoveDirectory(fullpath)
223
224 if sys.platform == 'win32':
225 os.chmod(file_path, stat.S_IWRITE)
226 if has_win32api:
227 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
228 try:
229 os.rmdir(file_path)
230 except OSError, e:
231 if e.errno != errno.EACCES or sys.platform != 'win32':
232 raise
233 print 'Failed to remove %s: trying again' % file_path
234 time.sleep(0.1)
235 os.rmdir(file_path)
236
237
238def SubprocessCall(command, in_directory, fail_status=None):
239 """Runs command, a list, in directory in_directory.
240
241 This function wraps SubprocessCallAndFilter, but does not perform the
242 filtering functions. See that function for a more complete usage
243 description.
244 """
245 # Call subprocess and capture nothing:
246 SubprocessCallAndFilter(command, in_directory, True, True, fail_status)
247
248
249def SubprocessCallAndFilter(command,
250 in_directory,
251 print_messages,
252 print_stdout,
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000253 fail_status=None, filter_fn=None):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000254 """Runs command, a list, in directory in_directory.
255
256 If print_messages is true, a message indicating what is being done
dpranke@google.com22e29d42009-10-28 00:48:26 +0000257 is printed to stdout. If print_messages is false, the message is printed
258 only if we actually need to print something else as well, so you can
259 get the context of the output. If print_messages is false and print_stdout
260 is false, no output at all is generated.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000261
262 Also, if print_stdout is true, the command's stdout is also forwarded
263 to stdout.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000264
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000265 If a filter_fn function is specified, it is expected to take a single
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000266 string argument, and it will be called with each line of the
267 subprocess's output. Each line has had the trailing newline character
268 trimmed.
269
270 If the command fails, as indicated by a nonzero exit status, gclient will
271 exit with an exit status of fail_status. If fail_status is None (the
272 default), gclient will raise an Error exception.
273 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000274 logging.debug(command)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000275 if print_messages:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000276 print('\n________ running \'%s\' in \'%s\''
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000277 % (' '.join(command), in_directory))
maruel@chromium.orgad0b13d2010-08-20 18:42:22 +0000278 env = os.environ.copy()
279 env['LANGUAGE'] = 'en'
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000280
maruel@chromium.orgad0b13d2010-08-20 18:42:22 +0000281 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
282 # executable, but shell=True makes subprocess on Linux fail when it's called
283 # with a list because it only tries to execute the first item in the list.
284 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
285 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE,
286 stderr=subprocess.STDOUT, env=env)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000287
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000288 # Do a flush of sys.stdout before we begin reading from the subprocess's
289 # stdout.
290 last_flushed_at = time.time()
291 sys.stdout.flush()
292
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000293 # Also, we need to forward stdout to prevent weird re-ordering of output.
294 # This has to be done on a per byte basis to make sure it is not buffered:
295 # normally buffering is done for each line, but if svn requests input, no
296 # end-of-line character is output after the prompt and it would not show up.
297 in_byte = kid.stdout.read(1)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000298 in_line = ''
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000299 while in_byte:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000300 if in_byte != '\r':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000301 if print_stdout:
dpranke@google.com22e29d42009-10-28 00:48:26 +0000302 if not print_messages:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000303 print('\n________ running \'%s\' in \'%s\''
dpranke@google.com22e29d42009-10-28 00:48:26 +0000304 % (' '.join(command), in_directory))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000305 print_messages = True
dpranke@google.com9e890f92009-10-28 01:32:29 +0000306 sys.stdout.write(in_byte)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000307 if in_byte != '\n':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000308 in_line += in_byte
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000309 if in_byte == '\n':
310 if filter_fn:
311 filter_fn(in_line)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000312 in_line = ''
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000313 # Flush at least 10 seconds between line writes. We wait at least 10
314 # seconds to avoid overloading the reader that called us with output,
315 # which can slow busy readers down.
316 if (time.time() - last_flushed_at) > 10:
317 last_flushed_at = time.time()
318 sys.stdout.flush()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000319 in_byte = kid.stdout.read(1)
320 rv = kid.wait()
321
322 if rv:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000323 msg = 'failed to run command: %s' % ' '.join(command)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000324
325 if fail_status != None:
maruel@chromium.orgad0b13d2010-08-20 18:42:22 +0000326 print >>sys.stderr, msg
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000327 sys.exit(fail_status)
328
329 raise Error(msg)
330
331
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000332def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000333 """Tries to find the gclient root."""
334 path = os.path.realpath(from_dir)
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000335 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.orgad0b13d2010-08-20 18:42:22 +0000336 next = os.path.split(path)
337 if not next[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000338 return None
maruel@chromium.orgad0b13d2010-08-20 18:42:22 +0000339 path = next[0]
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000340 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000341 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000342
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000343
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000344def PathDifference(root, subpath):
345 """Returns the difference subpath minus root."""
346 root = os.path.realpath(root)
347 subpath = os.path.realpath(subpath)
348 if not subpath.startswith(root):
349 return None
350 # If the root does not have a trailing \ or /, we add it so the returned
351 # path starts immediately after the seperator regardless of whether it is
352 # provided.
353 root = os.path.join(root, '')
354 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000355
356
357def FindFileUpwards(filename, path=None):
358 """Search upwards from the a directory (default: current) to find a file."""
359 if not path:
360 path = os.getcwd()
361 path = os.path.realpath(path)
362 while True:
363 file_path = os.path.join(path, filename)
364 if os.path.isfile(file_path):
365 return file_path
366 (new_path, _) = os.path.split(path)
367 if new_path == path:
368 return None
369 path = new_path
370
371
372def GetGClientRootAndEntries(path=None):
373 """Returns the gclient root and the dict of entries."""
374 config_file = '.gclient_entries'
375 config_path = FindFileUpwards(config_file, path)
376
377 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000378 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000379 return None
380
381 env = {}
382 execfile(config_path, env)
383 config_dir = os.path.dirname(config_path)
384 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000385
386
387class WorkItem(object):
388 """One work item."""
389 # A list of string, each being a WorkItem name.
390 requirements = []
391 # A unique string representing this work item.
392 name = None
393
394 def run(self):
395 pass
396
397
398class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000399 """Runs a set of WorkItem that have interdependencies and were WorkItem are
400 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000401
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000402 In gclient's case, Dependencies sometime needs to be run out of order due to
403 From() keyword. This class manages that all the required dependencies are run
404 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000405
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000406 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000407 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000408 def __init__(self, jobs, progress):
409 """jobs specifies the number of concurrent tasks to allow. progress is a
410 Progress instance."""
411 # Set when a thread is done or a new item is enqueued.
412 self.ready_cond = threading.Condition()
413 # Maximum number of concurrent tasks.
414 self.jobs = jobs
415 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000416 self.queued = []
417 # List of strings representing each Dependency.name that was run.
418 self.ran = []
419 # List of items currently running.
420 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000421 # Exceptions thrown if any.
422 self.exceptions = []
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000423 self.progress = progress
424 if self.progress:
425 self.progress.update()
426
427 def enqueue(self, d):
428 """Enqueue one Dependency to be executed later once its requirements are
429 satisfied.
430 """
431 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000432 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000433 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000434 self.queued.append(d)
435 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000436 logging.debug('enqueued(%s)' % d.name)
437 if self.progress:
438 self.progress._total = total + 1
439 self.progress.update(0)
440 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000441 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000442 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000443
444 def flush(self, *args, **kwargs):
445 """Runs all enqueued items until all are executed."""
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.org9e5317a2010-08-13 20:35:11 +0000448 while True:
449 # Check for task to run first, then wait.
450 while True:
451 if self.exceptions:
452 # Systematically flush the queue when there is an exception logged
453 # in.
454 self.queued = []
455 # Flush threads that have terminated.
456 self.running = [t for t in self.running if t.isAlive()]
457 if not self.queued and not self.running:
458 break
459 if self.jobs == len(self.running):
460 break
461 for i in xrange(len(self.queued)):
462 # Verify its requirements.
463 for r in self.queued[i].requirements:
464 if not r in self.ran:
465 # Requirement not met.
466 break
467 else:
468 # Start one work item: all its requirements are satisfied.
469 d = self.queued.pop(i)
470 new_thread = self._Worker(self, d, args=args, kwargs=kwargs)
471 if self.jobs > 1:
472 # Start the thread.
473 self.running.append(new_thread)
474 new_thread.start()
475 else:
476 # Run the 'thread' inside the main thread.
477 new_thread.run()
478 break
479 else:
480 # Couldn't find an item that could run. Break out the outher loop.
481 break
482 if not self.queued and not self.running:
483 break
484 # We need to poll here otherwise Ctrl-C isn't processed.
485 self.ready_cond.wait(10)
486 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000487 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000488 self.ready_cond.release()
489 assert not self.running, 'Now guaranteed to be single-threaded'
490 if self.exceptions:
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000491 # To get back the stack location correctly, the raise a, b, c form must be
492 # used, passing a tuple as the first argument doesn't work.
493 e = self.exceptions.pop(0)
494 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000495 if self.progress:
496 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000497
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000498 class _Worker(threading.Thread):
499 """One thread to execute one WorkItem."""
500 def __init__(self, parent, item, args=(), kwargs=None):
501 threading.Thread.__init__(self, name=item.name or 'Worker')
502 self.args = args
503 self.kwargs = kwargs or {}
504 self.item = item
505 self.parent = parent
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000506
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000507 def run(self):
508 """Runs in its own thread."""
509 logging.debug('running(%s)' % self.item.name)
510 exception = None
511 try:
512 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000513 except Exception:
514 # Catch exception location.
515 exception = sys.exc_info()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000516
517 # This assumes the following code won't throw an exception. Bad.
518 self.parent.ready_cond.acquire()
519 try:
520 if exception:
521 self.parent.exceptions.append(exception)
522 if self.parent.progress:
523 self.parent.progress.update(1)
524 assert not self.item.name in self.parent.ran
525 if not self.item.name in self.parent.ran:
526 self.parent.ran.append(self.item.name)
527 finally:
528 self.parent.ready_cond.notifyAll()
529 self.parent.ready_cond.release()