1# Copyright (c) 2009, Google Inc. All rights reserved. 2# Copyright (c) 2009 Apple Inc. All rights reserved. 3# 4# Redistribution and use in source and binary forms, with or without 5# modification, are permitted provided that the following conditions are 6# met: 7# 8# * Redistributions of source code must retain the above copyright 9# notice, this list of conditions and the following disclaimer. 10# * Redistributions in binary form must reproduce the above 11# copyright notice, this list of conditions and the following disclaimer 12# in the documentation and/or other materials provided with the 13# distribution. 14# * Neither the name of Google Inc. nor the names of its 15# contributors may be used to endorse or promote products derived from 16# this software without specific prior written permission. 17# 18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 30import os 31import StringIO 32import subprocess 33import sys 34 35from webkitpy.webkit_logging import tee 36 37 38class ScriptError(Exception): 39 40 def __init__(self, 41 message=None, 42 script_args=None, 43 exit_code=None, 44 output=None, 45 cwd=None): 46 if not message: 47 message = 'Failed to run "%s"' % script_args 48 if exit_code: 49 message += " exit_code: %d" % exit_code 50 if cwd: 51 message += " cwd: %s" % cwd 52 53 Exception.__init__(self, message) 54 self.script_args = script_args # 'args' is already used by Exception 55 self.exit_code = exit_code 56 self.output = output 57 self.cwd = cwd 58 59 def message_with_output(self, output_limit=500): 60 if self.output: 61 if output_limit and len(self.output) > output_limit: 62 return "%s\nLast %s characters of output:\n%s" % \ 63 (self, output_limit, self.output[-output_limit:]) 64 return "%s\n%s" % (self, self.output) 65 return str(self) 66 67 def command_name(self): 68 command_path = self.script_args 69 if type(command_path) is list: 70 command_path = command_path[0] 71 return os.path.basename(command_path) 72 73 74def run_command(*args, **kwargs): 75 # FIXME: This should not be a global static. 76 # New code should use Executive.run_command directly instead 77 return Executive().run_command(*args, **kwargs) 78 79 80class Executive(object): 81 82 def _run_command_with_teed_output(self, args, teed_output): 83 child_process = subprocess.Popen(args, 84 stdout=subprocess.PIPE, 85 stderr=subprocess.STDOUT) 86 87 # Use our own custom wait loop because Popen ignores a tee'd 88 # stderr/stdout. 89 # FIXME: This could be improved not to flatten output to stdout. 90 while True: 91 output_line = child_process.stdout.readline() 92 if output_line == "" and child_process.poll() != None: 93 return child_process.poll() 94 teed_output.write(output_line) 95 96 def run_and_throw_if_fail(self, args, quiet=False): 97 # Cache the child's output locally so it can be used for error reports. 98 child_out_file = StringIO.StringIO() 99 if quiet: 100 dev_null = open(os.devnull, "w") 101 child_stdout = tee(child_out_file, dev_null if quiet else sys.stdout) 102 exit_code = self._run_command_with_teed_output(args, child_stdout) 103 if quiet: 104 dev_null.close() 105 106 child_output = child_out_file.getvalue() 107 child_out_file.close() 108 109 if exit_code: 110 raise ScriptError(script_args=args, 111 exit_code=exit_code, 112 output=child_output) 113 114 @staticmethod 115 def cpu_count(): 116 # This API exists only in Python 2.6 and higher. :( 117 try: 118 import multiprocessing 119 return multiprocessing.cpu_count() 120 except (ImportError, NotImplementedError): 121 # This quantity is a lie but probably a reasonable guess for modern 122 # machines. 123 return 2 124 125 # Error handlers do not need to be static methods once all callers are 126 # updated to use an Executive object. 127 128 @staticmethod 129 def default_error_handler(error): 130 raise error 131 132 @staticmethod 133 def ignore_error(error): 134 pass 135 136 # FIXME: This should be merged with run_and_throw_if_fail 137 138 def run_command(self, 139 args, 140 cwd=None, 141 input=None, 142 error_handler=None, 143 return_exit_code=False, 144 return_stderr=True): 145 if hasattr(input, 'read'): # Check if the input is a file. 146 stdin = input 147 string_to_communicate = None 148 else: 149 stdin = subprocess.PIPE if input else None 150 string_to_communicate = input 151 if return_stderr: 152 stderr = subprocess.STDOUT 153 else: 154 stderr = None 155 156 process = subprocess.Popen(args, 157 stdin=stdin, 158 stdout=subprocess.PIPE, 159 stderr=stderr, 160 cwd=cwd) 161 output = process.communicate(string_to_communicate)[0] 162 exit_code = process.wait() 163 if exit_code: 164 script_error = ScriptError(script_args=args, 165 exit_code=exit_code, 166 output=output, 167 cwd=cwd) 168 (error_handler or self.default_error_handler)(script_error) 169 if return_exit_code: 170 return exit_code 171 return output 172