blob: 743e57bc163ed7dfb751686e5687d8f64ebbfc29 [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.org8aba5f72010-09-16 19:48:59 +000076 try:
77 return subprocess.Popen(args, **kwargs)
78 except OSError, e:
79 if e.errno == errno.EAGAIN and sys.platform == 'cygwin':
80 raise Error(
81 'Visit '
82 'http://code.google.com/p/chromium/wiki/CygwinDllRemappingFailure to '
83 'learn how to fix this error; you need to rebase your cygwin dlls')
84 raise
maruel@chromium.org3a292682010-08-23 18:54:55 +000085
86
maruel@chromium.orgac610232010-10-13 14:01:31 +000087def CheckCall(command, print_error=True, **kwargs):
maruel@chromium.org3a292682010-08-23 18:54:55 +000088 """Similar subprocess.check_call() but redirects stdout and
89 returns (stdout, stderr).
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000090
91 Works on python 2.4
92 """
maruel@chromium.org18111352009-12-20 17:21:28 +000093 try:
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000094 stderr = None
95 if not print_error:
96 stderr = subprocess.PIPE
maruel@chromium.orgac610232010-10-13 14:01:31 +000097 process = Popen(command, stdout=subprocess.PIPE, stderr=stderr, **kwargs)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000098 std_out, std_err = process.communicate()
maruel@chromium.org18111352009-12-20 17:21:28 +000099 except OSError, e:
maruel@chromium.orgac610232010-10-13 14:01:31 +0000100 raise CheckCallError(command, kwargs.get('cwd', None), e.errno, None)
maruel@chromium.org18111352009-12-20 17:21:28 +0000101 if process.returncode:
maruel@chromium.orgac610232010-10-13 14:01:31 +0000102 raise CheckCallError(command, kwargs.get('cwd', None), process.returncode,
103 std_out, std_err)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000104 return std_out, std_err
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +0000105
106
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000107def SplitUrlRevision(url):
108 """Splits url and returns a two-tuple: url, rev"""
109 if url.startswith('ssh:'):
110 # Make sure ssh://test@example.com/test.git@stable works
maruel@chromium.org116704f2010-06-11 17:34:38 +0000111 regex = r'(ssh://(?:[\w]+@)?[-\w:\.]+/[-\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000112 components = re.search(regex, url).groups()
113 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000114 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000115 if len(components) == 1:
116 components += [None]
117 return tuple(components)
118
119
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000120def ParseXML(output):
121 try:
122 return xml.dom.minidom.parseString(output)
123 except xml.parsers.expat.ExpatError:
124 return None
125
126
127def GetNamedNodeText(node, node_name):
128 child_nodes = node.getElementsByTagName(node_name)
129 if not child_nodes:
130 return None
131 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
132 return child_nodes[0].firstChild.nodeValue
133
134
135def GetNodeNamedAttributeText(node, node_name, attribute_name):
136 child_nodes = node.getElementsByTagName(node_name)
137 if not child_nodes:
138 return None
139 assert len(child_nodes) == 1
140 return child_nodes[0].getAttribute(attribute_name)
141
142
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000143def SyntaxErrorToError(filename, e):
144 """Raises a gclient_utils.Error exception with the human readable message"""
145 try:
146 # Try to construct a human readable error message
147 if filename:
148 error_message = 'There is a syntax error in %s\n' % filename
149 else:
150 error_message = 'There is a syntax error\n'
151 error_message += 'Line #%s, character %s: "%s"' % (
152 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
153 except:
154 # Something went wrong, re-raise the original exception
155 raise e
156 else:
157 raise Error(error_message)
158
159
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000160class PrintableObject(object):
161 def __str__(self):
162 output = ''
163 for i in dir(self):
164 if i.startswith('__'):
165 continue
166 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
167 return output
168
169
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000170def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000171 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000172 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000173 try:
174 content = f.read()
175 finally:
176 f.close()
177 return content
178
179
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000180def FileWrite(filename, content, mode='w'):
181 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000182 try:
183 f.write(content)
184 finally:
185 f.close()
186
187
188def RemoveDirectory(*path):
189 """Recursively removes a directory, even if it's marked read-only.
190
191 Remove the directory located at *path, if it exists.
192
193 shutil.rmtree() doesn't work on Windows if any of the files or directories
194 are read-only, which svn repositories and some .svn files are. We need to
195 be able to force the files to be writable (i.e., deletable) as we traverse
196 the tree.
197
198 Even with all this, Windows still sometimes fails to delete a file, citing
199 a permission error (maybe something to do with antivirus scans or disk
200 indexing). The best suggestion any of the user forums had was to wait a
201 bit and try again, so we do that too. It's hand-waving, but sometimes it
202 works. :/
203
204 On POSIX systems, things are a little bit simpler. The modes of the files
205 to be deleted doesn't matter, only the modes of the directories containing
206 them are significant. As the directory tree is traversed, each directory
207 has its mode set appropriately before descending into it. This should
208 result in the entire tree being removed, with the possible exception of
209 *path itself, because nothing attempts to change the mode of its parent.
210 Doing so would be hazardous, as it's not a directory slated for removal.
211 In the ordinary case, this is not a problem: for our purposes, the user
212 will never lack write permission on *path's parent.
213 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000214 logging.debug(path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000215 file_path = os.path.join(*path)
216 if not os.path.exists(file_path):
217 return
218
219 if os.path.islink(file_path) or not os.path.isdir(file_path):
maruel@chromium.org116704f2010-06-11 17:34:38 +0000220 raise Error('RemoveDirectory asked to remove non-directory %s' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000221
222 has_win32api = False
223 if sys.platform == 'win32':
224 has_win32api = True
225 # Some people don't have the APIs installed. In that case we'll do without.
226 try:
227 win32api = __import__('win32api')
228 win32con = __import__('win32con')
229 except ImportError:
230 has_win32api = False
231 else:
232 # On POSIX systems, we need the x-bit set on the directory to access it,
233 # the r-bit to see its contents, and the w-bit to remove files from it.
234 # The actual modes of the files within the directory is irrelevant.
235 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
236 for fn in os.listdir(file_path):
237 fullpath = os.path.join(file_path, fn)
238
239 # If fullpath is a symbolic link that points to a directory, isdir will
240 # be True, but we don't want to descend into that as a directory, we just
241 # want to remove the link. Check islink and treat links as ordinary files
242 # would be treated regardless of what they reference.
243 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
244 if sys.platform == 'win32':
245 os.chmod(fullpath, stat.S_IWRITE)
246 if has_win32api:
247 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
248 try:
249 os.remove(fullpath)
250 except OSError, e:
251 if e.errno != errno.EACCES or sys.platform != 'win32':
252 raise
253 print 'Failed to delete %s: trying again' % fullpath
254 time.sleep(0.1)
255 os.remove(fullpath)
256 else:
257 RemoveDirectory(fullpath)
258
259 if sys.platform == 'win32':
260 os.chmod(file_path, stat.S_IWRITE)
261 if has_win32api:
262 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
263 try:
264 os.rmdir(file_path)
265 except OSError, e:
266 if e.errno != errno.EACCES or sys.platform != 'win32':
267 raise
268 print 'Failed to remove %s: trying again' % file_path
269 time.sleep(0.1)
270 os.rmdir(file_path)
271
272
maruel@chromium.org17d01792010-09-01 18:07:10 +0000273def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
274 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000275
maruel@chromium.org17d01792010-09-01 18:07:10 +0000276 If |always| is True, a message indicating what is being done
277 is printed to stdout all the time even if not output is generated. Otherwise
278 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000279 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000280 stdout = kwargs.get('stdout', None) or sys.stdout
281 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000282 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000283 % (' '.join(args), kwargs.get('cwd', '.')))
284 else:
285 filter_fn = kwargs.get('filter_fn', None)
286 def filter_msg(line):
287 if line is None:
288 stdout.write('\n________ running \'%s\' in \'%s\'\n'
289 % (' '.join(args), kwargs.get('cwd', '.')))
290 elif filter_fn:
291 filter_fn(line)
292 kwargs['filter_fn'] = filter_msg
293 kwargs['call_filter_on_first_line'] = True
294 # Obviously.
295 kwargs['print_stdout'] = True
296 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000297
maruel@chromium.org17d01792010-09-01 18:07:10 +0000298
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000299def SoftClone(obj):
300 """Clones an object. copy.copy() doesn't work on 'file' objects."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000301 if obj.__class__.__name__ == 'SoftCloned':
302 return obj
303 class SoftCloned(object): pass
304 new_obj = SoftCloned()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000305 for member in dir(obj):
306 if member.startswith('_'):
307 continue
308 setattr(new_obj, member, getattr(obj, member))
309 return new_obj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000310
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000311
312def MakeFileAutoFlush(fileobj, delay=10):
313 """Creates a file object clone to automatically flush after N seconds."""
314 if hasattr(fileobj, 'last_flushed_at'):
315 # Already patched. Just update delay.
316 fileobj.delay = delay
317 return fileobj
318
319 new_fileobj = SoftClone(fileobj)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000320 if not hasattr(new_fileobj, 'lock'):
321 new_fileobj.lock = threading.Lock()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000322 new_fileobj.last_flushed_at = time.time()
323 new_fileobj.delay = delay
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000324 new_fileobj.old_auto_flush_write = new_fileobj.write
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000325 # Silence pylint.
326 new_fileobj.flush = fileobj.flush
327
328 def auto_flush_write(out):
329 new_fileobj.old_auto_flush_write(out)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000330 should_flush = False
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000331 new_fileobj.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000332 try:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000333 if (new_fileobj.delay and
334 (time.time() - new_fileobj.last_flushed_at) > new_fileobj.delay):
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000335 should_flush = True
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000336 new_fileobj.last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000337 finally:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000338 new_fileobj.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000339 if should_flush:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000340 new_fileobj.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000341
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000342 new_fileobj.write = auto_flush_write
343 return new_fileobj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000344
345
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000346def MakeFileAnnotated(fileobj):
347 """Creates a file object clone to automatically prepends every line in worker
348 threads with a NN> prefix."""
349 if hasattr(fileobj, 'output_buffers'):
350 # Already patched.
351 return fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000352
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000353 new_fileobj = SoftClone(fileobj)
354 if not hasattr(new_fileobj, 'lock'):
355 new_fileobj.lock = threading.Lock()
356 new_fileobj.output_buffers = {}
357 new_fileobj.old_annotated_write = new_fileobj.write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000358
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000359 def annotated_write(out):
360 index = getattr(threading.currentThread(), 'index', None)
361 if index is None:
362 # Undexed threads aren't buffered.
363 new_fileobj.old_annotated_write(out)
364 return
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000365
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000366 new_fileobj.lock.acquire()
367 try:
368 # Use a dummy array to hold the string so the code can be lockless.
369 # Strings are immutable, requiring to keep a lock for the whole dictionary
370 # otherwise. Using an array is faster than using a dummy object.
371 if not index in new_fileobj.output_buffers:
372 obj = new_fileobj.output_buffers[index] = ['']
373 else:
374 obj = new_fileobj.output_buffers[index]
375 finally:
376 new_fileobj.lock.release()
377
378 # Continue lockless.
379 obj[0] += out
380 while '\n' in obj[0]:
381 line, remaining = obj[0].split('\n', 1)
382 new_fileobj.old_annotated_write('%d>%s\n' % (index, line))
383 obj[0] = remaining
384
385 def full_flush():
386 """Flush buffered output."""
387 orphans = []
388 new_fileobj.lock.acquire()
389 try:
390 # Detect threads no longer existing.
391 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
392 indexed = filter(None, indexes)
393 for index in new_fileobj.output_buffers:
394 if not index in indexes:
395 orphans.append((index, new_fileobj.output_buffers[index][0]))
396 for orphan in orphans:
397 del new_fileobj.output_buffers[orphan[0]]
398 finally:
399 new_fileobj.lock.release()
400
401 # Don't keep the lock while writting. Will append \n when it shouldn't.
402 for orphan in orphans:
403 new_fileobj.old_annotated_write('%d>%s\n' % (orphan[0], orphan[1]))
404
405 new_fileobj.write = annotated_write
406 new_fileobj.full_flush = full_flush
407 return new_fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000408
409
maruel@chromium.org17d01792010-09-01 18:07:10 +0000410def CheckCallAndFilter(args, stdout=None, filter_fn=None,
411 print_stdout=None, call_filter_on_first_line=False,
412 **kwargs):
413 """Runs a command and calls back a filter function if needed.
414
415 Accepts all subprocess.Popen() parameters plus:
416 print_stdout: If True, the command's stdout is forwarded to stdout.
417 filter_fn: A function taking a single string argument called with each line
418 of the subprocess's output. Each line has the trailing newline
419 character trimmed.
420 stdout: Can be any bufferable output.
421
422 stderr is always redirected to stdout.
423 """
424 assert print_stdout or filter_fn
425 stdout = stdout or sys.stdout
426 filter_fn = filter_fn or (lambda x: None)
427 assert not 'stderr' in kwargs
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000428 kid = Popen(args, bufsize=0,
429 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
430 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000431
maruel@chromium.org17d01792010-09-01 18:07:10 +0000432 # Do a flush of stdout before we begin reading from the subprocess's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000433 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000434
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000435 # Also, we need to forward stdout to prevent weird re-ordering of output.
436 # This has to be done on a per byte basis to make sure it is not buffered:
437 # normally buffering is done for each line, but if svn requests input, no
438 # end-of-line character is output after the prompt and it would not show up.
439 in_byte = kid.stdout.read(1)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000440 if in_byte:
441 if call_filter_on_first_line:
442 filter_fn(None)
443 in_line = ''
444 while in_byte:
445 if in_byte != '\r':
446 if print_stdout:
447 stdout.write(in_byte)
448 if in_byte != '\n':
449 in_line += in_byte
450 else:
451 filter_fn(in_line)
452 in_line = ''
maruel@chromium.org17d01792010-09-01 18:07:10 +0000453 in_byte = kid.stdout.read(1)
454 # Flush the rest of buffered output. This is only an issue with
455 # stdout/stderr not ending with a \n.
456 if len(in_line):
457 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000458 rv = kid.wait()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000459 if rv:
maruel@chromium.org7b194c12010-09-07 20:57:09 +0000460 raise CheckCallError(args, kwargs.get('cwd', None), rv, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000461 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000462
463
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000464def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000465 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000466 real_from_dir = os.path.realpath(from_dir)
467 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000468 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000469 split_path = os.path.split(path)
470 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000471 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000472 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000473
474 # If we did not find the file in the current directory, make sure we are in a
475 # sub directory that is controlled by this configuration.
476 if path != real_from_dir:
477 entries_filename = os.path.join(path, filename + '_entries')
478 if not os.path.exists(entries_filename):
479 # If .gclient_entries does not exist, a previous call to gclient sync
480 # might have failed. In that case, we cannot verify that the .gclient
481 # is the one we want to use. In order to not to cause too much trouble,
482 # just issue a warning and return the path anyway.
483 print >>sys.stderr, ("%s file in parent directory %s might not be the "
484 "file you want to use" % (filename, path))
485 return path
486 scope = {}
487 try:
488 exec(FileRead(entries_filename), scope)
489 except SyntaxError, e:
490 SyntaxErrorToError(filename, e)
491 all_directories = scope['entries'].keys()
492 path_to_check = real_from_dir[len(path)+1:]
493 while path_to_check:
494 if path_to_check in all_directories:
495 return path
496 path_to_check = os.path.dirname(path_to_check)
497 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000498
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000499 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000500 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000501
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000502
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000503def PathDifference(root, subpath):
504 """Returns the difference subpath minus root."""
505 root = os.path.realpath(root)
506 subpath = os.path.realpath(subpath)
507 if not subpath.startswith(root):
508 return None
509 # If the root does not have a trailing \ or /, we add it so the returned
510 # path starts immediately after the seperator regardless of whether it is
511 # provided.
512 root = os.path.join(root, '')
513 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000514
515
516def FindFileUpwards(filename, path=None):
517 """Search upwards from the a directory (default: current) to find a file."""
518 if not path:
519 path = os.getcwd()
520 path = os.path.realpath(path)
521 while True:
522 file_path = os.path.join(path, filename)
523 if os.path.isfile(file_path):
524 return file_path
525 (new_path, _) = os.path.split(path)
526 if new_path == path:
527 return None
528 path = new_path
529
530
531def GetGClientRootAndEntries(path=None):
532 """Returns the gclient root and the dict of entries."""
533 config_file = '.gclient_entries'
534 config_path = FindFileUpwards(config_file, path)
535
536 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000537 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000538 return None
539
540 env = {}
541 execfile(config_path, env)
542 config_dir = os.path.dirname(config_path)
543 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000544
545
546class WorkItem(object):
547 """One work item."""
548 # A list of string, each being a WorkItem name.
549 requirements = []
550 # A unique string representing this work item.
551 name = None
552
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000553 def run(self, work_queue):
554 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000555 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000556 pass
557
558
559class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000560 """Runs a set of WorkItem that have interdependencies and were WorkItem are
561 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000562
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000563 In gclient's case, Dependencies sometime needs to be run out of order due to
564 From() keyword. This class manages that all the required dependencies are run
565 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000566
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000567 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000568 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000569 def __init__(self, jobs, progress):
570 """jobs specifies the number of concurrent tasks to allow. progress is a
571 Progress instance."""
572 # Set when a thread is done or a new item is enqueued.
573 self.ready_cond = threading.Condition()
574 # Maximum number of concurrent tasks.
575 self.jobs = jobs
576 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000577 self.queued = []
578 # List of strings representing each Dependency.name that was run.
579 self.ran = []
580 # List of items currently running.
581 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000582 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000583 self.exceptions = Queue.Queue()
584 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000585 self.progress = progress
586 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000587 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000588
589 def enqueue(self, d):
590 """Enqueue one Dependency to be executed later once its requirements are
591 satisfied.
592 """
593 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000594 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000595 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000596 self.queued.append(d)
597 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000598 logging.debug('enqueued(%s)' % d.name)
599 if self.progress:
600 self.progress._total = total + 1
601 self.progress.update(0)
602 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000603 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000604 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000605
606 def flush(self, *args, **kwargs):
607 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000608 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000609 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000610 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000611 while True:
612 # Check for task to run first, then wait.
613 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000614 if not self.exceptions.empty():
615 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000616 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000617 self._flush_terminated_threads()
618 if (not self.queued and not self.running or
619 self.jobs == len(self.running)):
620 # No more worker threads or can't queue anything.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000621 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000622
623 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000624 for i in xrange(len(self.queued)):
625 # Verify its requirements.
626 for r in self.queued[i].requirements:
627 if not r in self.ran:
628 # Requirement not met.
629 break
630 else:
631 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000632 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000633 break
634 else:
635 # Couldn't find an item that could run. Break out the outher loop.
636 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000637
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000638 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000639 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000640 break
641 # We need to poll here otherwise Ctrl-C isn't processed.
642 self.ready_cond.wait(10)
643 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000644 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000645 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000646
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000647 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000648 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000649 # To get back the stack location correctly, the raise a, b, c form must be
650 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000651 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000652 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000653 if self.progress:
654 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000655
maruel@chromium.org3742c842010-09-09 19:27:14 +0000656 def _flush_terminated_threads(self):
657 """Flush threads that have terminated."""
658 running = self.running
659 self.running = []
660 for t in running:
661 if t.isAlive():
662 self.running.append(t)
663 else:
664 t.join()
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000665 sys.stdout.full_flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000666 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000667 self.progress.update(1, t.item.name)
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000668 assert not t.item.name in self.ran
669 if not t.item.name in self.ran:
670 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000671
672 def _run_one_task(self, task_item, args, kwargs):
673 if self.jobs > 1:
674 # Start the thread.
675 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000676 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000677 self.running.append(new_thread)
678 new_thread.start()
679 else:
680 # Run the 'thread' inside the main thread. Don't try to catch any
681 # exception.
682 task_item.run(*args, **kwargs)
683 self.ran.append(task_item.name)
684 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000685 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000686
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000687 class _Worker(threading.Thread):
688 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000689 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000690 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org3742c842010-09-09 19:27:14 +0000691 logging.info(item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000692 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000693 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000694 self.args = args
695 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000696
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000697 def run(self):
698 """Runs in its own thread."""
699 logging.debug('running(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000700 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000701 try:
702 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000703 except Exception:
704 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000705 logging.info('Caught exception in thread %s' % self.item.name)
706 logging.info(str(sys.exc_info()))
707 work_queue.exceptions.put(sys.exc_info())
708 logging.info('Task %s done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000709
maruel@chromium.org3742c842010-09-09 19:27:14 +0000710 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000711 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000712 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000713 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000714 work_queue.ready_cond.release()