| OLD | NEW |
| 1 #!/usr/bin/python2.4 | 1 #!/usr/bin/python2.4 |
| 2 # Copyright 2008, Google Inc. | 2 # Copyright 2008, Google Inc. |
| 3 # All rights reserved. | 3 # All rights reserved. |
| 4 # | 4 # |
| 5 # Redistribution and use in source and binary forms, with or without | 5 # Redistribution and use in source and binary forms, with or without |
| 6 # modification, are permitted provided that the following conditions are | 6 # modification, are permitted provided that the following conditions are |
| 7 # met: | 7 # met: |
| 8 # | 8 # |
| 9 # * Redistributions of source code must retain the above copyright | 9 # * Redistributions of source code must retain the above copyright |
| 10 # notice, this list of conditions and the following disclaimer. | 10 # notice, this list of conditions and the following disclaimer. |
| (...skipping 14 matching lines...) Expand all Loading... |
| 25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | 27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 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. | 29 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 30 | 30 |
| 31 """Command output builder for SCons.""" | 31 """Command output builder for SCons.""" |
| 32 | 32 |
| 33 | 33 |
| 34 import os | 34 import os |
| 35 import signal |
| 36 import subprocess |
| 37 import sys |
| 38 import threading |
| 39 import time |
| 35 import SCons.Script | 40 import SCons.Script |
| 36 import subprocess | |
| 37 | 41 |
| 38 | 42 |
| 39 def RunCommand(cmdargs, cwdir=None, env=None, echo_output=True): | 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): |
| 40 """Runs an external command. | 98 """Runs an external command. |
| 41 | 99 |
| 42 Args: | 100 Args: |
| 43 cmdargs: A command string, or a tuple containing the command and its | 101 cmdargs: A command string, or a tuple containing the command and its |
| 44 arguments. | 102 arguments. |
| 45 cwdir: Working directory for the command, if not None. | 103 cwdir: Working directory for the command, if not None. |
| 46 env: Environment variables dict, if not None. | 104 env: Environment variables dict, if not None. |
| 47 echo_output: If True, output will be echoed to stdout. | 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. |
| 48 | 109 |
| 49 Returns: | 110 Returns: |
| 50 The integer errorlevel from the command. | 111 The integer errorlevel from the command. |
| 51 The combined stdout and stderr as a string. | 112 The combined stdout and stderr as a string. |
| 52 """ | 113 """ |
| 53 | |
| 54 # Force unicode string in the environment to strings. | 114 # Force unicode string in the environment to strings. |
| 55 if env: | 115 if env: |
| 56 env = dict([(k, str(v)) for k, v in env.items()]) | 116 env = dict([(k, str(v)) for k, v in env.items()]) |
| 117 start_time = time.time() |
| 57 child = subprocess.Popen(cmdargs, cwd=cwdir, env=env, shell=True, | 118 child = subprocess.Popen(cmdargs, cwd=cwdir, env=env, shell=True, |
| 119 universal_newlines=True, |
| 58 stdin=subprocess.PIPE, | 120 stdin=subprocess.PIPE, |
| 59 stdout=subprocess.PIPE, stderr=subprocess.STDOUT) | 121 stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| 60 child_out = [] | 122 child_out = [] |
| 61 child_retcode = None | 123 child_retcode = None |
| 62 | 124 |
| 63 # Need to poll the child process, since the stdout pipe can fill. | 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 |
| 64 while child_retcode is None: | 147 while child_retcode is None: |
| 148 time.sleep(1) # So we don't poll too frequently |
| 65 child_retcode = child.poll() | 149 child_retcode = child.poll() |
| 66 new_out = child.stdout.read() | 150 if timeout and child_retcode is None: |
| 67 if echo_output: | 151 elapsed = time.time() - start_time |
| 68 print new_out, | 152 if elapsed > timeout: |
| 69 child_out.append(new_out) | 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) |
| 70 | 162 |
| 71 if echo_output: | 163 if echo_output: |
| 72 print # end last line of output | 164 print # end last line of output |
| 73 return child_retcode, ''.join(child_out) | 165 return child_retcode, ''.join(child_out) |
| 74 | 166 |
| 75 | 167 |
| 76 def CommandOutputBuilder(target, source, env): | 168 def CommandOutputBuilder(target, source, env): |
| 77 """Command output builder. | 169 """Command output builder. |
| 78 | 170 |
| 79 Args: | 171 Args: |
| (...skipping 13 matching lines...) Expand all Loading... |
| 93 env = env.Clone() | 185 env = env.Clone() |
| 94 | 186 |
| 95 cmdline = env.subst('$COMMAND_OUTPUT_CMDLINE', target=target, source=source) | 187 cmdline = env.subst('$COMMAND_OUTPUT_CMDLINE', target=target, source=source) |
| 96 cwdir = env.subst('$COMMAND_OUTPUT_RUN_DIR', target=target, source=source) | 188 cwdir = env.subst('$COMMAND_OUTPUT_RUN_DIR', target=target, source=source) |
| 97 if cwdir: | 189 if cwdir: |
| 98 cwdir = os.path.normpath(cwdir) | 190 cwdir = os.path.normpath(cwdir) |
| 99 env.AppendENVPath('PATH', cwdir) | 191 env.AppendENVPath('PATH', cwdir) |
| 100 env.AppendENVPath('LD_LIBRARY_PATH', cwdir) | 192 env.AppendENVPath('LD_LIBRARY_PATH', cwdir) |
| 101 else: | 193 else: |
| 102 cwdir = None | 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') |
| 103 | 198 |
| 104 retcode, output = RunCommand(cmdline, cwdir=cwdir, env=env['ENV']) | 199 retcode, output = RunCommand(cmdline, cwdir=cwdir, env=env['ENV'], |
| 200 echo_output=cmdecho, timeout=timeout, |
| 201 timeout_errorlevel=timeout_errorlevel) |
| 105 | 202 |
| 106 # Save command line output | 203 # Save command line output |
| 107 output_file = open(str(target[0]), 'w') | 204 output_file = open(str(target[0]), 'w') |
| 108 output_file.write(output) | 205 output_file.write(output) |
| 109 output_file.close() | 206 output_file.close() |
| 110 | 207 |
| 111 return retcode | 208 return retcode |
| 112 | 209 |
| 113 | 210 |
| 114 def generate(env): | 211 def generate(env): |
| 115 # NOTE: SCons requires the use of this name, which fails gpylint. | 212 # NOTE: SCons requires the use of this name, which fails gpylint. |
| 116 """SCons entry point for this tool.""" | 213 """SCons entry point for this tool.""" |
| 117 | 214 |
| 118 # Add the builder and tell it which build environment variables we use. | 215 # Add the builder and tell it which build environment variables we use. |
| 119 action = SCons.Script.Action(CommandOutputBuilder, varlist=[ | 216 action = SCons.Script.Action( |
| 120 'COMMAND_OUTPUT_CMDLINE', | 217 CommandOutputBuilder, |
| 121 'COMMAND_OUTPUT_RUN_DIR', | 218 'Output "$COMMAND_OUTPUT_CMDLINE" to $TARGET', |
| 122 ]) | 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 ], ) |
| 123 builder = SCons.Script.Builder(action = action) | 227 builder = SCons.Script.Builder(action = action) |
| 124 env.Append(BUILDERS={'CommandOutput': builder}) | 228 env.Append(BUILDERS={'CommandOutput': builder}) |
| 125 | 229 |
| 126 # Default command line is to run the first input | 230 # Default command line is to run the first input |
| 127 env['COMMAND_OUTPUT_CMDLINE'] = '$SOURCE' | 231 env['COMMAND_OUTPUT_CMDLINE'] = '$SOURCE' |
| 128 | 232 |
| 129 # TODO(rspangler): add a pseudo-builder which takes an additional command | 233 # TODO(rspangler): add a pseudo-builder which takes an additional command |
| 130 # line as an argument. | 234 # line as an argument. |
| OLD | NEW |