1 #===----------------------------------------------------------------------===## 2 # 3 # The LLVM Compiler Infrastructure 4 # 5 # This file is dual licensed under the MIT and the University of Illinois Open 6 # Source Licenses. See LICENSE.TXT for details. 7 # 8 #===----------------------------------------------------------------------===## 9 10 from contextlib import contextmanager 11 import errno 12 import os 13 import platform 14 import signal 15 import subprocess 16 import sys 17 import tempfile 18 import threading 19 20 21 # FIXME: Most of these functions are cribbed from LIT 22 def to_bytes(str): 23 # Encode to UTF-8 to get binary data. 24 if isinstance(str, bytes): 25 return str 26 return str.encode('utf-8') 27 28 def to_string(bytes): 29 if isinstance(bytes, str): 30 return bytes 31 return to_bytes(bytes) 32 33 def convert_string(bytes): 34 try: 35 return to_string(bytes.decode('utf-8')) 36 except AttributeError: # 'str' object has no attribute 'decode'. 37 return str(bytes) 38 except UnicodeError: 39 return str(bytes) 40 41 42 def cleanFile(filename): 43 try: 44 os.remove(filename) 45 except OSError: 46 pass 47 48 49 @contextmanager 50 def guardedTempFilename(suffix='', prefix='', dir=None): 51 # Creates and yeilds a temporary filename within a with statement. The file 52 # is removed upon scope exit. 53 handle, name = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dir) 54 os.close(handle) 55 yield name 56 cleanFile(name) 57 58 59 @contextmanager 60 def guardedFilename(name): 61 # yeilds a filename within a with statement. The file is removed upon scope 62 # exit. 63 yield name 64 cleanFile(name) 65 66 67 @contextmanager 68 def nullContext(value): 69 # yeilds a variable within a with statement. No action is taken upon scope 70 # exit. 71 yield value 72 73 74 def makeReport(cmd, out, err, rc): 75 report = "Command: %s\n" % cmd 76 report += "Exit Code: %d\n" % rc 77 if out: 78 report += "Standard Output:\n--\n%s--\n" % out 79 if err: 80 report += "Standard Error:\n--\n%s--\n" % err 81 report += '\n' 82 return report 83 84 85 def capture(args, env=None): 86 """capture(command) - Run the given command (or argv list) in a shell and 87 return the standard output. Raises a CalledProcessError if the command 88 exits with a non-zero status.""" 89 p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 90 env=env) 91 out, err = p.communicate() 92 out = convert_string(out) 93 err = convert_string(err) 94 if p.returncode != 0: 95 raise subprocess.CalledProcessError(cmd=args, 96 returncode=p.returncode, 97 output="{}\n{}".format(out, err)) 98 return out 99 100 101 def which(command, paths = None): 102 """which(command, [paths]) - Look up the given command in the paths string 103 (or the PATH environment variable, if unspecified).""" 104 105 if paths is None: 106 paths = os.environ.get('PATH','') 107 108 # Check for absolute match first. 109 if os.path.isfile(command): 110 return command 111 112 # Would be nice if Python had a lib function for this. 113 if not paths: 114 paths = os.defpath 115 116 # Get suffixes to search. 117 # On Cygwin, 'PATHEXT' may exist but it should not be used. 118 if os.pathsep == ';': 119 pathext = os.environ.get('PATHEXT', '').split(';') 120 else: 121 pathext = [''] 122 123 # Search the paths... 124 for path in paths.split(os.pathsep): 125 for ext in pathext: 126 p = os.path.join(path, command + ext) 127 if os.path.exists(p) and not os.path.isdir(p): 128 return p 129 130 return None 131 132 133 def checkToolsPath(dir, tools): 134 for tool in tools: 135 if not os.path.exists(os.path.join(dir, tool)): 136 return False 137 return True 138 139 140 def whichTools(tools, paths): 141 for path in paths.split(os.pathsep): 142 if checkToolsPath(path, tools): 143 return path 144 return None 145 146 def mkdir_p(path): 147 """mkdir_p(path) - Make the "path" directory, if it does not exist; this 148 will also make directories for any missing parent directories.""" 149 if not path or os.path.exists(path): 150 return 151 152 parent = os.path.dirname(path) 153 if parent != path: 154 mkdir_p(parent) 155 156 try: 157 os.mkdir(path) 158 except OSError: 159 e = sys.exc_info()[1] 160 # Ignore EEXIST, which may occur during a race condition. 161 if e.errno != errno.EEXIST: 162 raise 163 164 165 class ExecuteCommandTimeoutException(Exception): 166 def __init__(self, msg, out, err, exitCode): 167 assert isinstance(msg, str) 168 assert isinstance(out, str) 169 assert isinstance(err, str) 170 assert isinstance(exitCode, int) 171 self.msg = msg 172 self.out = out 173 self.err = err 174 self.exitCode = exitCode 175 176 # Close extra file handles on UNIX (on Windows this cannot be done while 177 # also redirecting input). 178 kUseCloseFDs = not (platform.system() == 'Windows') 179 def executeCommand(command, cwd=None, env=None, input=None, timeout=0): 180 """ 181 Execute command ``command`` (list of arguments or string) 182 with 183 * working directory ``cwd`` (str), use None to use the current 184 working directory 185 * environment ``env`` (dict), use None for none 186 * Input to the command ``input`` (str), use string to pass 187 no input. 188 * Max execution time ``timeout`` (int) seconds. Use 0 for no timeout. 189 190 Returns a tuple (out, err, exitCode) where 191 * ``out`` (str) is the standard output of running the command 192 * ``err`` (str) is the standard error of running the command 193 * ``exitCode`` (int) is the exitCode of running the command 194 195 If the timeout is hit an ``ExecuteCommandTimeoutException`` 196 is raised. 197 """ 198 if input is not None: 199 input = to_bytes(input) 200 p = subprocess.Popen(command, cwd=cwd, 201 stdin=subprocess.PIPE, 202 stdout=subprocess.PIPE, 203 stderr=subprocess.PIPE, 204 env=env, close_fds=kUseCloseFDs) 205 timerObject = None 206 # FIXME: Because of the way nested function scopes work in Python 2.x we 207 # need to use a reference to a mutable object rather than a plain 208 # bool. In Python 3 we could use the "nonlocal" keyword but we need 209 # to support Python 2 as well. 210 hitTimeOut = [False] 211 try: 212 if timeout > 0: 213 def killProcess(): 214 # We may be invoking a shell so we need to kill the 215 # process and all its children. 216 hitTimeOut[0] = True 217 killProcessAndChildren(p.pid) 218 219 timerObject = threading.Timer(timeout, killProcess) 220 timerObject.start() 221 222 out,err = p.communicate(input=input) 223 exitCode = p.wait() 224 finally: 225 if timerObject != None: 226 timerObject.cancel() 227 228 # Ensure the resulting output is always of string type. 229 out = convert_string(out) 230 err = convert_string(err) 231 232 if hitTimeOut[0]: 233 raise ExecuteCommandTimeoutException( 234 msg='Reached timeout of {} seconds'.format(timeout), 235 out=out, 236 err=err, 237 exitCode=exitCode 238 ) 239 240 # Detect Ctrl-C in subprocess. 241 if exitCode == -signal.SIGINT: 242 raise KeyboardInterrupt 243 244 return out, err, exitCode 245 246 247 def killProcessAndChildren(pid): 248 """ 249 This function kills a process with ``pid`` and all its 250 running children (recursively). It is currently implemented 251 using the psutil module which provides a simple platform 252 neutral implementation. 253 254 TODO: Reimplement this without using psutil so we can 255 remove our dependency on it. 256 """ 257 import psutil 258 try: 259 psutilProc = psutil.Process(pid) 260 # Handle the different psutil API versions 261 try: 262 # psutil >= 2.x 263 children_iterator = psutilProc.children(recursive=True) 264 except AttributeError: 265 # psutil 1.x 266 children_iterator = psutilProc.get_children(recursive=True) 267 for child in children_iterator: 268 try: 269 child.kill() 270 except psutil.NoSuchProcess: 271 pass 272 psutilProc.kill() 273 except psutil.NoSuchProcess: 274 pass 275 276 277 def executeCommandVerbose(cmd, *args, **kwargs): 278 """ 279 Execute a command and print its output on failure. 280 """ 281 out, err, exitCode = executeCommand(cmd, *args, **kwargs) 282 if exitCode != 0: 283 report = makeReport(cmd, out, err, exitCode) 284 report += "\n\nFailed!" 285 sys.stderr.write('%s\n' % report) 286 return out, err, exitCode 287