blob: 9d14bd2554889e0d9bce345657990bca19b22b01 [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
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000288class StdoutAutoFlush(object):
289 """Automatically flush after N seconds."""
290 def __init__(self, stdout, delay=10):
291 self.lock = threading.Lock()
292 self.stdout = stdout
293 self.delay = delay
294 self.last_flushed_at = time.time()
295 self.stdout.flush()
296
297 def write(self, out):
298 """Thread-safe."""
299 self.stdout.write(out)
300 should_flush = False
301 with self.lock:
302 if (time.time() - self.last_flushed_at) > self.delay:
303 should_flush = True
304 self.last_flushed_at = time.time()
305 if should_flush:
306 self.stdout.flush()
307
308 def flush(self):
309 self.stdout.flush()
310
311
maruel@chromium.org17d01792010-09-01 18:07:10 +0000312def CheckCallAndFilter(args, stdout=None, filter_fn=None,
313 print_stdout=None, call_filter_on_first_line=False,
314 **kwargs):
315 """Runs a command and calls back a filter function if needed.
316
317 Accepts all subprocess.Popen() parameters plus:
318 print_stdout: If True, the command's stdout is forwarded to stdout.
319 filter_fn: A function taking a single string argument called with each line
320 of the subprocess's output. Each line has the trailing newline
321 character trimmed.
322 stdout: Can be any bufferable output.
323
324 stderr is always redirected to stdout.
325 """
326 assert print_stdout or filter_fn
327 stdout = stdout or sys.stdout
328 filter_fn = filter_fn or (lambda x: None)
329 assert not 'stderr' in kwargs
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000330 kid = Popen(args, bufsize=0,
331 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
332 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000333
maruel@chromium.org17d01792010-09-01 18:07:10 +0000334 # Do a flush of stdout before we begin reading from the subprocess's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000335 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000336
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000337 # Also, we need to forward stdout to prevent weird re-ordering of output.
338 # This has to be done on a per byte basis to make sure it is not buffered:
339 # normally buffering is done for each line, but if svn requests input, no
340 # end-of-line character is output after the prompt and it would not show up.
341 in_byte = kid.stdout.read(1)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000342 if in_byte:
343 if call_filter_on_first_line:
344 filter_fn(None)
345 in_line = ''
346 while in_byte:
347 if in_byte != '\r':
348 if print_stdout:
349 stdout.write(in_byte)
350 if in_byte != '\n':
351 in_line += in_byte
352 else:
353 filter_fn(in_line)
354 in_line = ''
maruel@chromium.org17d01792010-09-01 18:07:10 +0000355 in_byte = kid.stdout.read(1)
356 # Flush the rest of buffered output. This is only an issue with
357 # stdout/stderr not ending with a \n.
358 if len(in_line):
359 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000360 rv = kid.wait()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000361 if rv:
maruel@chromium.org7b194c12010-09-07 20:57:09 +0000362 raise CheckCallError(args, kwargs.get('cwd', None), rv, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000363 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000364
365
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000366def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000367 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000368 real_from_dir = os.path.realpath(from_dir)
369 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000370 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000371 split_path = os.path.split(path)
372 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000373 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000374 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000375
376 # If we did not find the file in the current directory, make sure we are in a
377 # sub directory that is controlled by this configuration.
378 if path != real_from_dir:
379 entries_filename = os.path.join(path, filename + '_entries')
380 if not os.path.exists(entries_filename):
381 # If .gclient_entries does not exist, a previous call to gclient sync
382 # might have failed. In that case, we cannot verify that the .gclient
383 # is the one we want to use. In order to not to cause too much trouble,
384 # just issue a warning and return the path anyway.
385 print >>sys.stderr, ("%s file in parent directory %s might not be the "
386 "file you want to use" % (filename, path))
387 return path
388 scope = {}
389 try:
390 exec(FileRead(entries_filename), scope)
391 except SyntaxError, e:
392 SyntaxErrorToError(filename, e)
393 all_directories = scope['entries'].keys()
394 path_to_check = real_from_dir[len(path)+1:]
395 while path_to_check:
396 if path_to_check in all_directories:
397 return path
398 path_to_check = os.path.dirname(path_to_check)
399 return None
400
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000401 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000402 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000403
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000404
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000405def PathDifference(root, subpath):
406 """Returns the difference subpath minus root."""
407 root = os.path.realpath(root)
408 subpath = os.path.realpath(subpath)
409 if not subpath.startswith(root):
410 return None
411 # If the root does not have a trailing \ or /, we add it so the returned
412 # path starts immediately after the seperator regardless of whether it is
413 # provided.
414 root = os.path.join(root, '')
415 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000416
417
418def FindFileUpwards(filename, path=None):
419 """Search upwards from the a directory (default: current) to find a file."""
420 if not path:
421 path = os.getcwd()
422 path = os.path.realpath(path)
423 while True:
424 file_path = os.path.join(path, filename)
425 if os.path.isfile(file_path):
426 return file_path
427 (new_path, _) = os.path.split(path)
428 if new_path == path:
429 return None
430 path = new_path
431
432
433def GetGClientRootAndEntries(path=None):
434 """Returns the gclient root and the dict of entries."""
435 config_file = '.gclient_entries'
436 config_path = FindFileUpwards(config_file, path)
437
438 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000439 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000440 return None
441
442 env = {}
443 execfile(config_path, env)
444 config_dir = os.path.dirname(config_path)
445 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000446
447
448class WorkItem(object):
449 """One work item."""
450 # A list of string, each being a WorkItem name.
451 requirements = []
452 # A unique string representing this work item.
453 name = None
454
455 def run(self):
456 pass
457
458
459class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000460 """Runs a set of WorkItem that have interdependencies and were WorkItem are
461 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000462
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000463 In gclient's case, Dependencies sometime needs to be run out of order due to
464 From() keyword. This class manages that all the required dependencies are run
465 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000466
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000467 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000468 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000469 def __init__(self, jobs, progress):
470 """jobs specifies the number of concurrent tasks to allow. progress is a
471 Progress instance."""
472 # Set when a thread is done or a new item is enqueued.
473 self.ready_cond = threading.Condition()
474 # Maximum number of concurrent tasks.
475 self.jobs = jobs
476 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000477 self.queued = []
478 # List of strings representing each Dependency.name that was run.
479 self.ran = []
480 # List of items currently running.
481 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000482 # Exceptions thrown if any.
483 self.exceptions = []
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000484 self.progress = progress
485 if self.progress:
486 self.progress.update()
487
488 def enqueue(self, d):
489 """Enqueue one Dependency to be executed later once its requirements are
490 satisfied.
491 """
492 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000493 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000494 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000495 self.queued.append(d)
496 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000497 logging.debug('enqueued(%s)' % d.name)
498 if self.progress:
499 self.progress._total = total + 1
500 self.progress.update(0)
501 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000502 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000503 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000504
505 def flush(self, *args, **kwargs):
506 """Runs all enqueued items until all are executed."""
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000507 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000508 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000509 while True:
510 # Check for task to run first, then wait.
511 while True:
512 if self.exceptions:
513 # Systematically flush the queue when there is an exception logged
514 # in.
515 self.queued = []
516 # Flush threads that have terminated.
517 self.running = [t for t in self.running if t.isAlive()]
518 if not self.queued and not self.running:
519 break
520 if self.jobs == len(self.running):
521 break
522 for i in xrange(len(self.queued)):
523 # Verify its requirements.
524 for r in self.queued[i].requirements:
525 if not r in self.ran:
526 # Requirement not met.
527 break
528 else:
529 # Start one work item: all its requirements are satisfied.
530 d = self.queued.pop(i)
531 new_thread = self._Worker(self, d, args=args, kwargs=kwargs)
532 if self.jobs > 1:
533 # Start the thread.
534 self.running.append(new_thread)
535 new_thread.start()
536 else:
537 # Run the 'thread' inside the main thread.
538 new_thread.run()
539 break
540 else:
541 # Couldn't find an item that could run. Break out the outher loop.
542 break
543 if not self.queued and not self.running:
544 break
545 # We need to poll here otherwise Ctrl-C isn't processed.
546 self.ready_cond.wait(10)
547 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000548 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000549 self.ready_cond.release()
550 assert not self.running, 'Now guaranteed to be single-threaded'
551 if self.exceptions:
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000552 # To get back the stack location correctly, the raise a, b, c form must be
553 # used, passing a tuple as the first argument doesn't work.
554 e = self.exceptions.pop(0)
555 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000556 if self.progress:
557 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000558
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000559 class _Worker(threading.Thread):
560 """One thread to execute one WorkItem."""
561 def __init__(self, parent, item, args=(), kwargs=None):
562 threading.Thread.__init__(self, name=item.name or 'Worker')
563 self.args = args
564 self.kwargs = kwargs or {}
565 self.item = item
566 self.parent = parent
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000567
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000568 def run(self):
569 """Runs in its own thread."""
570 logging.debug('running(%s)' % self.item.name)
571 exception = None
572 try:
573 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000574 except Exception:
575 # Catch exception location.
576 exception = sys.exc_info()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000577
578 # This assumes the following code won't throw an exception. Bad.
579 self.parent.ready_cond.acquire()
580 try:
581 if exception:
582 self.parent.exceptions.append(exception)
583 if self.parent.progress:
584 self.parent.progress.update(1)
585 assert not self.item.name in self.parent.ran
586 if not self.item.name in self.parent.ran:
587 self.parent.ran.append(self.item.name)
588 finally:
589 self.parent.ready_cond.notifyAll()
590 self.parent.ready_cond.release()