| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/python2.4 | |
| 2 # Copyright 2008, Google Inc. | |
| 3 # All rights reserved. | |
| 4 # | |
| 5 # Redistribution and use in source and binary forms, with or without | |
| 6 # modification, are permitted provided that the following conditions are | |
| 7 # met: | |
| 8 # | |
| 9 # * Redistributions of source code must retain the above copyright | |
| 10 # notice, this list of conditions and the following disclaimer. | |
| 11 # * Redistributions in binary form must reproduce the above | |
| 12 # copyright notice, this list of conditions and the following disclaimer | |
| 13 # in the documentation and/or other materials provided with the | |
| 14 # distribution. | |
| 15 # * Neither the name of Google Inc. nor the names of its | |
| 16 # contributors may be used to endorse or promote products derived from | |
| 17 # this software without specific prior written permission. | |
| 18 # | |
| 19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 20 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 21 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 22 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 23 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 24 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 29 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 30 | |
| 31 """Command output builder for SCons.""" | |
| 32 | |
| 33 | |
| 34 import os | |
| 35 import signal | |
| 36 import subprocess | |
| 37 import sys | |
| 38 import threading | |
| 39 import time | |
| 40 import SCons.Script | |
| 41 | |
| 42 | |
| 43 # TODO(rspangler): Move KillProcessTree() and RunCommand() into their own | |
| 44 # module. | |
| 45 | |
| 46 | |
| 47 def KillProcessTree(pid): | |
| 48 """Kills the process and all of its child processes. | |
| 49 | |
| 50 Args: | |
| 51 pid: process to kill. | |
| 52 | |
| 53 Raises: | |
| 54 OSError: Unsupported OS. | |
| 55 """ | |
| 56 | |
| 57 if sys.platform in ('win32', 'cygwin'): | |
| 58 # Use Windows' taskkill utility | |
| 59 killproc_path = '%s;%s\\system32;%s\\system32\\wbem' % ( | |
| 60 (os.environ['SYSTEMROOT'],) * 3) | |
| 61 killproc_cmd = 'taskkill /F /T /PID %d' % pid | |
| 62 killproc_task = subprocess.Popen(killproc_cmd, shell=True, | |
| 63 stdout=subprocess.PIPE, | |
| 64 env={'PATH':killproc_path}) | |
| 65 killproc_task.communicate() | |
| 66 | |
| 67 elif sys.platform in ('linux', 'linux2', 'darwin'): | |
| 68 # Use ps to get a list of processes | |
| 69 ps_task = subprocess.Popen(['/bin/ps', 'x', '-o', 'pid,ppid'], stdout=subpro
cess.PIPE) | |
| 70 ps_out = ps_task.communicate()[0] | |
| 71 | |
| 72 # Parse out a dict of pid->ppid | |
| 73 ppid = {} | |
| 74 for ps_line in ps_out.split('\n'): | |
| 75 w = ps_line.strip().split() | |
| 76 if len(w) < 2: | |
| 77 continue # Not enough words in this line to be a process list | |
| 78 try: | |
| 79 ppid[int(w[0])] = int(w[1]) | |
| 80 except ValueError: | |
| 81 pass # Header or footer | |
| 82 | |
| 83 # For each process, kill it if it or any of its parents is our child | |
| 84 for p in ppid: | |
| 85 p2 = p | |
| 86 while p2: | |
| 87 if p2 == pid: | |
| 88 os.kill(p, signal.SIGKILL) | |
| 89 break | |
| 90 p2 = ppid.get(p2) | |
| 91 | |
| 92 else: | |
| 93 raise OSError('Unsupported OS for KillProcessTree()') | |
| 94 | |
| 95 | |
| 96 def RunCommand(cmdargs, cwdir=None, env=None, echo_output=True, timeout=None, | |
| 97 timeout_errorlevel=14): | |
| 98 """Runs an external command. | |
| 99 | |
| 100 Args: | |
| 101 cmdargs: A command string, or a tuple containing the command and its | |
| 102 arguments. | |
| 103 cwdir: Working directory for the command, if not None. | |
| 104 env: Environment variables dict, if not None. | |
| 105 echo_output: If True, output will be echoed to stdout. | |
| 106 timeout: If not None, timeout for command in seconds. If command times | |
| 107 out, it will be killed and timeout_errorlevel will be returned. | |
| 108 timeout_errorlevel: The value to return if the command times out. | |
| 109 | |
| 110 Returns: | |
| 111 The integer errorlevel from the command. | |
| 112 The combined stdout and stderr as a string. | |
| 113 """ | |
| 114 # Force unicode string in the environment to strings. | |
| 115 if env: | |
| 116 env = dict([(k, str(v)) for k, v in env.items()]) | |
| 117 start_time = time.time() | |
| 118 child = subprocess.Popen(cmdargs, cwd=cwdir, env=env, shell=True, | |
| 119 universal_newlines=True, | |
| 120 stdin=subprocess.PIPE, | |
| 121 stdout=subprocess.PIPE, stderr=subprocess.STDOUT) | |
| 122 child_out = [] | |
| 123 child_retcode = None | |
| 124 | |
| 125 def _ReadThread(): | |
| 126 """Thread worker function to read output from child process. | |
| 127 | |
| 128 Necessary since there is no cross-platform way of doing non-blocking | |
| 129 reads of the output pipe. | |
| 130 """ | |
| 131 read_run = True | |
| 132 while read_run: | |
| 133 # Need to have a delay of 1 cycle between child completing and | |
| 134 # thread exit, to pick up the final output from the child. | |
| 135 if child_retcode is not None: | |
| 136 read_run = False | |
| 137 new_out = child.stdout.read() | |
| 138 if new_out: | |
| 139 if echo_output: | |
| 140 print new_out, | |
| 141 child_out.append(new_out) | |
| 142 | |
| 143 read_thread = threading.Thread(target=_ReadThread) | |
| 144 read_thread.start() | |
| 145 | |
| 146 # Wait for child to exit or timeout | |
| 147 while child_retcode is None: | |
| 148 time.sleep(1) # So we don't poll too frequently | |
| 149 child_retcode = child.poll() | |
| 150 if timeout and child_retcode is None: | |
| 151 elapsed = time.time() - start_time | |
| 152 if elapsed > timeout: | |
| 153 print '*** RunCommand() timeout:', cmdargs | |
| 154 KillProcessTree(child.pid) | |
| 155 child_retcode = timeout_errorlevel | |
| 156 | |
| 157 # Wait for worker thread to pick up final output and die | |
| 158 read_thread.join(5) | |
| 159 if read_thread.isAlive(): | |
| 160 print '*** Error: RunCommand() read thread did not exit.' | |
| 161 sys.exit(1) | |
| 162 | |
| 163 if echo_output: | |
| 164 print # end last line of output | |
| 165 return child_retcode, ''.join(child_out) | |
| 166 | |
| 167 | |
| 168 def CommandOutputBuilder(target, source, env): | |
| 169 """Command output builder. | |
| 170 | |
| 171 Args: | |
| 172 self: Environment in which to build | |
| 173 target: List of target nodes | |
| 174 source: List of source nodes | |
| 175 | |
| 176 Returns: | |
| 177 None or 0 if successful; nonzero to indicate failure. | |
| 178 | |
| 179 Runs the command specified in the COMMAND_OUTPUT_CMDLINE environment variable | |
| 180 and stores its output in the first target file. Additional target files | |
| 181 should be specified if the command creates additional output files. | |
| 182 | |
| 183 Runs the command in the COMMAND_OUTPUT_RUN_DIR subdirectory. | |
| 184 """ | |
| 185 env = env.Clone() | |
| 186 | |
| 187 cmdline = env.subst('$COMMAND_OUTPUT_CMDLINE', target=target, source=source) | |
| 188 cwdir = env.subst('$COMMAND_OUTPUT_RUN_DIR', target=target, source=source) | |
| 189 if cwdir: | |
| 190 cwdir = os.path.normpath(cwdir) | |
| 191 env.AppendENVPath('PATH', cwdir) | |
| 192 env.AppendENVPath('LD_LIBRARY_PATH', cwdir) | |
| 193 else: | |
| 194 cwdir = None | |
| 195 cmdecho = env.get('COMMAND_OUTPUT_ECHO', True) | |
| 196 timeout = env.get('COMMAND_OUTPUT_TIMEOUT') | |
| 197 timeout_errorlevel = env.get('COMMAND_OUTPUT_TIMEOUT_ERRORLEVEL') | |
| 198 | |
| 199 retcode, output = RunCommand(cmdline, cwdir=cwdir, env=env['ENV'], | |
| 200 echo_output=cmdecho, timeout=timeout, | |
| 201 timeout_errorlevel=timeout_errorlevel) | |
| 202 | |
| 203 # Save command line output | |
| 204 output_file = open(str(target[0]), 'w') | |
| 205 output_file.write(output) | |
| 206 output_file.close() | |
| 207 | |
| 208 return retcode | |
| 209 | |
| 210 | |
| 211 def generate(env): | |
| 212 # NOTE: SCons requires the use of this name, which fails gpylint. | |
| 213 """SCons entry point for this tool.""" | |
| 214 | |
| 215 # Add the builder and tell it which build environment variables we use. | |
| 216 action = SCons.Script.Action( | |
| 217 CommandOutputBuilder, | |
| 218 'Output "$COMMAND_OUTPUT_CMDLINE" to $TARGET', | |
| 219 varlist=[ | |
| 220 'COMMAND_OUTPUT_CMDLINE', | |
| 221 'COMMAND_OUTPUT_RUN_DIR', | |
| 222 'COMMAND_OUTPUT_TIMEOUT', | |
| 223 'COMMAND_OUTPUT_TIMEOUT_ERRORLEVEL', | |
| 224 # We use COMMAND_OUTPUT_ECHO also, but that doesn't change the | |
| 225 # command being run or its output. | |
| 226 ], ) | |
| 227 builder = SCons.Script.Builder(action = action) | |
| 228 env.Append(BUILDERS={'CommandOutput': builder}) | |
| 229 | |
| 230 # Default command line is to run the first input | |
| 231 env['COMMAND_OUTPUT_CMDLINE'] = '$SOURCE' | |
| 232 | |
| 233 # TODO(rspangler): add a pseudo-builder which takes an additional command | |
| 234 # line as an argument. | |
| OLD | NEW |