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 10from contextlib import contextmanager 11import errno 12import os 13import platform 14import signal 15import subprocess 16import sys 17import tempfile 18import threading 19 20 21# FIXME: Most of these functions are cribbed from LIT 22def 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 28def to_string(bytes): 29 if isinstance(bytes, str): 30 return bytes 31 return to_bytes(bytes) 32 33def 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 42def cleanFile(filename): 43 try: 44 os.remove(filename) 45 except OSError: 46 pass 47 48 49@contextmanager 50def 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 60def 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 68def nullContext(value): 69 # yeilds a variable within a with statement. No action is taken upon scope 70 # exit. 71 yield value 72 73 74def 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 85def 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 101def 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 133def 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 140def whichTools(tools, paths): 141 for path in paths.split(os.pathsep): 142 if checkToolsPath(path, tools): 143 return path 144 return None 145 146def 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 165class 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). 178kUseCloseFDs = not (platform.system() == 'Windows') 179def 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 247def 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 277def 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