blob: d2ba580c11739d68e40543038102f18b6ca9e523 [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)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +000039 Error.__init__(self, command)
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
maruel@chromium.org9c531262010-09-08 13:41:13 +0000301 self.lock.acquire()
302 try:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000303 if (time.time() - self.last_flushed_at) > self.delay:
304 should_flush = True
305 self.last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000306 finally:
307 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000308 if should_flush:
309 self.stdout.flush()
310
311 def flush(self):
312 self.stdout.flush()
313
314
maruel@chromium.org17d01792010-09-01 18:07:10 +0000315def CheckCallAndFilter(args, stdout=None, filter_fn=None,
316 print_stdout=None, call_filter_on_first_line=False,
317 **kwargs):
318 """Runs a command and calls back a filter function if needed.
319
320 Accepts all subprocess.Popen() parameters plus:
321 print_stdout: If True, the command's stdout is forwarded to stdout.
322 filter_fn: A function taking a single string argument called with each line
323 of the subprocess's output. Each line has the trailing newline
324 character trimmed.
325 stdout: Can be any bufferable output.
326
327 stderr is always redirected to stdout.
328 """
329 assert print_stdout or filter_fn
330 stdout = stdout or sys.stdout
331 filter_fn = filter_fn or (lambda x: None)
332 assert not 'stderr' in kwargs
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000333 kid = Popen(args, bufsize=0,
334 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
335 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000336
maruel@chromium.org17d01792010-09-01 18:07:10 +0000337 # Do a flush of stdout before we begin reading from the subprocess's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000338 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000339
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000340 # Also, we need to forward stdout to prevent weird re-ordering of output.
341 # This has to be done on a per byte basis to make sure it is not buffered:
342 # normally buffering is done for each line, but if svn requests input, no
343 # end-of-line character is output after the prompt and it would not show up.
344 in_byte = kid.stdout.read(1)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000345 if in_byte:
346 if call_filter_on_first_line:
347 filter_fn(None)
348 in_line = ''
349 while in_byte:
350 if in_byte != '\r':
351 if print_stdout:
352 stdout.write(in_byte)
353 if in_byte != '\n':
354 in_line += in_byte
355 else:
356 filter_fn(in_line)
357 in_line = ''
maruel@chromium.org17d01792010-09-01 18:07:10 +0000358 in_byte = kid.stdout.read(1)
359 # Flush the rest of buffered output. This is only an issue with
360 # stdout/stderr not ending with a \n.
361 if len(in_line):
362 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000363 rv = kid.wait()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000364 if rv:
maruel@chromium.org7b194c12010-09-07 20:57:09 +0000365 raise CheckCallError(args, kwargs.get('cwd', None), rv, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000366 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000367
368
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000369def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000370 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000371 real_from_dir = os.path.realpath(from_dir)
372 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000373 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000374 split_path = os.path.split(path)
375 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000376 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000377 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000378
379 # If we did not find the file in the current directory, make sure we are in a
380 # sub directory that is controlled by this configuration.
381 if path != real_from_dir:
382 entries_filename = os.path.join(path, filename + '_entries')
383 if not os.path.exists(entries_filename):
384 # If .gclient_entries does not exist, a previous call to gclient sync
385 # might have failed. In that case, we cannot verify that the .gclient
386 # is the one we want to use. In order to not to cause too much trouble,
387 # just issue a warning and return the path anyway.
388 print >>sys.stderr, ("%s file in parent directory %s might not be the "
389 "file you want to use" % (filename, path))
390 return path
391 scope = {}
392 try:
393 exec(FileRead(entries_filename), scope)
394 except SyntaxError, e:
395 SyntaxErrorToError(filename, e)
396 all_directories = scope['entries'].keys()
397 path_to_check = real_from_dir[len(path)+1:]
398 while path_to_check:
399 if path_to_check in all_directories:
400 return path
401 path_to_check = os.path.dirname(path_to_check)
402 return None
403
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000404 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000405 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000406
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000407
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000408def PathDifference(root, subpath):
409 """Returns the difference subpath minus root."""
410 root = os.path.realpath(root)
411 subpath = os.path.realpath(subpath)
412 if not subpath.startswith(root):
413 return None
414 # If the root does not have a trailing \ or /, we add it so the returned
415 # path starts immediately after the seperator regardless of whether it is
416 # provided.
417 root = os.path.join(root, '')
418 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000419
420
421def FindFileUpwards(filename, path=None):
422 """Search upwards from the a directory (default: current) to find a file."""
423 if not path:
424 path = os.getcwd()
425 path = os.path.realpath(path)
426 while True:
427 file_path = os.path.join(path, filename)
428 if os.path.isfile(file_path):
429 return file_path
430 (new_path, _) = os.path.split(path)
431 if new_path == path:
432 return None
433 path = new_path
434
435
436def GetGClientRootAndEntries(path=None):
437 """Returns the gclient root and the dict of entries."""
438 config_file = '.gclient_entries'
439 config_path = FindFileUpwards(config_file, path)
440
441 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000442 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000443 return None
444
445 env = {}
446 execfile(config_path, env)
447 config_dir = os.path.dirname(config_path)
448 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000449
450
451class WorkItem(object):
452 """One work item."""
453 # A list of string, each being a WorkItem name.
454 requirements = []
455 # A unique string representing this work item.
456 name = None
457
458 def run(self):
459 pass
460
461
462class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000463 """Runs a set of WorkItem that have interdependencies and were WorkItem are
464 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000465
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000466 In gclient's case, Dependencies sometime needs to be run out of order due to
467 From() keyword. This class manages that all the required dependencies are run
468 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000469
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000470 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000471 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000472 def __init__(self, jobs, progress):
473 """jobs specifies the number of concurrent tasks to allow. progress is a
474 Progress instance."""
475 # Set when a thread is done or a new item is enqueued.
476 self.ready_cond = threading.Condition()
477 # Maximum number of concurrent tasks.
478 self.jobs = jobs
479 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000480 self.queued = []
481 # List of strings representing each Dependency.name that was run.
482 self.ran = []
483 # List of items currently running.
484 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000485 # Exceptions thrown if any.
486 self.exceptions = []
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000487 self.progress = progress
488 if self.progress:
489 self.progress.update()
490
491 def enqueue(self, d):
492 """Enqueue one Dependency to be executed later once its requirements are
493 satisfied.
494 """
495 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000496 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000497 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000498 self.queued.append(d)
499 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000500 logging.debug('enqueued(%s)' % d.name)
501 if self.progress:
502 self.progress._total = total + 1
503 self.progress.update(0)
504 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000505 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000506 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000507
508 def flush(self, *args, **kwargs):
509 """Runs all enqueued items until all are executed."""
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000510 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000511 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000512 while True:
513 # Check for task to run first, then wait.
514 while True:
515 if self.exceptions:
516 # Systematically flush the queue when there is an exception logged
517 # in.
518 self.queued = []
519 # Flush threads that have terminated.
520 self.running = [t for t in self.running if t.isAlive()]
521 if not self.queued and not self.running:
522 break
523 if self.jobs == len(self.running):
524 break
525 for i in xrange(len(self.queued)):
526 # Verify its requirements.
527 for r in self.queued[i].requirements:
528 if not r in self.ran:
529 # Requirement not met.
530 break
531 else:
532 # Start one work item: all its requirements are satisfied.
533 d = self.queued.pop(i)
534 new_thread = self._Worker(self, d, args=args, kwargs=kwargs)
535 if self.jobs > 1:
536 # Start the thread.
537 self.running.append(new_thread)
538 new_thread.start()
539 else:
540 # Run the 'thread' inside the main thread.
541 new_thread.run()
542 break
543 else:
544 # Couldn't find an item that could run. Break out the outher loop.
545 break
546 if not self.queued and not self.running:
547 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()
553 assert not self.running, 'Now guaranteed to be single-threaded'
554 if self.exceptions:
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000555 # To get back the stack location correctly, the raise a, b, c form must be
556 # used, passing a tuple as the first argument doesn't work.
557 e = self.exceptions.pop(0)
558 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000559 if self.progress:
560 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000561
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000562 class _Worker(threading.Thread):
563 """One thread to execute one WorkItem."""
564 def __init__(self, parent, item, args=(), kwargs=None):
565 threading.Thread.__init__(self, name=item.name or 'Worker')
566 self.args = args
567 self.kwargs = kwargs or {}
568 self.item = item
569 self.parent = parent
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000570
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000571 def run(self):
572 """Runs in its own thread."""
573 logging.debug('running(%s)' % self.item.name)
574 exception = None
575 try:
576 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000577 except Exception:
578 # Catch exception location.
579 exception = sys.exc_info()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000580
581 # This assumes the following code won't throw an exception. Bad.
582 self.parent.ready_cond.acquire()
583 try:
584 if exception:
585 self.parent.exceptions.append(exception)
586 if self.parent.progress:
587 self.parent.progress.update(1)
588 assert not self.item.name in self.parent.ran
589 if not self.item.name in self.parent.ran:
590 self.parent.ran.append(self.item.name)
591 finally:
592 self.parent.ready_cond.notifyAll()
593 self.parent.ready_cond.release()