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