blob: ecc5eac52feb3ff6f25a709d4a1342a2aab558b7 [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.org3742c842010-09-09 19:27:14 +000017import copy
maruel@chromium.org167b9e62009-09-17 17:41:02 +000018import errno
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000019import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000020import os
maruel@chromium.org3742c842010-09-09 19:27:14 +000021import Queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000022import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000023import stat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000024import subprocess
25import sys
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000026import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000027import time
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000028import xml.dom.minidom
maruel@chromium.org167b9e62009-09-17 17:41:02 +000029import xml.parsers.expat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000030
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000031
maruel@chromium.org66c83e62010-09-07 14:18:45 +000032class Error(Exception):
33 """gclient exception class."""
34 pass
35
36
37class CheckCallError(OSError, Error):
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000038 """CheckCall() returned non-0."""
maruel@chromium.org66c83e62010-09-07 14:18:45 +000039 def __init__(self, command, cwd, returncode, stdout, stderr=None):
40 OSError.__init__(self, command, cwd, returncode, stdout, stderr)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +000041 Error.__init__(self, command)
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000042 self.command = command
43 self.cwd = cwd
maruel@chromium.org66c83e62010-09-07 14:18:45 +000044 self.returncode = returncode
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000045 self.stdout = stdout
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000046 self.stderr = stderr
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000047
maruel@chromium.org7b194c12010-09-07 20:57:09 +000048 def __str__(self):
49 out = ' '.join(self.command)
50 if self.cwd:
51 out += ' in ' + self.cwd
52 if self.returncode is not None:
53 out += ' returned %d' % self.returncode
54 if self.stdout is not None:
55 out += '\nstdout: %s\n' % self.stdout
56 if self.stderr is not None:
57 out += '\nstderr: %s\n' % self.stderr
58 return out
59
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000060
maruel@chromium.orga1693be2010-09-03 19:09:35 +000061def Popen(args, **kwargs):
maruel@chromium.org3a292682010-08-23 18:54:55 +000062 """Calls subprocess.Popen() with hacks to work around certain behaviors.
63
64 Ensure English outpout for svn and make it work reliably on Windows.
65 """
maruel@chromium.orga1693be2010-09-03 19:09:35 +000066 logging.debug(u'%s, cwd=%s' % (u' '.join(args), kwargs.get('cwd', '')))
maruel@chromium.org3a292682010-08-23 18:54:55 +000067 if not 'env' in kwargs:
maruel@chromium.org3a292682010-08-23 18:54:55 +000068 # It's easier to parse the stdout if it is always in English.
69 kwargs['env'] = os.environ.copy()
70 kwargs['env']['LANGUAGE'] = 'en'
71 if not 'shell' in kwargs:
maruel@chromium.org3a292682010-08-23 18:54:55 +000072 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
73 # executable, but shell=True makes subprocess on Linux fail when it's called
74 # with a list because it only tries to execute the first item in the list.
75 kwargs['shell'] = (sys.platform=='win32')
maruel@chromium.orga1693be2010-09-03 19:09:35 +000076 return subprocess.Popen(args, **kwargs)
maruel@chromium.org3a292682010-08-23 18:54:55 +000077
78
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000079def CheckCall(command, cwd=None, print_error=True):
maruel@chromium.org3a292682010-08-23 18:54:55 +000080 """Similar subprocess.check_call() but redirects stdout and
81 returns (stdout, stderr).
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000082
83 Works on python 2.4
84 """
maruel@chromium.org18111352009-12-20 17:21:28 +000085 try:
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000086 stderr = None
87 if not print_error:
88 stderr = subprocess.PIPE
maruel@chromium.org3a292682010-08-23 18:54:55 +000089 process = Popen(command, cwd=cwd, stdout=subprocess.PIPE, stderr=stderr)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000090 std_out, std_err = process.communicate()
maruel@chromium.org18111352009-12-20 17:21:28 +000091 except OSError, e:
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +000092 raise CheckCallError(command, cwd, e.errno, None)
maruel@chromium.org18111352009-12-20 17:21:28 +000093 if process.returncode:
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000094 raise CheckCallError(command, cwd, process.returncode, std_out, std_err)
95 return std_out, std_err
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000096
97
msb@chromium.orgac915bb2009-11-13 17:03:01 +000098def SplitUrlRevision(url):
99 """Splits url and returns a two-tuple: url, rev"""
100 if url.startswith('ssh:'):
101 # Make sure ssh://test@example.com/test.git@stable works
maruel@chromium.org116704f2010-06-11 17:34:38 +0000102 regex = r'(ssh://(?:[\w]+@)?[-\w:\.]+/[-\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000103 components = re.search(regex, url).groups()
104 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000105 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000106 if len(components) == 1:
107 components += [None]
108 return tuple(components)
109
110
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000111def ParseXML(output):
112 try:
113 return xml.dom.minidom.parseString(output)
114 except xml.parsers.expat.ExpatError:
115 return None
116
117
118def GetNamedNodeText(node, node_name):
119 child_nodes = node.getElementsByTagName(node_name)
120 if not child_nodes:
121 return None
122 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
123 return child_nodes[0].firstChild.nodeValue
124
125
126def GetNodeNamedAttributeText(node, node_name, attribute_name):
127 child_nodes = node.getElementsByTagName(node_name)
128 if not child_nodes:
129 return None
130 assert len(child_nodes) == 1
131 return child_nodes[0].getAttribute(attribute_name)
132
133
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000134def SyntaxErrorToError(filename, e):
135 """Raises a gclient_utils.Error exception with the human readable message"""
136 try:
137 # Try to construct a human readable error message
138 if filename:
139 error_message = 'There is a syntax error in %s\n' % filename
140 else:
141 error_message = 'There is a syntax error\n'
142 error_message += 'Line #%s, character %s: "%s"' % (
143 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
144 except:
145 # Something went wrong, re-raise the original exception
146 raise e
147 else:
148 raise Error(error_message)
149
150
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000151class PrintableObject(object):
152 def __str__(self):
153 output = ''
154 for i in dir(self):
155 if i.startswith('__'):
156 continue
157 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
158 return output
159
160
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000161def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000162 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000163 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000164 try:
165 content = f.read()
166 finally:
167 f.close()
168 return content
169
170
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000171def FileWrite(filename, content, mode='w'):
172 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000173 try:
174 f.write(content)
175 finally:
176 f.close()
177
178
179def RemoveDirectory(*path):
180 """Recursively removes a directory, even if it's marked read-only.
181
182 Remove the directory located at *path, if it exists.
183
184 shutil.rmtree() doesn't work on Windows if any of the files or directories
185 are read-only, which svn repositories and some .svn files are. We need to
186 be able to force the files to be writable (i.e., deletable) as we traverse
187 the tree.
188
189 Even with all this, Windows still sometimes fails to delete a file, citing
190 a permission error (maybe something to do with antivirus scans or disk
191 indexing). The best suggestion any of the user forums had was to wait a
192 bit and try again, so we do that too. It's hand-waving, but sometimes it
193 works. :/
194
195 On POSIX systems, things are a little bit simpler. The modes of the files
196 to be deleted doesn't matter, only the modes of the directories containing
197 them are significant. As the directory tree is traversed, each directory
198 has its mode set appropriately before descending into it. This should
199 result in the entire tree being removed, with the possible exception of
200 *path itself, because nothing attempts to change the mode of its parent.
201 Doing so would be hazardous, as it's not a directory slated for removal.
202 In the ordinary case, this is not a problem: for our purposes, the user
203 will never lack write permission on *path's parent.
204 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000205 logging.debug(path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000206 file_path = os.path.join(*path)
207 if not os.path.exists(file_path):
208 return
209
210 if os.path.islink(file_path) or not os.path.isdir(file_path):
maruel@chromium.org116704f2010-06-11 17:34:38 +0000211 raise Error('RemoveDirectory asked to remove non-directory %s' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000212
213 has_win32api = False
214 if sys.platform == 'win32':
215 has_win32api = True
216 # Some people don't have the APIs installed. In that case we'll do without.
217 try:
218 win32api = __import__('win32api')
219 win32con = __import__('win32con')
220 except ImportError:
221 has_win32api = False
222 else:
223 # On POSIX systems, we need the x-bit set on the directory to access it,
224 # the r-bit to see its contents, and the w-bit to remove files from it.
225 # The actual modes of the files within the directory is irrelevant.
226 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
227 for fn in os.listdir(file_path):
228 fullpath = os.path.join(file_path, fn)
229
230 # If fullpath is a symbolic link that points to a directory, isdir will
231 # be True, but we don't want to descend into that as a directory, we just
232 # want to remove the link. Check islink and treat links as ordinary files
233 # would be treated regardless of what they reference.
234 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
235 if sys.platform == 'win32':
236 os.chmod(fullpath, stat.S_IWRITE)
237 if has_win32api:
238 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
239 try:
240 os.remove(fullpath)
241 except OSError, e:
242 if e.errno != errno.EACCES or sys.platform != 'win32':
243 raise
244 print 'Failed to delete %s: trying again' % fullpath
245 time.sleep(0.1)
246 os.remove(fullpath)
247 else:
248 RemoveDirectory(fullpath)
249
250 if sys.platform == 'win32':
251 os.chmod(file_path, stat.S_IWRITE)
252 if has_win32api:
253 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
254 try:
255 os.rmdir(file_path)
256 except OSError, e:
257 if e.errno != errno.EACCES or sys.platform != 'win32':
258 raise
259 print 'Failed to remove %s: trying again' % file_path
260 time.sleep(0.1)
261 os.rmdir(file_path)
262
263
maruel@chromium.org17d01792010-09-01 18:07:10 +0000264def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
265 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000266
maruel@chromium.org17d01792010-09-01 18:07:10 +0000267 If |always| is True, a message indicating what is being done
268 is printed to stdout all the time even if not output is generated. Otherwise
269 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000270 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000271 stdout = kwargs.get('stdout', None) or sys.stdout
272 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000273 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000274 % (' '.join(args), kwargs.get('cwd', '.')))
275 else:
276 filter_fn = kwargs.get('filter_fn', None)
277 def filter_msg(line):
278 if line is None:
279 stdout.write('\n________ running \'%s\' in \'%s\'\n'
280 % (' '.join(args), kwargs.get('cwd', '.')))
281 elif filter_fn:
282 filter_fn(line)
283 kwargs['filter_fn'] = filter_msg
284 kwargs['call_filter_on_first_line'] = True
285 # Obviously.
286 kwargs['print_stdout'] = True
287 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000288
maruel@chromium.org17d01792010-09-01 18:07:10 +0000289
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000290class StdoutAutoFlush(object):
291 """Automatically flush after N seconds."""
292 def __init__(self, stdout, delay=10):
293 self.lock = threading.Lock()
294 self.stdout = stdout
295 self.delay = delay
296 self.last_flushed_at = time.time()
297 self.stdout.flush()
298
299 def write(self, out):
300 """Thread-safe."""
301 self.stdout.write(out)
302 should_flush = False
maruel@chromium.org9c531262010-09-08 13:41:13 +0000303 self.lock.acquire()
304 try:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000305 if (time.time() - self.last_flushed_at) > self.delay:
306 should_flush = True
307 self.last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000308 finally:
309 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000310 if should_flush:
311 self.stdout.flush()
312
313 def flush(self):
314 self.stdout.flush()
315
316
maruel@chromium.org17d01792010-09-01 18:07:10 +0000317def CheckCallAndFilter(args, stdout=None, filter_fn=None,
318 print_stdout=None, call_filter_on_first_line=False,
319 **kwargs):
320 """Runs a command and calls back a filter function if needed.
321
322 Accepts all subprocess.Popen() parameters plus:
323 print_stdout: If True, the command's stdout is forwarded to stdout.
324 filter_fn: A function taking a single string argument called with each line
325 of the subprocess's output. Each line has the trailing newline
326 character trimmed.
327 stdout: Can be any bufferable output.
328
329 stderr is always redirected to stdout.
330 """
331 assert print_stdout or filter_fn
332 stdout = stdout or sys.stdout
333 filter_fn = filter_fn or (lambda x: None)
334 assert not 'stderr' in kwargs
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000335 kid = Popen(args, bufsize=0,
336 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
337 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000338
maruel@chromium.org17d01792010-09-01 18:07:10 +0000339 # Do a flush of stdout before we begin reading from the subprocess's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000340 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000341
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000342 # Also, we need to forward stdout to prevent weird re-ordering of output.
343 # This has to be done on a per byte basis to make sure it is not buffered:
344 # normally buffering is done for each line, but if svn requests input, no
345 # end-of-line character is output after the prompt and it would not show up.
346 in_byte = kid.stdout.read(1)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000347 if in_byte:
348 if call_filter_on_first_line:
349 filter_fn(None)
350 in_line = ''
351 while in_byte:
352 if in_byte != '\r':
353 if print_stdout:
354 stdout.write(in_byte)
355 if in_byte != '\n':
356 in_line += in_byte
357 else:
358 filter_fn(in_line)
359 in_line = ''
maruel@chromium.org17d01792010-09-01 18:07:10 +0000360 in_byte = kid.stdout.read(1)
361 # Flush the rest of buffered output. This is only an issue with
362 # stdout/stderr not ending with a \n.
363 if len(in_line):
364 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000365 rv = kid.wait()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000366 if rv:
maruel@chromium.org7b194c12010-09-07 20:57:09 +0000367 raise CheckCallError(args, kwargs.get('cwd', None), rv, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000368 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000369
370
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000371def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000372 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000373 real_from_dir = os.path.realpath(from_dir)
374 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000375 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000376 split_path = os.path.split(path)
377 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000378 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000379 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000380
381 # If we did not find the file in the current directory, make sure we are in a
382 # sub directory that is controlled by this configuration.
383 if path != real_from_dir:
384 entries_filename = os.path.join(path, filename + '_entries')
385 if not os.path.exists(entries_filename):
386 # If .gclient_entries does not exist, a previous call to gclient sync
387 # might have failed. In that case, we cannot verify that the .gclient
388 # is the one we want to use. In order to not to cause too much trouble,
389 # just issue a warning and return the path anyway.
390 print >>sys.stderr, ("%s file in parent directory %s might not be the "
391 "file you want to use" % (filename, path))
392 return path
393 scope = {}
394 try:
395 exec(FileRead(entries_filename), scope)
396 except SyntaxError, e:
397 SyntaxErrorToError(filename, e)
398 all_directories = scope['entries'].keys()
399 path_to_check = real_from_dir[len(path)+1:]
400 while path_to_check:
401 if path_to_check in all_directories:
402 return path
403 path_to_check = os.path.dirname(path_to_check)
404 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000405
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000406 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000407 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000408
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000409
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000410def PathDifference(root, subpath):
411 """Returns the difference subpath minus root."""
412 root = os.path.realpath(root)
413 subpath = os.path.realpath(subpath)
414 if not subpath.startswith(root):
415 return None
416 # If the root does not have a trailing \ or /, we add it so the returned
417 # path starts immediately after the seperator regardless of whether it is
418 # provided.
419 root = os.path.join(root, '')
420 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000421
422
423def FindFileUpwards(filename, path=None):
424 """Search upwards from the a directory (default: current) to find a file."""
425 if not path:
426 path = os.getcwd()
427 path = os.path.realpath(path)
428 while True:
429 file_path = os.path.join(path, filename)
430 if os.path.isfile(file_path):
431 return file_path
432 (new_path, _) = os.path.split(path)
433 if new_path == path:
434 return None
435 path = new_path
436
437
438def GetGClientRootAndEntries(path=None):
439 """Returns the gclient root and the dict of entries."""
440 config_file = '.gclient_entries'
441 config_path = FindFileUpwards(config_file, path)
442
443 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000444 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000445 return None
446
447 env = {}
448 execfile(config_path, env)
449 config_dir = os.path.dirname(config_path)
450 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000451
452
453class WorkItem(object):
454 """One work item."""
455 # A list of string, each being a WorkItem name.
456 requirements = []
457 # A unique string representing this work item.
458 name = None
459
maruel@chromium.org3742c842010-09-09 19:27:14 +0000460 def run(self, work_queue, options):
461 """work_queue and options are passed as keyword arguments so they should be
462 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000463 pass
464
465
466class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000467 """Runs a set of WorkItem that have interdependencies and were WorkItem are
468 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000469
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000470 In gclient's case, Dependencies sometime needs to be run out of order due to
471 From() keyword. This class manages that all the required dependencies are run
472 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000473
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000474 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000475 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000476 def __init__(self, jobs, progress):
477 """jobs specifies the number of concurrent tasks to allow. progress is a
478 Progress instance."""
479 # Set when a thread is done or a new item is enqueued.
480 self.ready_cond = threading.Condition()
481 # Maximum number of concurrent tasks.
482 self.jobs = jobs
483 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000484 self.queued = []
485 # List of strings representing each Dependency.name that was run.
486 self.ran = []
487 # List of items currently running.
488 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000489 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000490 self.exceptions = Queue.Queue()
491 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000492 self.progress = progress
493 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000494 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000495
496 def enqueue(self, d):
497 """Enqueue one Dependency to be executed later once its requirements are
498 satisfied.
499 """
500 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000501 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000502 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000503 self.queued.append(d)
504 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000505 logging.debug('enqueued(%s)' % d.name)
506 if self.progress:
507 self.progress._total = total + 1
508 self.progress.update(0)
509 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000510 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000511 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000512
513 def flush(self, *args, **kwargs):
514 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000515 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000516 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000517 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000518 while True:
519 # Check for task to run first, then wait.
520 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000521 if not self.exceptions.empty():
522 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000523 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000524 self._flush_terminated_threads()
525 if (not self.queued and not self.running or
526 self.jobs == len(self.running)):
527 # No more worker threads or can't queue anything.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000528 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000529
530 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000531 for i in xrange(len(self.queued)):
532 # Verify its requirements.
533 for r in self.queued[i].requirements:
534 if not r in self.ran:
535 # Requirement not met.
536 break
537 else:
538 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000539 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000540 break
541 else:
542 # Couldn't find an item that could run. Break out the outher loop.
543 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000544
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000545 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000546 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000547 break
548 # We need to poll here otherwise Ctrl-C isn't processed.
549 self.ready_cond.wait(10)
550 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000551 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000552 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000553
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000554 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000555 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000556 # To get back the stack location correctly, the raise a, b, c form must be
557 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000558 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000559 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000560 if self.progress:
561 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000562
maruel@chromium.org3742c842010-09-09 19:27:14 +0000563 def _flush_terminated_threads(self):
564 """Flush threads that have terminated."""
565 running = self.running
566 self.running = []
567 for t in running:
568 if t.isAlive():
569 self.running.append(t)
570 else:
571 t.join()
572 t.kwargs['options'].stdout.flush()
573 if self.progress:
574 self.progress.update(1)
575 assert not t.name in self.ran
576 if not t.name in self.ran:
577 self.ran.append(t.name)
578
579 def _run_one_task(self, task_item, args, kwargs):
580 if self.jobs > 1:
581 # Start the thread.
582 index = len(self.ran) + len(self.running) + 1
583 # Copy 'options' just to be safe.
584 task_kwargs = kwargs.copy()
585 task_kwargs['options'] = copy.copy(task_kwargs['options'])
586 new_thread = self._Worker(task_item, args, task_kwargs)
587 self.running.append(new_thread)
588 new_thread.start()
589 else:
590 # Run the 'thread' inside the main thread. Don't try to catch any
591 # exception.
592 task_item.run(*args, **kwargs)
593 self.ran.append(task_item.name)
594 if self.progress:
595 self.progress.update(1)
596
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000597 class _Worker(threading.Thread):
598 """One thread to execute one WorkItem."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000599 def __init__(self, item, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000600 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org3742c842010-09-09 19:27:14 +0000601 logging.info(item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000602 self.item = item
maruel@chromium.org3742c842010-09-09 19:27:14 +0000603 self.args = args
604 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000605
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000606 def run(self):
607 """Runs in its own thread."""
608 logging.debug('running(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000609 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000610 try:
611 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000612 except Exception:
613 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000614 logging.info('Caught exception in thread %s' % self.item.name)
615 logging.info(str(sys.exc_info()))
616 work_queue.exceptions.put(sys.exc_info())
617 logging.info('Task %s done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000618
maruel@chromium.org3742c842010-09-09 19:27:14 +0000619 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000620 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000621 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000622 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000623 work_queue.ready_cond.release()