blob: 04767974e1bea980270f3d9a8dffd8a73934a9f6 [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
maruel@chromium.org7b194c12010-09-07 20:57:09 +000046 def __str__(self):
47 out = ' '.join(self.command)
48 if self.cwd:
49 out += ' in ' + self.cwd
50 if self.returncode is not None:
51 out += ' returned %d' % self.returncode
52 if self.stdout is not None:
53 out += '\nstdout: %s\n' % self.stdout
54 if self.stderr is not None:
55 out += '\nstderr: %s\n' % self.stderr
56 return out
57
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000058
maruel@chromium.orga1693be2010-09-03 19:09:35 +000059def Popen(args, **kwargs):
maruel@chromium.org3a292682010-08-23 18:54:55 +000060 """Calls subprocess.Popen() with hacks to work around certain behaviors.
61
62 Ensure English outpout for svn and make it work reliably on Windows.
63 """
maruel@chromium.orga1693be2010-09-03 19:09:35 +000064 logging.debug(u'%s, cwd=%s' % (u' '.join(args), kwargs.get('cwd', '')))
maruel@chromium.org3a292682010-08-23 18:54:55 +000065 if not 'env' in kwargs:
maruel@chromium.org3a292682010-08-23 18:54:55 +000066 # It's easier to parse the stdout if it is always in English.
67 kwargs['env'] = os.environ.copy()
68 kwargs['env']['LANGUAGE'] = 'en'
69 if not 'shell' in kwargs:
maruel@chromium.org3a292682010-08-23 18:54:55 +000070 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
71 # executable, but shell=True makes subprocess on Linux fail when it's called
72 # with a list because it only tries to execute the first item in the list.
73 kwargs['shell'] = (sys.platform=='win32')
maruel@chromium.orga1693be2010-09-03 19:09:35 +000074 return subprocess.Popen(args, **kwargs)
maruel@chromium.org3a292682010-08-23 18:54:55 +000075
76
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000077def CheckCall(command, cwd=None, print_error=True):
maruel@chromium.org3a292682010-08-23 18:54:55 +000078 """Similar subprocess.check_call() but redirects stdout and
79 returns (stdout, stderr).
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000080
81 Works on python 2.4
82 """
maruel@chromium.org18111352009-12-20 17:21:28 +000083 try:
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000084 stderr = None
85 if not print_error:
86 stderr = subprocess.PIPE
maruel@chromium.org3a292682010-08-23 18:54:55 +000087 process = Popen(command, cwd=cwd, stdout=subprocess.PIPE, stderr=stderr)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000088 std_out, std_err = process.communicate()
maruel@chromium.org18111352009-12-20 17:21:28 +000089 except OSError, e:
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +000090 raise CheckCallError(command, cwd, e.errno, None)
maruel@chromium.org18111352009-12-20 17:21:28 +000091 if process.returncode:
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000092 raise CheckCallError(command, cwd, process.returncode, std_out, std_err)
93 return std_out, std_err
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000094
95
msb@chromium.orgac915bb2009-11-13 17:03:01 +000096def SplitUrlRevision(url):
97 """Splits url and returns a two-tuple: url, rev"""
98 if url.startswith('ssh:'):
99 # Make sure ssh://test@example.com/test.git@stable works
maruel@chromium.org116704f2010-06-11 17:34:38 +0000100 regex = r'(ssh://(?:[\w]+@)?[-\w:\.]+/[-\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000101 components = re.search(regex, url).groups()
102 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000103 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000104 if len(components) == 1:
105 components += [None]
106 return tuple(components)
107
108
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000109def ParseXML(output):
110 try:
111 return xml.dom.minidom.parseString(output)
112 except xml.parsers.expat.ExpatError:
113 return None
114
115
116def GetNamedNodeText(node, node_name):
117 child_nodes = node.getElementsByTagName(node_name)
118 if not child_nodes:
119 return None
120 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
121 return child_nodes[0].firstChild.nodeValue
122
123
124def GetNodeNamedAttributeText(node, node_name, attribute_name):
125 child_nodes = node.getElementsByTagName(node_name)
126 if not child_nodes:
127 return None
128 assert len(child_nodes) == 1
129 return child_nodes[0].getAttribute(attribute_name)
130
131
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000132def SyntaxErrorToError(filename, e):
133 """Raises a gclient_utils.Error exception with the human readable message"""
134 try:
135 # Try to construct a human readable error message
136 if filename:
137 error_message = 'There is a syntax error in %s\n' % filename
138 else:
139 error_message = 'There is a syntax error\n'
140 error_message += 'Line #%s, character %s: "%s"' % (
141 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
142 except:
143 # Something went wrong, re-raise the original exception
144 raise e
145 else:
146 raise Error(error_message)
147
148
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000149class PrintableObject(object):
150 def __str__(self):
151 output = ''
152 for i in dir(self):
153 if i.startswith('__'):
154 continue
155 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
156 return output
157
158
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000159def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000160 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000161 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000162 try:
163 content = f.read()
164 finally:
165 f.close()
166 return content
167
168
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000169def FileWrite(filename, content, mode='w'):
170 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000171 try:
172 f.write(content)
173 finally:
174 f.close()
175
176
177def RemoveDirectory(*path):
178 """Recursively removes a directory, even if it's marked read-only.
179
180 Remove the directory located at *path, if it exists.
181
182 shutil.rmtree() doesn't work on Windows if any of the files or directories
183 are read-only, which svn repositories and some .svn files are. We need to
184 be able to force the files to be writable (i.e., deletable) as we traverse
185 the tree.
186
187 Even with all this, Windows still sometimes fails to delete a file, citing
188 a permission error (maybe something to do with antivirus scans or disk
189 indexing). The best suggestion any of the user forums had was to wait a
190 bit and try again, so we do that too. It's hand-waving, but sometimes it
191 works. :/
192
193 On POSIX systems, things are a little bit simpler. The modes of the files
194 to be deleted doesn't matter, only the modes of the directories containing
195 them are significant. As the directory tree is traversed, each directory
196 has its mode set appropriately before descending into it. This should
197 result in the entire tree being removed, with the possible exception of
198 *path itself, because nothing attempts to change the mode of its parent.
199 Doing so would be hazardous, as it's not a directory slated for removal.
200 In the ordinary case, this is not a problem: for our purposes, the user
201 will never lack write permission on *path's parent.
202 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000203 logging.debug(path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000204 file_path = os.path.join(*path)
205 if not os.path.exists(file_path):
206 return
207
208 if os.path.islink(file_path) or not os.path.isdir(file_path):
maruel@chromium.org116704f2010-06-11 17:34:38 +0000209 raise Error('RemoveDirectory asked to remove non-directory %s' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000210
211 has_win32api = False
212 if sys.platform == 'win32':
213 has_win32api = True
214 # Some people don't have the APIs installed. In that case we'll do without.
215 try:
216 win32api = __import__('win32api')
217 win32con = __import__('win32con')
218 except ImportError:
219 has_win32api = False
220 else:
221 # On POSIX systems, we need the x-bit set on the directory to access it,
222 # the r-bit to see its contents, and the w-bit to remove files from it.
223 # The actual modes of the files within the directory is irrelevant.
224 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
225 for fn in os.listdir(file_path):
226 fullpath = os.path.join(file_path, fn)
227
228 # If fullpath is a symbolic link that points to a directory, isdir will
229 # be True, but we don't want to descend into that as a directory, we just
230 # want to remove the link. Check islink and treat links as ordinary files
231 # would be treated regardless of what they reference.
232 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
233 if sys.platform == 'win32':
234 os.chmod(fullpath, stat.S_IWRITE)
235 if has_win32api:
236 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
237 try:
238 os.remove(fullpath)
239 except OSError, e:
240 if e.errno != errno.EACCES or sys.platform != 'win32':
241 raise
242 print 'Failed to delete %s: trying again' % fullpath
243 time.sleep(0.1)
244 os.remove(fullpath)
245 else:
246 RemoveDirectory(fullpath)
247
248 if sys.platform == 'win32':
249 os.chmod(file_path, stat.S_IWRITE)
250 if has_win32api:
251 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
252 try:
253 os.rmdir(file_path)
254 except OSError, e:
255 if e.errno != errno.EACCES or sys.platform != 'win32':
256 raise
257 print 'Failed to remove %s: trying again' % file_path
258 time.sleep(0.1)
259 os.rmdir(file_path)
260
261
maruel@chromium.org17d01792010-09-01 18:07:10 +0000262def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
263 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000264
maruel@chromium.org17d01792010-09-01 18:07:10 +0000265 If |always| is True, a message indicating what is being done
266 is printed to stdout all the time even if not output is generated. Otherwise
267 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000268 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000269 stdout = kwargs.get('stdout', None) or sys.stdout
270 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000271 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000272 % (' '.join(args), kwargs.get('cwd', '.')))
273 else:
274 filter_fn = kwargs.get('filter_fn', None)
275 def filter_msg(line):
276 if line is None:
277 stdout.write('\n________ running \'%s\' in \'%s\'\n'
278 % (' '.join(args), kwargs.get('cwd', '.')))
279 elif filter_fn:
280 filter_fn(line)
281 kwargs['filter_fn'] = filter_msg
282 kwargs['call_filter_on_first_line'] = True
283 # Obviously.
284 kwargs['print_stdout'] = True
285 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000286
maruel@chromium.org17d01792010-09-01 18:07:10 +0000287
288def CheckCallAndFilter(args, stdout=None, filter_fn=None,
289 print_stdout=None, call_filter_on_first_line=False,
290 **kwargs):
291 """Runs a command and calls back a filter function if needed.
292
293 Accepts all subprocess.Popen() parameters plus:
294 print_stdout: If True, the command's stdout is forwarded to stdout.
295 filter_fn: A function taking a single string argument called with each line
296 of the subprocess's output. Each line has the trailing newline
297 character trimmed.
298 stdout: Can be any bufferable output.
299
300 stderr is always redirected to stdout.
301 """
302 assert print_stdout or filter_fn
303 stdout = stdout or sys.stdout
304 filter_fn = filter_fn or (lambda x: None)
305 assert not 'stderr' in kwargs
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000306 kid = Popen(args, bufsize=0,
307 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
308 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000309
maruel@chromium.org17d01792010-09-01 18:07:10 +0000310 # Do a flush of stdout before we begin reading from the subprocess's stdout
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000311 last_flushed_at = time.time()
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000312 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000313
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000314 # Also, we need to forward stdout to prevent weird re-ordering of output.
315 # This has to be done on a per byte basis to make sure it is not buffered:
316 # normally buffering is done for each line, but if svn requests input, no
317 # end-of-line character is output after the prompt and it would not show up.
318 in_byte = kid.stdout.read(1)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000319 if in_byte:
320 if call_filter_on_first_line:
321 filter_fn(None)
322 in_line = ''
323 while in_byte:
324 if in_byte != '\r':
325 if print_stdout:
326 stdout.write(in_byte)
327 if in_byte != '\n':
328 in_line += in_byte
329 else:
330 filter_fn(in_line)
331 in_line = ''
332 # Flush at least 10 seconds between line writes. We wait at least 10
333 # seconds to avoid overloading the reader that called us with output,
334 # which can slow busy readers down.
335 if (time.time() - last_flushed_at) > 10:
336 last_flushed_at = time.time()
337 stdout.flush()
338 in_byte = kid.stdout.read(1)
339 # Flush the rest of buffered output. This is only an issue with
340 # stdout/stderr not ending with a \n.
341 if len(in_line):
342 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000343 rv = kid.wait()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000344 if rv:
maruel@chromium.org7b194c12010-09-07 20:57:09 +0000345 raise CheckCallError(args, kwargs.get('cwd', None), rv, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000346 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000347
348
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000349def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000350 """Tries to find the gclient root."""
351 path = os.path.realpath(from_dir)
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000352 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000353 split_path = os.path.split(path)
354 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000355 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000356 path = split_path[0]
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000357 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000358 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000359
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000360
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000361def PathDifference(root, subpath):
362 """Returns the difference subpath minus root."""
363 root = os.path.realpath(root)
364 subpath = os.path.realpath(subpath)
365 if not subpath.startswith(root):
366 return None
367 # If the root does not have a trailing \ or /, we add it so the returned
368 # path starts immediately after the seperator regardless of whether it is
369 # provided.
370 root = os.path.join(root, '')
371 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000372
373
374def FindFileUpwards(filename, path=None):
375 """Search upwards from the a directory (default: current) to find a file."""
376 if not path:
377 path = os.getcwd()
378 path = os.path.realpath(path)
379 while True:
380 file_path = os.path.join(path, filename)
381 if os.path.isfile(file_path):
382 return file_path
383 (new_path, _) = os.path.split(path)
384 if new_path == path:
385 return None
386 path = new_path
387
388
389def GetGClientRootAndEntries(path=None):
390 """Returns the gclient root and the dict of entries."""
391 config_file = '.gclient_entries'
392 config_path = FindFileUpwards(config_file, path)
393
394 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000395 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000396 return None
397
398 env = {}
399 execfile(config_path, env)
400 config_dir = os.path.dirname(config_path)
401 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000402
403
404class WorkItem(object):
405 """One work item."""
406 # A list of string, each being a WorkItem name.
407 requirements = []
408 # A unique string representing this work item.
409 name = None
410
411 def run(self):
412 pass
413
414
415class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000416 """Runs a set of WorkItem that have interdependencies and were WorkItem are
417 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000418
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000419 In gclient's case, Dependencies sometime needs to be run out of order due to
420 From() keyword. This class manages that all the required dependencies are run
421 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000422
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000423 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000424 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000425 def __init__(self, jobs, progress):
426 """jobs specifies the number of concurrent tasks to allow. progress is a
427 Progress instance."""
428 # Set when a thread is done or a new item is enqueued.
429 self.ready_cond = threading.Condition()
430 # Maximum number of concurrent tasks.
431 self.jobs = jobs
432 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000433 self.queued = []
434 # List of strings representing each Dependency.name that was run.
435 self.ran = []
436 # List of items currently running.
437 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000438 # Exceptions thrown if any.
439 self.exceptions = []
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000440 self.progress = progress
441 if self.progress:
442 self.progress.update()
443
444 def enqueue(self, d):
445 """Enqueue one Dependency to be executed later once its requirements are
446 satisfied.
447 """
448 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000449 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000450 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000451 self.queued.append(d)
452 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000453 logging.debug('enqueued(%s)' % d.name)
454 if self.progress:
455 self.progress._total = total + 1
456 self.progress.update(0)
457 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000458 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000459 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000460
461 def flush(self, *args, **kwargs):
462 """Runs all enqueued items until all are executed."""
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000463 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000464 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000465 while True:
466 # Check for task to run first, then wait.
467 while True:
468 if self.exceptions:
469 # Systematically flush the queue when there is an exception logged
470 # in.
471 self.queued = []
472 # Flush threads that have terminated.
473 self.running = [t for t in self.running if t.isAlive()]
474 if not self.queued and not self.running:
475 break
476 if self.jobs == len(self.running):
477 break
478 for i in xrange(len(self.queued)):
479 # Verify its requirements.
480 for r in self.queued[i].requirements:
481 if not r in self.ran:
482 # Requirement not met.
483 break
484 else:
485 # Start one work item: all its requirements are satisfied.
486 d = self.queued.pop(i)
487 new_thread = self._Worker(self, d, args=args, kwargs=kwargs)
488 if self.jobs > 1:
489 # Start the thread.
490 self.running.append(new_thread)
491 new_thread.start()
492 else:
493 # Run the 'thread' inside the main thread.
494 new_thread.run()
495 break
496 else:
497 # Couldn't find an item that could run. Break out the outher loop.
498 break
499 if not self.queued and not self.running:
500 break
501 # We need to poll here otherwise Ctrl-C isn't processed.
502 self.ready_cond.wait(10)
503 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000504 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000505 self.ready_cond.release()
506 assert not self.running, 'Now guaranteed to be single-threaded'
507 if self.exceptions:
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000508 # To get back the stack location correctly, the raise a, b, c form must be
509 # used, passing a tuple as the first argument doesn't work.
510 e = self.exceptions.pop(0)
511 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000512 if self.progress:
513 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000514
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000515 class _Worker(threading.Thread):
516 """One thread to execute one WorkItem."""
517 def __init__(self, parent, item, args=(), kwargs=None):
518 threading.Thread.__init__(self, name=item.name or 'Worker')
519 self.args = args
520 self.kwargs = kwargs or {}
521 self.item = item
522 self.parent = parent
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000523
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000524 def run(self):
525 """Runs in its own thread."""
526 logging.debug('running(%s)' % self.item.name)
527 exception = None
528 try:
529 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000530 except Exception:
531 # Catch exception location.
532 exception = sys.exc_info()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000533
534 # This assumes the following code won't throw an exception. Bad.
535 self.parent.ready_cond.acquire()
536 try:
537 if exception:
538 self.parent.exceptions.append(exception)
539 if self.parent.progress:
540 self.parent.progress.update(1)
541 assert not self.item.name in self.parent.ran
542 if not self.item.name in self.parent.ran:
543 self.parent.ran.append(self.item.name)
544 finally:
545 self.parent.ready_cond.notifyAll()
546 self.parent.ready_cond.release()