blob: cf2cedec5b01284653a96f57061af0dc95dce95a [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.org66c83e62010-09-07 14:18:45 +000030class Error(Exception):
31 """gclient exception class."""
32 pass
33
34
35class CheckCallError(OSError, Error):
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000036 """CheckCall() returned non-0."""
maruel@chromium.org66c83e62010-09-07 14:18:45 +000037 def __init__(self, command, cwd, returncode, stdout, stderr=None):
38 OSError.__init__(self, command, cwd, returncode, stdout, stderr)
39 Error.__init__(self)
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000040 self.command = command
41 self.cwd = cwd
maruel@chromium.org66c83e62010-09-07 14:18:45 +000042 self.returncode = returncode
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000043 self.stdout = stdout
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000044 self.stderr = stderr
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000045
46
maruel@chromium.orga1693be2010-09-03 19:09:35 +000047def Popen(args, **kwargs):
maruel@chromium.org3a292682010-08-23 18:54:55 +000048 """Calls subprocess.Popen() with hacks to work around certain behaviors.
49
50 Ensure English outpout for svn and make it work reliably on Windows.
51 """
maruel@chromium.orga1693be2010-09-03 19:09:35 +000052 logging.debug(u'%s, cwd=%s' % (u' '.join(args), kwargs.get('cwd', '')))
maruel@chromium.org3a292682010-08-23 18:54:55 +000053 if not 'env' in kwargs:
maruel@chromium.org3a292682010-08-23 18:54:55 +000054 # It's easier to parse the stdout if it is always in English.
55 kwargs['env'] = os.environ.copy()
56 kwargs['env']['LANGUAGE'] = 'en'
57 if not 'shell' in kwargs:
maruel@chromium.org3a292682010-08-23 18:54:55 +000058 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
59 # executable, but shell=True makes subprocess on Linux fail when it's called
60 # with a list because it only tries to execute the first item in the list.
61 kwargs['shell'] = (sys.platform=='win32')
maruel@chromium.orga1693be2010-09-03 19:09:35 +000062 return subprocess.Popen(args, **kwargs)
maruel@chromium.org3a292682010-08-23 18:54:55 +000063
64
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000065def CheckCall(command, cwd=None, print_error=True):
maruel@chromium.org3a292682010-08-23 18:54:55 +000066 """Similar subprocess.check_call() but redirects stdout and
67 returns (stdout, stderr).
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000068
69 Works on python 2.4
70 """
maruel@chromium.org18111352009-12-20 17:21:28 +000071 try:
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000072 stderr = None
73 if not print_error:
74 stderr = subprocess.PIPE
maruel@chromium.org3a292682010-08-23 18:54:55 +000075 process = Popen(command, cwd=cwd, stdout=subprocess.PIPE, stderr=stderr)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000076 std_out, std_err = process.communicate()
maruel@chromium.org18111352009-12-20 17:21:28 +000077 except OSError, e:
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +000078 raise CheckCallError(command, cwd, e.errno, None)
maruel@chromium.org18111352009-12-20 17:21:28 +000079 if process.returncode:
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000080 raise CheckCallError(command, cwd, process.returncode, std_out, std_err)
81 return std_out, std_err
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000082
83
msb@chromium.orgac915bb2009-11-13 17:03:01 +000084def SplitUrlRevision(url):
85 """Splits url and returns a two-tuple: url, rev"""
86 if url.startswith('ssh:'):
87 # Make sure ssh://test@example.com/test.git@stable works
maruel@chromium.org116704f2010-06-11 17:34:38 +000088 regex = r'(ssh://(?:[\w]+@)?[-\w:\.]+/[-\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000089 components = re.search(regex, url).groups()
90 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000091 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000092 if len(components) == 1:
93 components += [None]
94 return tuple(components)
95
96
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000097def ParseXML(output):
98 try:
99 return xml.dom.minidom.parseString(output)
100 except xml.parsers.expat.ExpatError:
101 return None
102
103
104def GetNamedNodeText(node, node_name):
105 child_nodes = node.getElementsByTagName(node_name)
106 if not child_nodes:
107 return None
108 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
109 return child_nodes[0].firstChild.nodeValue
110
111
112def GetNodeNamedAttributeText(node, node_name, attribute_name):
113 child_nodes = node.getElementsByTagName(node_name)
114 if not child_nodes:
115 return None
116 assert len(child_nodes) == 1
117 return child_nodes[0].getAttribute(attribute_name)
118
119
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000120def SyntaxErrorToError(filename, e):
121 """Raises a gclient_utils.Error exception with the human readable message"""
122 try:
123 # Try to construct a human readable error message
124 if filename:
125 error_message = 'There is a syntax error in %s\n' % filename
126 else:
127 error_message = 'There is a syntax error\n'
128 error_message += 'Line #%s, character %s: "%s"' % (
129 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
130 except:
131 # Something went wrong, re-raise the original exception
132 raise e
133 else:
134 raise Error(error_message)
135
136
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000137class PrintableObject(object):
138 def __str__(self):
139 output = ''
140 for i in dir(self):
141 if i.startswith('__'):
142 continue
143 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
144 return output
145
146
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000147def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000148 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000149 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000150 try:
151 content = f.read()
152 finally:
153 f.close()
154 return content
155
156
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000157def FileWrite(filename, content, mode='w'):
158 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000159 try:
160 f.write(content)
161 finally:
162 f.close()
163
164
165def RemoveDirectory(*path):
166 """Recursively removes a directory, even if it's marked read-only.
167
168 Remove the directory located at *path, if it exists.
169
170 shutil.rmtree() doesn't work on Windows if any of the files or directories
171 are read-only, which svn repositories and some .svn files are. We need to
172 be able to force the files to be writable (i.e., deletable) as we traverse
173 the tree.
174
175 Even with all this, Windows still sometimes fails to delete a file, citing
176 a permission error (maybe something to do with antivirus scans or disk
177 indexing). The best suggestion any of the user forums had was to wait a
178 bit and try again, so we do that too. It's hand-waving, but sometimes it
179 works. :/
180
181 On POSIX systems, things are a little bit simpler. The modes of the files
182 to be deleted doesn't matter, only the modes of the directories containing
183 them are significant. As the directory tree is traversed, each directory
184 has its mode set appropriately before descending into it. This should
185 result in the entire tree being removed, with the possible exception of
186 *path itself, because nothing attempts to change the mode of its parent.
187 Doing so would be hazardous, as it's not a directory slated for removal.
188 In the ordinary case, this is not a problem: for our purposes, the user
189 will never lack write permission on *path's parent.
190 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000191 logging.debug(path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000192 file_path = os.path.join(*path)
193 if not os.path.exists(file_path):
194 return
195
196 if os.path.islink(file_path) or not os.path.isdir(file_path):
maruel@chromium.org116704f2010-06-11 17:34:38 +0000197 raise Error('RemoveDirectory asked to remove non-directory %s' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000198
199 has_win32api = False
200 if sys.platform == 'win32':
201 has_win32api = True
202 # Some people don't have the APIs installed. In that case we'll do without.
203 try:
204 win32api = __import__('win32api')
205 win32con = __import__('win32con')
206 except ImportError:
207 has_win32api = False
208 else:
209 # On POSIX systems, we need the x-bit set on the directory to access it,
210 # the r-bit to see its contents, and the w-bit to remove files from it.
211 # The actual modes of the files within the directory is irrelevant.
212 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
213 for fn in os.listdir(file_path):
214 fullpath = os.path.join(file_path, fn)
215
216 # If fullpath is a symbolic link that points to a directory, isdir will
217 # be True, but we don't want to descend into that as a directory, we just
218 # want to remove the link. Check islink and treat links as ordinary files
219 # would be treated regardless of what they reference.
220 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
221 if sys.platform == 'win32':
222 os.chmod(fullpath, stat.S_IWRITE)
223 if has_win32api:
224 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
225 try:
226 os.remove(fullpath)
227 except OSError, e:
228 if e.errno != errno.EACCES or sys.platform != 'win32':
229 raise
230 print 'Failed to delete %s: trying again' % fullpath
231 time.sleep(0.1)
232 os.remove(fullpath)
233 else:
234 RemoveDirectory(fullpath)
235
236 if sys.platform == 'win32':
237 os.chmod(file_path, stat.S_IWRITE)
238 if has_win32api:
239 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
240 try:
241 os.rmdir(file_path)
242 except OSError, e:
243 if e.errno != errno.EACCES or sys.platform != 'win32':
244 raise
245 print 'Failed to remove %s: trying again' % file_path
246 time.sleep(0.1)
247 os.rmdir(file_path)
248
249
maruel@chromium.org17d01792010-09-01 18:07:10 +0000250def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
251 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000252
maruel@chromium.org17d01792010-09-01 18:07:10 +0000253 If |always| is True, a message indicating what is being done
254 is printed to stdout all the time even if not output is generated. Otherwise
255 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000256 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000257 stdout = kwargs.get('stdout', None) or sys.stdout
258 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000259 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000260 % (' '.join(args), kwargs.get('cwd', '.')))
261 else:
262 filter_fn = kwargs.get('filter_fn', None)
263 def filter_msg(line):
264 if line is None:
265 stdout.write('\n________ running \'%s\' in \'%s\'\n'
266 % (' '.join(args), kwargs.get('cwd', '.')))
267 elif filter_fn:
268 filter_fn(line)
269 kwargs['filter_fn'] = filter_msg
270 kwargs['call_filter_on_first_line'] = True
271 # Obviously.
272 kwargs['print_stdout'] = True
273 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000274
maruel@chromium.org17d01792010-09-01 18:07:10 +0000275
276def CheckCallAndFilter(args, stdout=None, filter_fn=None,
277 print_stdout=None, call_filter_on_first_line=False,
278 **kwargs):
279 """Runs a command and calls back a filter function if needed.
280
281 Accepts all subprocess.Popen() parameters plus:
282 print_stdout: If True, the command's stdout is forwarded to stdout.
283 filter_fn: A function taking a single string argument called with each line
284 of the subprocess's output. Each line has the trailing newline
285 character trimmed.
286 stdout: Can be any bufferable output.
287
288 stderr is always redirected to stdout.
289 """
290 assert print_stdout or filter_fn
291 stdout = stdout or sys.stdout
292 filter_fn = filter_fn or (lambda x: None)
293 assert not 'stderr' in kwargs
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000294 kid = Popen(args, bufsize=0,
295 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
296 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000297
maruel@chromium.org17d01792010-09-01 18:07:10 +0000298 # Do a flush of stdout before we begin reading from the subprocess's stdout
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000299 last_flushed_at = time.time()
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000300 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000301
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000302 # Also, we need to forward stdout to prevent weird re-ordering of output.
303 # This has to be done on a per byte basis to make sure it is not buffered:
304 # normally buffering is done for each line, but if svn requests input, no
305 # end-of-line character is output after the prompt and it would not show up.
306 in_byte = kid.stdout.read(1)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000307 if in_byte:
308 if call_filter_on_first_line:
309 filter_fn(None)
310 in_line = ''
311 while in_byte:
312 if in_byte != '\r':
313 if print_stdout:
314 stdout.write(in_byte)
315 if in_byte != '\n':
316 in_line += in_byte
317 else:
318 filter_fn(in_line)
319 in_line = ''
320 # Flush at least 10 seconds between line writes. We wait at least 10
321 # seconds to avoid overloading the reader that called us with output,
322 # which can slow busy readers down.
323 if (time.time() - last_flushed_at) > 10:
324 last_flushed_at = time.time()
325 stdout.flush()
326 in_byte = kid.stdout.read(1)
327 # Flush the rest of buffered output. This is only an issue with
328 # stdout/stderr not ending with a \n.
329 if len(in_line):
330 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000331 rv = kid.wait()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000332 if rv:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000333 raise Error('failed to run command: %s' % ' '.join(args))
334 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000335
336
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000337def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000338 """Tries to find the gclient root."""
339 path = os.path.realpath(from_dir)
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000340 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000341 split_path = os.path.split(path)
342 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000343 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000344 path = split_path[0]
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000345 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000346 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000347
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000348
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000349def PathDifference(root, subpath):
350 """Returns the difference subpath minus root."""
351 root = os.path.realpath(root)
352 subpath = os.path.realpath(subpath)
353 if not subpath.startswith(root):
354 return None
355 # If the root does not have a trailing \ or /, we add it so the returned
356 # path starts immediately after the seperator regardless of whether it is
357 # provided.
358 root = os.path.join(root, '')
359 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000360
361
362def FindFileUpwards(filename, path=None):
363 """Search upwards from the a directory (default: current) to find a file."""
364 if not path:
365 path = os.getcwd()
366 path = os.path.realpath(path)
367 while True:
368 file_path = os.path.join(path, filename)
369 if os.path.isfile(file_path):
370 return file_path
371 (new_path, _) = os.path.split(path)
372 if new_path == path:
373 return None
374 path = new_path
375
376
377def GetGClientRootAndEntries(path=None):
378 """Returns the gclient root and the dict of entries."""
379 config_file = '.gclient_entries'
380 config_path = FindFileUpwards(config_file, path)
381
382 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000383 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000384 return None
385
386 env = {}
387 execfile(config_path, env)
388 config_dir = os.path.dirname(config_path)
389 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000390
391
392class WorkItem(object):
393 """One work item."""
394 # A list of string, each being a WorkItem name.
395 requirements = []
396 # A unique string representing this work item.
397 name = None
398
399 def run(self):
400 pass
401
402
403class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000404 """Runs a set of WorkItem that have interdependencies and were WorkItem are
405 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000406
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000407 In gclient's case, Dependencies sometime needs to be run out of order due to
408 From() keyword. This class manages that all the required dependencies are run
409 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000410
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000411 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000412 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000413 def __init__(self, jobs, progress):
414 """jobs specifies the number of concurrent tasks to allow. progress is a
415 Progress instance."""
416 # Set when a thread is done or a new item is enqueued.
417 self.ready_cond = threading.Condition()
418 # Maximum number of concurrent tasks.
419 self.jobs = jobs
420 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000421 self.queued = []
422 # List of strings representing each Dependency.name that was run.
423 self.ran = []
424 # List of items currently running.
425 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000426 # Exceptions thrown if any.
427 self.exceptions = []
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000428 self.progress = progress
429 if self.progress:
430 self.progress.update()
431
432 def enqueue(self, d):
433 """Enqueue one Dependency to be executed later once its requirements are
434 satisfied.
435 """
436 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000437 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000438 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000439 self.queued.append(d)
440 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000441 logging.debug('enqueued(%s)' % d.name)
442 if self.progress:
443 self.progress._total = total + 1
444 self.progress.update(0)
445 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000446 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000447 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000448
449 def flush(self, *args, **kwargs):
450 """Runs all enqueued items until all are executed."""
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000451 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000452 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000453 while True:
454 # Check for task to run first, then wait.
455 while True:
456 if self.exceptions:
457 # Systematically flush the queue when there is an exception logged
458 # in.
459 self.queued = []
460 # Flush threads that have terminated.
461 self.running = [t for t in self.running if t.isAlive()]
462 if not self.queued and not self.running:
463 break
464 if self.jobs == len(self.running):
465 break
466 for i in xrange(len(self.queued)):
467 # Verify its requirements.
468 for r in self.queued[i].requirements:
469 if not r in self.ran:
470 # Requirement not met.
471 break
472 else:
473 # Start one work item: all its requirements are satisfied.
474 d = self.queued.pop(i)
475 new_thread = self._Worker(self, d, args=args, kwargs=kwargs)
476 if self.jobs > 1:
477 # Start the thread.
478 self.running.append(new_thread)
479 new_thread.start()
480 else:
481 # Run the 'thread' inside the main thread.
482 new_thread.run()
483 break
484 else:
485 # Couldn't find an item that could run. Break out the outher loop.
486 break
487 if not self.queued and not self.running:
488 break
489 # We need to poll here otherwise Ctrl-C isn't processed.
490 self.ready_cond.wait(10)
491 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000492 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000493 self.ready_cond.release()
494 assert not self.running, 'Now guaranteed to be single-threaded'
495 if self.exceptions:
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000496 # To get back the stack location correctly, the raise a, b, c form must be
497 # used, passing a tuple as the first argument doesn't work.
498 e = self.exceptions.pop(0)
499 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000500 if self.progress:
501 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000502
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000503 class _Worker(threading.Thread):
504 """One thread to execute one WorkItem."""
505 def __init__(self, parent, item, args=(), kwargs=None):
506 threading.Thread.__init__(self, name=item.name or 'Worker')
507 self.args = args
508 self.kwargs = kwargs or {}
509 self.item = item
510 self.parent = parent
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000511
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000512 def run(self):
513 """Runs in its own thread."""
514 logging.debug('running(%s)' % self.item.name)
515 exception = None
516 try:
517 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000518 except Exception:
519 # Catch exception location.
520 exception = sys.exc_info()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000521
522 # This assumes the following code won't throw an exception. Bad.
523 self.parent.ready_cond.acquire()
524 try:
525 if exception:
526 self.parent.exceptions.append(exception)
527 if self.parent.progress:
528 self.parent.progress.update(1)
529 assert not self.item.name in self.parent.ran
530 if not self.item.name in self.parent.ran:
531 self.parent.ran.append(self.item.name)
532 finally:
533 self.parent.ready_cond.notifyAll()
534 self.parent.ready_cond.release()