blob: 8506321c3d79e003a7c05158715d6ed268871d48 [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)
maruel@chromium.orga488c7d2010-08-22 03:49:18 +0000320 # Flush the rest of buffered output. This is only an issue with files not
321 # ending with a \n.
322 if len(in_line) and filter_fn:
323 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000324 rv = kid.wait()
325
326 if rv:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000327 msg = 'failed to run command: %s' % ' '.join(command)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000328
329 if fail_status != None:
maruel@chromium.orgad0b13d2010-08-20 18:42:22 +0000330 print >>sys.stderr, msg
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000331 sys.exit(fail_status)
332
333 raise Error(msg)
334
335
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000336def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000337 """Tries to find the gclient root."""
338 path = os.path.realpath(from_dir)
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000339 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.orgad0b13d2010-08-20 18:42:22 +0000340 next = os.path.split(path)
341 if not next[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000342 return None
maruel@chromium.orgad0b13d2010-08-20 18:42:22 +0000343 path = next[0]
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000344 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000345 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000346
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000347
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000348def PathDifference(root, subpath):
349 """Returns the difference subpath minus root."""
350 root = os.path.realpath(root)
351 subpath = os.path.realpath(subpath)
352 if not subpath.startswith(root):
353 return None
354 # If the root does not have a trailing \ or /, we add it so the returned
355 # path starts immediately after the seperator regardless of whether it is
356 # provided.
357 root = os.path.join(root, '')
358 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000359
360
361def FindFileUpwards(filename, path=None):
362 """Search upwards from the a directory (default: current) to find a file."""
363 if not path:
364 path = os.getcwd()
365 path = os.path.realpath(path)
366 while True:
367 file_path = os.path.join(path, filename)
368 if os.path.isfile(file_path):
369 return file_path
370 (new_path, _) = os.path.split(path)
371 if new_path == path:
372 return None
373 path = new_path
374
375
376def GetGClientRootAndEntries(path=None):
377 """Returns the gclient root and the dict of entries."""
378 config_file = '.gclient_entries'
379 config_path = FindFileUpwards(config_file, path)
380
381 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000382 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000383 return None
384
385 env = {}
386 execfile(config_path, env)
387 config_dir = os.path.dirname(config_path)
388 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000389
390
391class WorkItem(object):
392 """One work item."""
393 # A list of string, each being a WorkItem name.
394 requirements = []
395 # A unique string representing this work item.
396 name = None
397
398 def run(self):
399 pass
400
401
402class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000403 """Runs a set of WorkItem that have interdependencies and were WorkItem are
404 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000405
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000406 In gclient's case, Dependencies sometime needs to be run out of order due to
407 From() keyword. This class manages that all the required dependencies are run
408 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000409
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000410 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000411 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000412 def __init__(self, jobs, progress):
413 """jobs specifies the number of concurrent tasks to allow. progress is a
414 Progress instance."""
415 # Set when a thread is done or a new item is enqueued.
416 self.ready_cond = threading.Condition()
417 # Maximum number of concurrent tasks.
418 self.jobs = jobs
419 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000420 self.queued = []
421 # List of strings representing each Dependency.name that was run.
422 self.ran = []
423 # List of items currently running.
424 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000425 # Exceptions thrown if any.
426 self.exceptions = []
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000427 self.progress = progress
428 if self.progress:
429 self.progress.update()
430
431 def enqueue(self, d):
432 """Enqueue one Dependency to be executed later once its requirements are
433 satisfied.
434 """
435 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000436 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000437 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000438 self.queued.append(d)
439 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000440 logging.debug('enqueued(%s)' % d.name)
441 if self.progress:
442 self.progress._total = total + 1
443 self.progress.update(0)
444 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000445 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000446 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000447
448 def flush(self, *args, **kwargs):
449 """Runs all enqueued items until all are executed."""
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000450 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000451 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000452 while True:
453 # Check for task to run first, then wait.
454 while True:
455 if self.exceptions:
456 # Systematically flush the queue when there is an exception logged
457 # in.
458 self.queued = []
459 # Flush threads that have terminated.
460 self.running = [t for t in self.running if t.isAlive()]
461 if not self.queued and not self.running:
462 break
463 if self.jobs == len(self.running):
464 break
465 for i in xrange(len(self.queued)):
466 # Verify its requirements.
467 for r in self.queued[i].requirements:
468 if not r in self.ran:
469 # Requirement not met.
470 break
471 else:
472 # Start one work item: all its requirements are satisfied.
473 d = self.queued.pop(i)
474 new_thread = self._Worker(self, d, args=args, kwargs=kwargs)
475 if self.jobs > 1:
476 # Start the thread.
477 self.running.append(new_thread)
478 new_thread.start()
479 else:
480 # Run the 'thread' inside the main thread.
481 new_thread.run()
482 break
483 else:
484 # Couldn't find an item that could run. Break out the outher loop.
485 break
486 if not self.queued and not self.running:
487 break
488 # We need to poll here otherwise Ctrl-C isn't processed.
489 self.ready_cond.wait(10)
490 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000491 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000492 self.ready_cond.release()
493 assert not self.running, 'Now guaranteed to be single-threaded'
494 if self.exceptions:
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000495 # To get back the stack location correctly, the raise a, b, c form must be
496 # used, passing a tuple as the first argument doesn't work.
497 e = self.exceptions.pop(0)
498 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000499 if self.progress:
500 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000501
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000502 class _Worker(threading.Thread):
503 """One thread to execute one WorkItem."""
504 def __init__(self, parent, item, args=(), kwargs=None):
505 threading.Thread.__init__(self, name=item.name or 'Worker')
506 self.args = args
507 self.kwargs = kwargs or {}
508 self.item = item
509 self.parent = parent
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000510
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000511 def run(self):
512 """Runs in its own thread."""
513 logging.debug('running(%s)' % self.item.name)
514 exception = None
515 try:
516 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000517 except Exception:
518 # Catch exception location.
519 exception = sys.exc_info()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000520
521 # This assumes the following code won't throw an exception. Bad.
522 self.parent.ready_cond.acquire()
523 try:
524 if exception:
525 self.parent.exceptions.append(exception)
526 if self.parent.progress:
527 self.parent.progress.update(1)
528 assert not self.item.name in self.parent.ran
529 if not self.item.name in self.parent.ran:
530 self.parent.ran.append(self.item.name)
531 finally:
532 self.parent.ready_cond.notifyAll()
533 self.parent.ready_cond.release()