blob: 66514f12aeb5991e2131aa1c987875e1e242dacb [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."""
maruel@chromium.org17d01792010-09-01 18:07:10 +0000121 # TODO(maruel): Merge with CheckCallError.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000122 pass
123
124
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000125def SyntaxErrorToError(filename, e):
126 """Raises a gclient_utils.Error exception with the human readable message"""
127 try:
128 # Try to construct a human readable error message
129 if filename:
130 error_message = 'There is a syntax error in %s\n' % filename
131 else:
132 error_message = 'There is a syntax error\n'
133 error_message += 'Line #%s, character %s: "%s"' % (
134 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
135 except:
136 # Something went wrong, re-raise the original exception
137 raise e
138 else:
139 raise Error(error_message)
140
141
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000142class PrintableObject(object):
143 def __str__(self):
144 output = ''
145 for i in dir(self):
146 if i.startswith('__'):
147 continue
148 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
149 return output
150
151
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000152def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000153 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000154 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000155 try:
156 content = f.read()
157 finally:
158 f.close()
159 return content
160
161
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000162def FileWrite(filename, content, mode='w'):
163 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000164 try:
165 f.write(content)
166 finally:
167 f.close()
168
169
170def RemoveDirectory(*path):
171 """Recursively removes a directory, even if it's marked read-only.
172
173 Remove the directory located at *path, if it exists.
174
175 shutil.rmtree() doesn't work on Windows if any of the files or directories
176 are read-only, which svn repositories and some .svn files are. We need to
177 be able to force the files to be writable (i.e., deletable) as we traverse
178 the tree.
179
180 Even with all this, Windows still sometimes fails to delete a file, citing
181 a permission error (maybe something to do with antivirus scans or disk
182 indexing). The best suggestion any of the user forums had was to wait a
183 bit and try again, so we do that too. It's hand-waving, but sometimes it
184 works. :/
185
186 On POSIX systems, things are a little bit simpler. The modes of the files
187 to be deleted doesn't matter, only the modes of the directories containing
188 them are significant. As the directory tree is traversed, each directory
189 has its mode set appropriately before descending into it. This should
190 result in the entire tree being removed, with the possible exception of
191 *path itself, because nothing attempts to change the mode of its parent.
192 Doing so would be hazardous, as it's not a directory slated for removal.
193 In the ordinary case, this is not a problem: for our purposes, the user
194 will never lack write permission on *path's parent.
195 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000196 logging.debug(path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000197 file_path = os.path.join(*path)
198 if not os.path.exists(file_path):
199 return
200
201 if os.path.islink(file_path) or not os.path.isdir(file_path):
maruel@chromium.org116704f2010-06-11 17:34:38 +0000202 raise Error('RemoveDirectory asked to remove non-directory %s' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000203
204 has_win32api = False
205 if sys.platform == 'win32':
206 has_win32api = True
207 # Some people don't have the APIs installed. In that case we'll do without.
208 try:
209 win32api = __import__('win32api')
210 win32con = __import__('win32con')
211 except ImportError:
212 has_win32api = False
213 else:
214 # On POSIX systems, we need the x-bit set on the directory to access it,
215 # the r-bit to see its contents, and the w-bit to remove files from it.
216 # The actual modes of the files within the directory is irrelevant.
217 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
218 for fn in os.listdir(file_path):
219 fullpath = os.path.join(file_path, fn)
220
221 # If fullpath is a symbolic link that points to a directory, isdir will
222 # be True, but we don't want to descend into that as a directory, we just
223 # want to remove the link. Check islink and treat links as ordinary files
224 # would be treated regardless of what they reference.
225 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
226 if sys.platform == 'win32':
227 os.chmod(fullpath, stat.S_IWRITE)
228 if has_win32api:
229 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
230 try:
231 os.remove(fullpath)
232 except OSError, e:
233 if e.errno != errno.EACCES or sys.platform != 'win32':
234 raise
235 print 'Failed to delete %s: trying again' % fullpath
236 time.sleep(0.1)
237 os.remove(fullpath)
238 else:
239 RemoveDirectory(fullpath)
240
241 if sys.platform == 'win32':
242 os.chmod(file_path, stat.S_IWRITE)
243 if has_win32api:
244 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
245 try:
246 os.rmdir(file_path)
247 except OSError, e:
248 if e.errno != errno.EACCES or sys.platform != 'win32':
249 raise
250 print 'Failed to remove %s: trying again' % file_path
251 time.sleep(0.1)
252 os.rmdir(file_path)
253
254
maruel@chromium.org17d01792010-09-01 18:07:10 +0000255def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
256 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000257
maruel@chromium.org17d01792010-09-01 18:07:10 +0000258 If |always| is True, a message indicating what is being done
259 is printed to stdout all the time even if not output is generated. Otherwise
260 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000261 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000262 stdout = kwargs.get('stdout', None) or sys.stdout
263 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000264 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000265 % (' '.join(args), kwargs.get('cwd', '.')))
266 else:
267 filter_fn = kwargs.get('filter_fn', None)
268 def filter_msg(line):
269 if line is None:
270 stdout.write('\n________ running \'%s\' in \'%s\'\n'
271 % (' '.join(args), kwargs.get('cwd', '.')))
272 elif filter_fn:
273 filter_fn(line)
274 kwargs['filter_fn'] = filter_msg
275 kwargs['call_filter_on_first_line'] = True
276 # Obviously.
277 kwargs['print_stdout'] = True
278 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000279
maruel@chromium.org17d01792010-09-01 18:07:10 +0000280
281def CheckCallAndFilter(args, stdout=None, filter_fn=None,
282 print_stdout=None, call_filter_on_first_line=False,
283 **kwargs):
284 """Runs a command and calls back a filter function if needed.
285
286 Accepts all subprocess.Popen() parameters plus:
287 print_stdout: If True, the command's stdout is forwarded to stdout.
288 filter_fn: A function taking a single string argument called with each line
289 of the subprocess's output. Each line has the trailing newline
290 character trimmed.
291 stdout: Can be any bufferable output.
292
293 stderr is always redirected to stdout.
294 """
295 assert print_stdout or filter_fn
296 stdout = stdout or sys.stdout
297 filter_fn = filter_fn or (lambda x: None)
298 assert not 'stderr' in kwargs
299 logging.debug(args)
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000300 kid = Popen(args, bufsize=0,
301 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
302 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000303
maruel@chromium.org17d01792010-09-01 18:07:10 +0000304 # Do a flush of stdout before we begin reading from the subprocess's stdout
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000305 last_flushed_at = time.time()
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000306 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000307
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000308 # Also, we need to forward stdout to prevent weird re-ordering of output.
309 # This has to be done on a per byte basis to make sure it is not buffered:
310 # normally buffering is done for each line, but if svn requests input, no
311 # end-of-line character is output after the prompt and it would not show up.
312 in_byte = kid.stdout.read(1)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000313 if in_byte:
314 if call_filter_on_first_line:
315 filter_fn(None)
316 in_line = ''
317 while in_byte:
318 if in_byte != '\r':
319 if print_stdout:
320 stdout.write(in_byte)
321 if in_byte != '\n':
322 in_line += in_byte
323 else:
324 filter_fn(in_line)
325 in_line = ''
326 # 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()
331 stdout.flush()
332 in_byte = kid.stdout.read(1)
333 # Flush the rest of buffered output. This is only an issue with
334 # stdout/stderr not ending with a \n.
335 if len(in_line):
336 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000337 rv = kid.wait()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000338 if rv:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000339 raise Error('failed to run command: %s' % ' '.join(args))
340 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000341
342
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000343def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000344 """Tries to find the gclient root."""
345 path = os.path.realpath(from_dir)
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000346 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000347 split_path = os.path.split(path)
348 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000349 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000350 path = split_path[0]
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000351 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000352 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000353
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000354
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000355def PathDifference(root, subpath):
356 """Returns the difference subpath minus root."""
357 root = os.path.realpath(root)
358 subpath = os.path.realpath(subpath)
359 if not subpath.startswith(root):
360 return None
361 # If the root does not have a trailing \ or /, we add it so the returned
362 # path starts immediately after the seperator regardless of whether it is
363 # provided.
364 root = os.path.join(root, '')
365 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000366
367
368def FindFileUpwards(filename, path=None):
369 """Search upwards from the a directory (default: current) to find a file."""
370 if not path:
371 path = os.getcwd()
372 path = os.path.realpath(path)
373 while True:
374 file_path = os.path.join(path, filename)
375 if os.path.isfile(file_path):
376 return file_path
377 (new_path, _) = os.path.split(path)
378 if new_path == path:
379 return None
380 path = new_path
381
382
383def GetGClientRootAndEntries(path=None):
384 """Returns the gclient root and the dict of entries."""
385 config_file = '.gclient_entries'
386 config_path = FindFileUpwards(config_file, path)
387
388 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000389 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000390 return None
391
392 env = {}
393 execfile(config_path, env)
394 config_dir = os.path.dirname(config_path)
395 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000396
397
398class WorkItem(object):
399 """One work item."""
400 # A list of string, each being a WorkItem name.
401 requirements = []
402 # A unique string representing this work item.
403 name = None
404
405 def run(self):
406 pass
407
408
409class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000410 """Runs a set of WorkItem that have interdependencies and were WorkItem are
411 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000412
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000413 In gclient's case, Dependencies sometime needs to be run out of order due to
414 From() keyword. This class manages that all the required dependencies are run
415 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000416
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000417 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000418 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000419 def __init__(self, jobs, progress):
420 """jobs specifies the number of concurrent tasks to allow. progress is a
421 Progress instance."""
422 # Set when a thread is done or a new item is enqueued.
423 self.ready_cond = threading.Condition()
424 # Maximum number of concurrent tasks.
425 self.jobs = jobs
426 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000427 self.queued = []
428 # List of strings representing each Dependency.name that was run.
429 self.ran = []
430 # List of items currently running.
431 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000432 # Exceptions thrown if any.
433 self.exceptions = []
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000434 self.progress = progress
435 if self.progress:
436 self.progress.update()
437
438 def enqueue(self, d):
439 """Enqueue one Dependency to be executed later once its requirements are
440 satisfied.
441 """
442 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000443 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000444 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000445 self.queued.append(d)
446 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000447 logging.debug('enqueued(%s)' % d.name)
448 if self.progress:
449 self.progress._total = total + 1
450 self.progress.update(0)
451 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000452 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000453 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000454
455 def flush(self, *args, **kwargs):
456 """Runs all enqueued items until all are executed."""
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000457 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000458 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000459 while True:
460 # Check for task to run first, then wait.
461 while True:
462 if self.exceptions:
463 # Systematically flush the queue when there is an exception logged
464 # in.
465 self.queued = []
466 # Flush threads that have terminated.
467 self.running = [t for t in self.running if t.isAlive()]
468 if not self.queued and not self.running:
469 break
470 if self.jobs == len(self.running):
471 break
472 for i in xrange(len(self.queued)):
473 # Verify its requirements.
474 for r in self.queued[i].requirements:
475 if not r in self.ran:
476 # Requirement not met.
477 break
478 else:
479 # Start one work item: all its requirements are satisfied.
480 d = self.queued.pop(i)
481 new_thread = self._Worker(self, d, args=args, kwargs=kwargs)
482 if self.jobs > 1:
483 # Start the thread.
484 self.running.append(new_thread)
485 new_thread.start()
486 else:
487 # Run the 'thread' inside the main thread.
488 new_thread.run()
489 break
490 else:
491 # Couldn't find an item that could run. Break out the outher loop.
492 break
493 if not self.queued and not self.running:
494 break
495 # We need to poll here otherwise Ctrl-C isn't processed.
496 self.ready_cond.wait(10)
497 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000498 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000499 self.ready_cond.release()
500 assert not self.running, 'Now guaranteed to be single-threaded'
501 if self.exceptions:
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000502 # To get back the stack location correctly, the raise a, b, c form must be
503 # used, passing a tuple as the first argument doesn't work.
504 e = self.exceptions.pop(0)
505 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000506 if self.progress:
507 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000508
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000509 class _Worker(threading.Thread):
510 """One thread to execute one WorkItem."""
511 def __init__(self, parent, item, args=(), kwargs=None):
512 threading.Thread.__init__(self, name=item.name or 'Worker')
513 self.args = args
514 self.kwargs = kwargs or {}
515 self.item = item
516 self.parent = parent
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000517
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000518 def run(self):
519 """Runs in its own thread."""
520 logging.debug('running(%s)' % self.item.name)
521 exception = None
522 try:
523 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000524 except Exception:
525 # Catch exception location.
526 exception = sys.exc_info()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000527
528 # This assumes the following code won't throw an exception. Bad.
529 self.parent.ready_cond.acquire()
530 try:
531 if exception:
532 self.parent.exceptions.append(exception)
533 if self.parent.progress:
534 self.parent.progress.update(1)
535 assert not self.item.name in self.parent.ran
536 if not self.item.name in self.parent.ran:
537 self.parent.ran.append(self.item.name)
538 finally:
539 self.parent.ready_cond.notifyAll()
540 self.parent.ready_cond.release()