blob: d21ade51b0015943efcf310b674250911beeb155 [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."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000351 real_from_dir = os.path.realpath(from_dir)
352 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000353 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000354 split_path = os.path.split(path)
355 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000356 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000357 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000358
359 # If we did not find the file in the current directory, make sure we are in a
360 # sub directory that is controlled by this configuration.
361 if path != real_from_dir:
362 entries_filename = os.path.join(path, filename + '_entries')
363 if not os.path.exists(entries_filename):
364 # If .gclient_entries does not exist, a previous call to gclient sync
365 # might have failed. In that case, we cannot verify that the .gclient
366 # is the one we want to use. In order to not to cause too much trouble,
367 # just issue a warning and return the path anyway.
368 print >>sys.stderr, ("%s file in parent directory %s might not be the "
369 "file you want to use" % (filename, path))
370 return path
371 scope = {}
372 try:
373 exec(FileRead(entries_filename), scope)
374 except SyntaxError, e:
375 SyntaxErrorToError(filename, e)
376 all_directories = scope['entries'].keys()
377 path_to_check = real_from_dir[len(path)+1:]
378 while path_to_check:
379 if path_to_check in all_directories:
380 return path
381 path_to_check = os.path.dirname(path_to_check)
382 return None
383
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000384 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000385 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000386
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000387
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000388def PathDifference(root, subpath):
389 """Returns the difference subpath minus root."""
390 root = os.path.realpath(root)
391 subpath = os.path.realpath(subpath)
392 if not subpath.startswith(root):
393 return None
394 # If the root does not have a trailing \ or /, we add it so the returned
395 # path starts immediately after the seperator regardless of whether it is
396 # provided.
397 root = os.path.join(root, '')
398 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000399
400
401def FindFileUpwards(filename, path=None):
402 """Search upwards from the a directory (default: current) to find a file."""
403 if not path:
404 path = os.getcwd()
405 path = os.path.realpath(path)
406 while True:
407 file_path = os.path.join(path, filename)
408 if os.path.isfile(file_path):
409 return file_path
410 (new_path, _) = os.path.split(path)
411 if new_path == path:
412 return None
413 path = new_path
414
415
416def GetGClientRootAndEntries(path=None):
417 """Returns the gclient root and the dict of entries."""
418 config_file = '.gclient_entries'
419 config_path = FindFileUpwards(config_file, path)
420
421 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000422 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000423 return None
424
425 env = {}
426 execfile(config_path, env)
427 config_dir = os.path.dirname(config_path)
428 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000429
430
431class WorkItem(object):
432 """One work item."""
433 # A list of string, each being a WorkItem name.
434 requirements = []
435 # A unique string representing this work item.
436 name = None
437
438 def run(self):
439 pass
440
441
442class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000443 """Runs a set of WorkItem that have interdependencies and were WorkItem are
444 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000445
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000446 In gclient's case, Dependencies sometime needs to be run out of order due to
447 From() keyword. This class manages that all the required dependencies are run
448 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000449
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000450 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000451 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000452 def __init__(self, jobs, progress):
453 """jobs specifies the number of concurrent tasks to allow. progress is a
454 Progress instance."""
455 # Set when a thread is done or a new item is enqueued.
456 self.ready_cond = threading.Condition()
457 # Maximum number of concurrent tasks.
458 self.jobs = jobs
459 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000460 self.queued = []
461 # List of strings representing each Dependency.name that was run.
462 self.ran = []
463 # List of items currently running.
464 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000465 # Exceptions thrown if any.
466 self.exceptions = []
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000467 self.progress = progress
468 if self.progress:
469 self.progress.update()
470
471 def enqueue(self, d):
472 """Enqueue one Dependency to be executed later once its requirements are
473 satisfied.
474 """
475 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000476 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000477 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000478 self.queued.append(d)
479 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000480 logging.debug('enqueued(%s)' % d.name)
481 if self.progress:
482 self.progress._total = total + 1
483 self.progress.update(0)
484 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000485 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000486 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000487
488 def flush(self, *args, **kwargs):
489 """Runs all enqueued items until all are executed."""
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000490 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000491 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000492 while True:
493 # Check for task to run first, then wait.
494 while True:
495 if self.exceptions:
496 # Systematically flush the queue when there is an exception logged
497 # in.
498 self.queued = []
499 # Flush threads that have terminated.
500 self.running = [t for t in self.running if t.isAlive()]
501 if not self.queued and not self.running:
502 break
503 if self.jobs == len(self.running):
504 break
505 for i in xrange(len(self.queued)):
506 # Verify its requirements.
507 for r in self.queued[i].requirements:
508 if not r in self.ran:
509 # Requirement not met.
510 break
511 else:
512 # Start one work item: all its requirements are satisfied.
513 d = self.queued.pop(i)
514 new_thread = self._Worker(self, d, args=args, kwargs=kwargs)
515 if self.jobs > 1:
516 # Start the thread.
517 self.running.append(new_thread)
518 new_thread.start()
519 else:
520 # Run the 'thread' inside the main thread.
521 new_thread.run()
522 break
523 else:
524 # Couldn't find an item that could run. Break out the outher loop.
525 break
526 if not self.queued and not self.running:
527 break
528 # We need to poll here otherwise Ctrl-C isn't processed.
529 self.ready_cond.wait(10)
530 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000531 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000532 self.ready_cond.release()
533 assert not self.running, 'Now guaranteed to be single-threaded'
534 if self.exceptions:
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000535 # To get back the stack location correctly, the raise a, b, c form must be
536 # used, passing a tuple as the first argument doesn't work.
537 e = self.exceptions.pop(0)
538 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000539 if self.progress:
540 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000541
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000542 class _Worker(threading.Thread):
543 """One thread to execute one WorkItem."""
544 def __init__(self, parent, item, args=(), kwargs=None):
545 threading.Thread.__init__(self, name=item.name or 'Worker')
546 self.args = args
547 self.kwargs = kwargs or {}
548 self.item = item
549 self.parent = parent
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000550
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000551 def run(self):
552 """Runs in its own thread."""
553 logging.debug('running(%s)' % self.item.name)
554 exception = None
555 try:
556 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000557 except Exception:
558 # Catch exception location.
559 exception = sys.exc_info()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000560
561 # This assumes the following code won't throw an exception. Bad.
562 self.parent.ready_cond.acquire()
563 try:
564 if exception:
565 self.parent.exceptions.append(exception)
566 if self.parent.progress:
567 self.parent.progress.update(1)
568 assert not self.item.name in self.parent.ran
569 if not self.item.name in self.parent.ran:
570 self.parent.ran.append(self.item.name)
571 finally:
572 self.parent.ready_cond.notifyAll()
573 self.parent.ready_cond.release()