blob: d0fbb1ef73431b4980f929b735aee80a8a3f8c16 [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
maruel@chromium.org3742c842010-09-09 19:27:14 +000020import Queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000021import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000022import stat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000023import subprocess
24import sys
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000025import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000026import time
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000027import xml.dom.minidom
maruel@chromium.org167b9e62009-09-17 17:41:02 +000028import xml.parsers.expat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000029
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000030
maruel@chromium.org66c83e62010-09-07 14:18:45 +000031class Error(Exception):
32 """gclient exception class."""
33 pass
34
35
36class CheckCallError(OSError, Error):
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000037 """CheckCall() returned non-0."""
maruel@chromium.org66c83e62010-09-07 14:18:45 +000038 def __init__(self, command, cwd, returncode, stdout, stderr=None):
39 OSError.__init__(self, command, cwd, returncode, stdout, stderr)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +000040 Error.__init__(self, command)
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000041 self.command = command
42 self.cwd = cwd
maruel@chromium.org66c83e62010-09-07 14:18:45 +000043 self.returncode = returncode
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000044 self.stdout = stdout
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000045 self.stderr = stderr
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000046
maruel@chromium.org7b194c12010-09-07 20:57:09 +000047 def __str__(self):
48 out = ' '.join(self.command)
49 if self.cwd:
50 out += ' in ' + self.cwd
51 if self.returncode is not None:
52 out += ' returned %d' % self.returncode
53 if self.stdout is not None:
54 out += '\nstdout: %s\n' % self.stdout
55 if self.stderr is not None:
56 out += '\nstderr: %s\n' % self.stderr
57 return out
58
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000059
maruel@chromium.orga1693be2010-09-03 19:09:35 +000060def Popen(args, **kwargs):
maruel@chromium.org3a292682010-08-23 18:54:55 +000061 """Calls subprocess.Popen() with hacks to work around certain behaviors.
62
63 Ensure English outpout for svn and make it work reliably on Windows.
64 """
maruel@chromium.orga1693be2010-09-03 19:09:35 +000065 logging.debug(u'%s, cwd=%s' % (u' '.join(args), kwargs.get('cwd', '')))
maruel@chromium.org3a292682010-08-23 18:54:55 +000066 if not 'env' in kwargs:
maruel@chromium.org3a292682010-08-23 18:54:55 +000067 # It's easier to parse the stdout if it is always in English.
68 kwargs['env'] = os.environ.copy()
69 kwargs['env']['LANGUAGE'] = 'en'
70 if not 'shell' in kwargs:
maruel@chromium.org3a292682010-08-23 18:54:55 +000071 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
72 # executable, but shell=True makes subprocess on Linux fail when it's called
73 # with a list because it only tries to execute the first item in the list.
74 kwargs['shell'] = (sys.platform=='win32')
maruel@chromium.org8aba5f72010-09-16 19:48:59 +000075 try:
76 return subprocess.Popen(args, **kwargs)
77 except OSError, e:
78 if e.errno == errno.EAGAIN and sys.platform == 'cygwin':
79 raise Error(
80 'Visit '
81 'http://code.google.com/p/chromium/wiki/CygwinDllRemappingFailure to '
82 'learn how to fix this error; you need to rebase your cygwin dlls')
83 raise
maruel@chromium.org3a292682010-08-23 18:54:55 +000084
85
maruel@chromium.orgac610232010-10-13 14:01:31 +000086def CheckCall(command, print_error=True, **kwargs):
maruel@chromium.org3a292682010-08-23 18:54:55 +000087 """Similar subprocess.check_call() but redirects stdout and
88 returns (stdout, stderr).
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000089
90 Works on python 2.4
91 """
maruel@chromium.org18111352009-12-20 17:21:28 +000092 try:
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000093 stderr = None
94 if not print_error:
95 stderr = subprocess.PIPE
maruel@chromium.orgac610232010-10-13 14:01:31 +000096 process = Popen(command, stdout=subprocess.PIPE, stderr=stderr, **kwargs)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000097 std_out, std_err = process.communicate()
maruel@chromium.org18111352009-12-20 17:21:28 +000098 except OSError, e:
maruel@chromium.orgac610232010-10-13 14:01:31 +000099 raise CheckCallError(command, kwargs.get('cwd', None), e.errno, None)
maruel@chromium.org18111352009-12-20 17:21:28 +0000100 if process.returncode:
maruel@chromium.orgac610232010-10-13 14:01:31 +0000101 raise CheckCallError(command, kwargs.get('cwd', None), process.returncode,
102 std_out, std_err)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000103 return std_out, std_err
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +0000104
105
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000106def SplitUrlRevision(url):
107 """Splits url and returns a two-tuple: url, rev"""
108 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +0000109 # Make sure ssh://user-name@example.com/~/test.git@stable works
110 regex = r'(ssh://(?:[-\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000111 components = re.search(regex, url).groups()
112 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000113 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000114 if len(components) == 1:
115 components += [None]
116 return tuple(components)
117
118
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000119def ParseXML(output):
120 try:
121 return xml.dom.minidom.parseString(output)
122 except xml.parsers.expat.ExpatError:
123 return None
124
125
126def GetNamedNodeText(node, node_name):
127 child_nodes = node.getElementsByTagName(node_name)
128 if not child_nodes:
129 return None
130 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
131 return child_nodes[0].firstChild.nodeValue
132
133
134def GetNodeNamedAttributeText(node, node_name, attribute_name):
135 child_nodes = node.getElementsByTagName(node_name)
136 if not child_nodes:
137 return None
138 assert len(child_nodes) == 1
139 return child_nodes[0].getAttribute(attribute_name)
140
141
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000142def SyntaxErrorToError(filename, e):
143 """Raises a gclient_utils.Error exception with the human readable message"""
144 try:
145 # Try to construct a human readable error message
146 if filename:
147 error_message = 'There is a syntax error in %s\n' % filename
148 else:
149 error_message = 'There is a syntax error\n'
150 error_message += 'Line #%s, character %s: "%s"' % (
151 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
152 except:
153 # Something went wrong, re-raise the original exception
154 raise e
155 else:
156 raise Error(error_message)
157
158
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000159class PrintableObject(object):
160 def __str__(self):
161 output = ''
162 for i in dir(self):
163 if i.startswith('__'):
164 continue
165 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
166 return output
167
168
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000169def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000170 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000171 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000172 try:
173 content = f.read()
174 finally:
175 f.close()
176 return content
177
178
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000179def FileWrite(filename, content, mode='w'):
180 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000181 try:
182 f.write(content)
183 finally:
184 f.close()
185
186
187def RemoveDirectory(*path):
188 """Recursively removes a directory, even if it's marked read-only.
189
190 Remove the directory located at *path, if it exists.
191
192 shutil.rmtree() doesn't work on Windows if any of the files or directories
193 are read-only, which svn repositories and some .svn files are. We need to
194 be able to force the files to be writable (i.e., deletable) as we traverse
195 the tree.
196
197 Even with all this, Windows still sometimes fails to delete a file, citing
198 a permission error (maybe something to do with antivirus scans or disk
199 indexing). The best suggestion any of the user forums had was to wait a
200 bit and try again, so we do that too. It's hand-waving, but sometimes it
201 works. :/
202
203 On POSIX systems, things are a little bit simpler. The modes of the files
204 to be deleted doesn't matter, only the modes of the directories containing
205 them are significant. As the directory tree is traversed, each directory
206 has its mode set appropriately before descending into it. This should
207 result in the entire tree being removed, with the possible exception of
208 *path itself, because nothing attempts to change the mode of its parent.
209 Doing so would be hazardous, as it's not a directory slated for removal.
210 In the ordinary case, this is not a problem: for our purposes, the user
211 will never lack write permission on *path's parent.
212 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000213 logging.debug(path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000214 file_path = os.path.join(*path)
215 if not os.path.exists(file_path):
216 return
217
218 if os.path.islink(file_path) or not os.path.isdir(file_path):
maruel@chromium.org116704f2010-06-11 17:34:38 +0000219 raise Error('RemoveDirectory asked to remove non-directory %s' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000220
221 has_win32api = False
222 if sys.platform == 'win32':
223 has_win32api = True
224 # Some people don't have the APIs installed. In that case we'll do without.
225 try:
226 win32api = __import__('win32api')
227 win32con = __import__('win32con')
228 except ImportError:
229 has_win32api = False
230 else:
231 # On POSIX systems, we need the x-bit set on the directory to access it,
232 # the r-bit to see its contents, and the w-bit to remove files from it.
233 # The actual modes of the files within the directory is irrelevant.
234 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
235 for fn in os.listdir(file_path):
236 fullpath = os.path.join(file_path, fn)
237
238 # If fullpath is a symbolic link that points to a directory, isdir will
239 # be True, but we don't want to descend into that as a directory, we just
240 # want to remove the link. Check islink and treat links as ordinary files
241 # would be treated regardless of what they reference.
242 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
243 if sys.platform == 'win32':
244 os.chmod(fullpath, stat.S_IWRITE)
245 if has_win32api:
246 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
247 try:
248 os.remove(fullpath)
249 except OSError, e:
250 if e.errno != errno.EACCES or sys.platform != 'win32':
251 raise
252 print 'Failed to delete %s: trying again' % fullpath
253 time.sleep(0.1)
254 os.remove(fullpath)
255 else:
256 RemoveDirectory(fullpath)
257
258 if sys.platform == 'win32':
259 os.chmod(file_path, stat.S_IWRITE)
260 if has_win32api:
261 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
262 try:
263 os.rmdir(file_path)
264 except OSError, e:
265 if e.errno != errno.EACCES or sys.platform != 'win32':
266 raise
267 print 'Failed to remove %s: trying again' % file_path
268 time.sleep(0.1)
269 os.rmdir(file_path)
270
271
maruel@chromium.org17d01792010-09-01 18:07:10 +0000272def CheckCallAndFilterAndHeader(args, always=False, **kwargs):
273 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000274
maruel@chromium.org17d01792010-09-01 18:07:10 +0000275 If |always| is True, a message indicating what is being done
276 is printed to stdout all the time even if not output is generated. Otherwise
277 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000278 """
maruel@chromium.org17d01792010-09-01 18:07:10 +0000279 stdout = kwargs.get('stdout', None) or sys.stdout
280 if always:
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000281 stdout.write('\n________ running \'%s\' in \'%s\'\n'
maruel@chromium.org17d01792010-09-01 18:07:10 +0000282 % (' '.join(args), kwargs.get('cwd', '.')))
283 else:
284 filter_fn = kwargs.get('filter_fn', None)
285 def filter_msg(line):
286 if line is None:
287 stdout.write('\n________ running \'%s\' in \'%s\'\n'
288 % (' '.join(args), kwargs.get('cwd', '.')))
289 elif filter_fn:
290 filter_fn(line)
291 kwargs['filter_fn'] = filter_msg
292 kwargs['call_filter_on_first_line'] = True
293 # Obviously.
294 kwargs['print_stdout'] = True
295 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000296
maruel@chromium.org17d01792010-09-01 18:07:10 +0000297
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000298def SoftClone(obj):
299 """Clones an object. copy.copy() doesn't work on 'file' objects."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000300 if obj.__class__.__name__ == 'SoftCloned':
301 return obj
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000302 class SoftCloned(object):
303 pass
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000304 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
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000319 # Attribute 'XXX' defined outside __init__
320 # pylint: disable=W0201
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000321 new_fileobj = SoftClone(fileobj)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000322 if not hasattr(new_fileobj, 'lock'):
323 new_fileobj.lock = threading.Lock()
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000324 new_fileobj.last_flushed_at = time.time()
325 new_fileobj.delay = delay
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000326 new_fileobj.old_auto_flush_write = new_fileobj.write
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000327 # Silence pylint.
328 new_fileobj.flush = fileobj.flush
329
330 def auto_flush_write(out):
331 new_fileobj.old_auto_flush_write(out)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000332 should_flush = False
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000333 new_fileobj.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000334 try:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000335 if (new_fileobj.delay and
336 (time.time() - new_fileobj.last_flushed_at) > new_fileobj.delay):
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000337 should_flush = True
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000338 new_fileobj.last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000339 finally:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000340 new_fileobj.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000341 if should_flush:
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000342 new_fileobj.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000343
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000344 new_fileobj.write = auto_flush_write
345 return new_fileobj
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000346
347
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000348def MakeFileAnnotated(fileobj):
349 """Creates a file object clone to automatically prepends every line in worker
350 threads with a NN> prefix."""
351 if hasattr(fileobj, 'output_buffers'):
352 # Already patched.
353 return fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000354
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000355 # Attribute 'XXX' defined outside __init__
356 # pylint: disable=W0201
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000357 new_fileobj = SoftClone(fileobj)
358 if not hasattr(new_fileobj, 'lock'):
359 new_fileobj.lock = threading.Lock()
360 new_fileobj.output_buffers = {}
361 new_fileobj.old_annotated_write = new_fileobj.write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000362
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000363 def annotated_write(out):
364 index = getattr(threading.currentThread(), 'index', None)
365 if index is None:
366 # Undexed threads aren't buffered.
367 new_fileobj.old_annotated_write(out)
368 return
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000369
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000370 new_fileobj.lock.acquire()
371 try:
372 # Use a dummy array to hold the string so the code can be lockless.
373 # Strings are immutable, requiring to keep a lock for the whole dictionary
374 # otherwise. Using an array is faster than using a dummy object.
375 if not index in new_fileobj.output_buffers:
376 obj = new_fileobj.output_buffers[index] = ['']
377 else:
378 obj = new_fileobj.output_buffers[index]
379 finally:
380 new_fileobj.lock.release()
381
382 # Continue lockless.
383 obj[0] += out
384 while '\n' in obj[0]:
385 line, remaining = obj[0].split('\n', 1)
386 new_fileobj.old_annotated_write('%d>%s\n' % (index, line))
387 obj[0] = remaining
388
389 def full_flush():
390 """Flush buffered output."""
391 orphans = []
392 new_fileobj.lock.acquire()
393 try:
394 # Detect threads no longer existing.
395 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000396 indexes = filter(None, indexes)
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000397 for index in new_fileobj.output_buffers:
398 if not index in indexes:
399 orphans.append((index, new_fileobj.output_buffers[index][0]))
400 for orphan in orphans:
401 del new_fileobj.output_buffers[orphan[0]]
402 finally:
403 new_fileobj.lock.release()
404
405 # Don't keep the lock while writting. Will append \n when it shouldn't.
406 for orphan in orphans:
407 new_fileobj.old_annotated_write('%d>%s\n' % (orphan[0], orphan[1]))
408
409 new_fileobj.write = annotated_write
410 new_fileobj.full_flush = full_flush
411 return new_fileobj
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000412
413
maruel@chromium.org17d01792010-09-01 18:07:10 +0000414def CheckCallAndFilter(args, stdout=None, filter_fn=None,
415 print_stdout=None, call_filter_on_first_line=False,
416 **kwargs):
417 """Runs a command and calls back a filter function if needed.
418
419 Accepts all subprocess.Popen() parameters plus:
420 print_stdout: If True, the command's stdout is forwarded to stdout.
421 filter_fn: A function taking a single string argument called with each line
422 of the subprocess's output. Each line has the trailing newline
423 character trimmed.
424 stdout: Can be any bufferable output.
425
426 stderr is always redirected to stdout.
427 """
428 assert print_stdout or filter_fn
429 stdout = stdout or sys.stdout
430 filter_fn = filter_fn or (lambda x: None)
431 assert not 'stderr' in kwargs
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000432 kid = Popen(args, bufsize=0,
433 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
434 **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000435
maruel@chromium.org17d01792010-09-01 18:07:10 +0000436 # Do a flush of stdout before we begin reading from the subprocess's stdout
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000437 stdout.flush()
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000438
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000439 # Also, we need to forward stdout to prevent weird re-ordering of output.
440 # This has to be done on a per byte basis to make sure it is not buffered:
441 # normally buffering is done for each line, but if svn requests input, no
442 # end-of-line character is output after the prompt and it would not show up.
443 in_byte = kid.stdout.read(1)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000444 if in_byte:
445 if call_filter_on_first_line:
446 filter_fn(None)
447 in_line = ''
448 while in_byte:
449 if in_byte != '\r':
450 if print_stdout:
451 stdout.write(in_byte)
452 if in_byte != '\n':
453 in_line += in_byte
454 else:
455 filter_fn(in_line)
456 in_line = ''
maruel@chromium.org17d01792010-09-01 18:07:10 +0000457 in_byte = kid.stdout.read(1)
458 # Flush the rest of buffered output. This is only an issue with
459 # stdout/stderr not ending with a \n.
460 if len(in_line):
461 filter_fn(in_line)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000462 rv = kid.wait()
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000463 if rv:
maruel@chromium.org7b194c12010-09-07 20:57:09 +0000464 raise CheckCallError(args, kwargs.get('cwd', None), rv, None)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000465 return 0
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000466
467
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000468def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000469 """Tries to find the gclient root."""
jochen@chromium.org20760a52010-09-08 08:47:28 +0000470 real_from_dir = os.path.realpath(from_dir)
471 path = real_from_dir
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000472 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.org3a292682010-08-23 18:54:55 +0000473 split_path = os.path.split(path)
474 if not split_path[1]:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000475 return None
maruel@chromium.org3a292682010-08-23 18:54:55 +0000476 path = split_path[0]
jochen@chromium.org20760a52010-09-08 08:47:28 +0000477
478 # If we did not find the file in the current directory, make sure we are in a
479 # sub directory that is controlled by this configuration.
480 if path != real_from_dir:
481 entries_filename = os.path.join(path, filename + '_entries')
482 if not os.path.exists(entries_filename):
483 # If .gclient_entries does not exist, a previous call to gclient sync
484 # might have failed. In that case, we cannot verify that the .gclient
485 # is the one we want to use. In order to not to cause too much trouble,
486 # just issue a warning and return the path anyway.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000487 print >> sys.stderr, ("%s file in parent directory %s might not be the "
jochen@chromium.org20760a52010-09-08 08:47:28 +0000488 "file you want to use" % (filename, path))
489 return path
490 scope = {}
491 try:
492 exec(FileRead(entries_filename), scope)
493 except SyntaxError, e:
494 SyntaxErrorToError(filename, e)
495 all_directories = scope['entries'].keys()
496 path_to_check = real_from_dir[len(path)+1:]
497 while path_to_check:
498 if path_to_check in all_directories:
499 return path
500 path_to_check = os.path.dirname(path_to_check)
501 return None
maruel@chromium.org3742c842010-09-09 19:27:14 +0000502
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000503 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000504 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000505
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000506
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000507def PathDifference(root, subpath):
508 """Returns the difference subpath minus root."""
509 root = os.path.realpath(root)
510 subpath = os.path.realpath(subpath)
511 if not subpath.startswith(root):
512 return None
513 # If the root does not have a trailing \ or /, we add it so the returned
514 # path starts immediately after the seperator regardless of whether it is
515 # provided.
516 root = os.path.join(root, '')
517 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000518
519
520def FindFileUpwards(filename, path=None):
521 """Search upwards from the a directory (default: current) to find a file."""
522 if not path:
523 path = os.getcwd()
524 path = os.path.realpath(path)
525 while True:
526 file_path = os.path.join(path, filename)
527 if os.path.isfile(file_path):
528 return file_path
529 (new_path, _) = os.path.split(path)
530 if new_path == path:
531 return None
532 path = new_path
533
534
535def GetGClientRootAndEntries(path=None):
536 """Returns the gclient root and the dict of entries."""
537 config_file = '.gclient_entries'
538 config_path = FindFileUpwards(config_file, path)
539
540 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000541 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000542 return None
543
544 env = {}
545 execfile(config_path, env)
546 config_dir = os.path.dirname(config_path)
547 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000548
549
550class WorkItem(object):
551 """One work item."""
552 # A list of string, each being a WorkItem name.
553 requirements = []
554 # A unique string representing this work item.
555 name = None
556
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000557 def run(self, work_queue):
558 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000559 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000560 pass
561
562
563class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000564 """Runs a set of WorkItem that have interdependencies and were WorkItem are
565 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000566
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000567 In gclient's case, Dependencies sometime needs to be run out of order due to
568 From() keyword. This class manages that all the required dependencies are run
569 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000570
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000571 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000572 """
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000573 def __init__(self, jobs, progress):
574 """jobs specifies the number of concurrent tasks to allow. progress is a
575 Progress instance."""
576 # Set when a thread is done or a new item is enqueued.
577 self.ready_cond = threading.Condition()
578 # Maximum number of concurrent tasks.
579 self.jobs = jobs
580 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000581 self.queued = []
582 # List of strings representing each Dependency.name that was run.
583 self.ran = []
584 # List of items currently running.
585 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000586 # Exceptions thrown if any.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000587 self.exceptions = Queue.Queue()
588 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000589 self.progress = progress
590 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000591 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000592
593 def enqueue(self, d):
594 """Enqueue one Dependency to be executed later once its requirements are
595 satisfied.
596 """
597 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000598 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000599 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000600 self.queued.append(d)
601 total = len(self.queued) + len(self.ran) + len(self.running)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000602 logging.debug('enqueued(%s)' % d.name)
603 if self.progress:
604 self.progress._total = total + 1
605 self.progress.update(0)
606 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000607 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000608 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000609
610 def flush(self, *args, **kwargs):
611 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000612 kwargs['work_queue'] = self
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000613 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000614 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000615 while True:
616 # Check for task to run first, then wait.
617 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000618 if not self.exceptions.empty():
619 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000620 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000621 self._flush_terminated_threads()
622 if (not self.queued and not self.running or
623 self.jobs == len(self.running)):
624 # No more worker threads or can't queue anything.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000625 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000626
627 # Check for new tasks to start.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000628 for i in xrange(len(self.queued)):
629 # Verify its requirements.
630 for r in self.queued[i].requirements:
631 if not r in self.ran:
632 # Requirement not met.
633 break
634 else:
635 # Start one work item: all its requirements are satisfied.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000636 self._run_one_task(self.queued.pop(i), args, kwargs)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000637 break
638 else:
639 # Couldn't find an item that could run. Break out the outher loop.
640 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000641
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000642 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000643 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000644 break
645 # We need to poll here otherwise Ctrl-C isn't processed.
646 self.ready_cond.wait(10)
647 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000648 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000649 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000650
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000651 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000652 if not self.exceptions.empty():
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000653 # To get back the stack location correctly, the raise a, b, c form must be
654 # used, passing a tuple as the first argument doesn't work.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000655 e = self.exceptions.get()
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000656 raise e[0], e[1], e[2]
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000657 if self.progress:
658 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000659
maruel@chromium.org3742c842010-09-09 19:27:14 +0000660 def _flush_terminated_threads(self):
661 """Flush threads that have terminated."""
662 running = self.running
663 self.running = []
664 for t in running:
665 if t.isAlive():
666 self.running.append(t)
667 else:
668 t.join()
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000669 sys.stdout.full_flush()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000670 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000671 self.progress.update(1, t.item.name)
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000672 assert not t.item.name in self.ran
673 if not t.item.name in self.ran:
674 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000675
676 def _run_one_task(self, task_item, args, kwargs):
677 if self.jobs > 1:
678 # Start the thread.
679 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000680 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000681 self.running.append(new_thread)
682 new_thread.start()
683 else:
684 # Run the 'thread' inside the main thread. Don't try to catch any
685 # exception.
686 task_item.run(*args, **kwargs)
687 self.ran.append(task_item.name)
688 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000689 self.progress.update(1, ', '.join(t.item.name for t in self.running))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000690
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000691 class _Worker(threading.Thread):
692 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000693 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000694 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org3742c842010-09-09 19:27:14 +0000695 logging.info(item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000696 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000697 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000698 self.args = args
699 self.kwargs = kwargs
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000700
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000701 def run(self):
702 """Runs in its own thread."""
703 logging.debug('running(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000704 work_queue = self.kwargs['work_queue']
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000705 # It's necessary to catch all exceptions.
706 # pylint: disable=W0703
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000707 try:
708 self.item.run(*self.args, **self.kwargs)
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000709 except Exception:
710 # Catch exception location.
maruel@chromium.org3742c842010-09-09 19:27:14 +0000711 logging.info('Caught exception in thread %s' % self.item.name)
712 logging.info(str(sys.exc_info()))
713 work_queue.exceptions.put(sys.exc_info())
714 logging.info('Task %s done' % self.item.name)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000715
maruel@chromium.org3742c842010-09-09 19:27:14 +0000716 work_queue.ready_cond.acquire()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000717 try:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000718 work_queue.ready_cond.notifyAll()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000719 finally:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000720 work_queue.ready_cond.release()