| #!/usr/bin/python2.4 |
| # Copyright 2008, Google Inc. |
| # All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # * Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # * Redistributions in binary form must reproduce the above |
| # copyright notice, this list of conditions and the following disclaimer |
| # in the documentation and/or other materials provided with the |
| # distribution. |
| # * Neither the name of Google Inc. nor the names of its |
| # contributors may be used to endorse or promote products derived from |
| # this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| """Command output builder for SCons.""" |
| |
| |
| import os |
| import signal |
| import subprocess |
| import sys |
| import threading |
| import time |
| import SCons.Script |
| |
| |
| # TODO(rspangler): Move KillProcessTree() and RunCommand() into their own |
| # module. |
| |
| |
| def KillProcessTree(pid): |
| """Kills the process and all of its child processes. |
| |
| Args: |
| pid: process to kill. |
| |
| Raises: |
| OSError: Unsupported OS. |
| """ |
| |
| if sys.platform in ('win32', 'cygwin'): |
| # Use Windows' taskkill utility |
| killproc_path = '%s;%s\\system32;%s\\system32\\wbem' % ( |
| (os.environ['SYSTEMROOT'],) * 3) |
| killproc_cmd = 'taskkill /F /T /PID %d' % pid |
| killproc_task = subprocess.Popen(killproc_cmd, shell=True, |
| stdout=subprocess.PIPE, |
| env={'PATH':killproc_path}) |
| killproc_task.communicate() |
| |
| elif sys.platform in ('linux', 'linux2', 'darwin'): |
| # Use ps to get a list of processes |
| ps_task = subprocess.Popen(['/bin/ps', 'x', '-o', 'pid,ppid'], stdout=subprocess.PIPE) |
| ps_out = ps_task.communicate()[0] |
| |
| # Parse out a dict of pid->ppid |
| ppid = {} |
| for ps_line in ps_out.split('\n'): |
| w = ps_line.strip().split() |
| if len(w) < 2: |
| continue # Not enough words in this line to be a process list |
| try: |
| ppid[int(w[0])] = int(w[1]) |
| except ValueError: |
| pass # Header or footer |
| |
| # For each process, kill it if it or any of its parents is our child |
| for p in ppid: |
| p2 = p |
| while p2: |
| if p2 == pid: |
| os.kill(p, signal.SIGKILL) |
| break |
| p2 = ppid.get(p2) |
| |
| else: |
| raise OSError('Unsupported OS for KillProcessTree()') |
| |
| |
| def RunCommand(cmdargs, cwdir=None, env=None, echo_output=True, timeout=None, |
| timeout_errorlevel=14): |
| """Runs an external command. |
| |
| Args: |
| cmdargs: A command string, or a tuple containing the command and its |
| arguments. |
| cwdir: Working directory for the command, if not None. |
| env: Environment variables dict, if not None. |
| echo_output: If True, output will be echoed to stdout. |
| timeout: If not None, timeout for command in seconds. If command times |
| out, it will be killed and timeout_errorlevel will be returned. |
| timeout_errorlevel: The value to return if the command times out. |
| |
| Returns: |
| The integer errorlevel from the command. |
| The combined stdout and stderr as a string. |
| """ |
| # Force unicode string in the environment to strings. |
| if env: |
| env = dict([(k, str(v)) for k, v in env.items()]) |
| start_time = time.time() |
| child = subprocess.Popen(cmdargs, cwd=cwdir, env=env, shell=True, |
| universal_newlines=True, |
| stdin=subprocess.PIPE, |
| stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| child_out = [] |
| child_retcode = None |
| |
| def _ReadThread(): |
| """Thread worker function to read output from child process. |
| |
| Necessary since there is no cross-platform way of doing non-blocking |
| reads of the output pipe. |
| """ |
| read_run = True |
| while read_run: |
| # Need to have a delay of 1 cycle between child completing and |
| # thread exit, to pick up the final output from the child. |
| if child_retcode is not None: |
| read_run = False |
| new_out = child.stdout.read() |
| if new_out: |
| if echo_output: |
| print new_out, |
| child_out.append(new_out) |
| |
| read_thread = threading.Thread(target=_ReadThread) |
| read_thread.start() |
| |
| # Wait for child to exit or timeout |
| while child_retcode is None: |
| time.sleep(1) # So we don't poll too frequently |
| child_retcode = child.poll() |
| if timeout and child_retcode is None: |
| elapsed = time.time() - start_time |
| if elapsed > timeout: |
| print '*** RunCommand() timeout:', cmdargs |
| KillProcessTree(child.pid) |
| child_retcode = timeout_errorlevel |
| |
| # Wait for worker thread to pick up final output and die |
| read_thread.join(5) |
| if read_thread.isAlive(): |
| print '*** Error: RunCommand() read thread did not exit.' |
| sys.exit(1) |
| |
| if echo_output: |
| print # end last line of output |
| return child_retcode, ''.join(child_out) |
| |
| |
| def CommandOutputBuilder(target, source, env): |
| """Command output builder. |
| |
| Args: |
| self: Environment in which to build |
| target: List of target nodes |
| source: List of source nodes |
| |
| Returns: |
| None or 0 if successful; nonzero to indicate failure. |
| |
| Runs the command specified in the COMMAND_OUTPUT_CMDLINE environment variable |
| and stores its output in the first target file. Additional target files |
| should be specified if the command creates additional output files. |
| |
| Runs the command in the COMMAND_OUTPUT_RUN_DIR subdirectory. |
| """ |
| env = env.Clone() |
| |
| cmdline = env.subst('$COMMAND_OUTPUT_CMDLINE', target=target, source=source) |
| cwdir = env.subst('$COMMAND_OUTPUT_RUN_DIR', target=target, source=source) |
| if cwdir: |
| cwdir = os.path.normpath(cwdir) |
| env.AppendENVPath('PATH', cwdir) |
| env.AppendENVPath('LD_LIBRARY_PATH', cwdir) |
| else: |
| cwdir = None |
| cmdecho = env.get('COMMAND_OUTPUT_ECHO', True) |
| timeout = env.get('COMMAND_OUTPUT_TIMEOUT') |
| timeout_errorlevel = env.get('COMMAND_OUTPUT_TIMEOUT_ERRORLEVEL') |
| |
| retcode, output = RunCommand(cmdline, cwdir=cwdir, env=env['ENV'], |
| echo_output=cmdecho, timeout=timeout, |
| timeout_errorlevel=timeout_errorlevel) |
| |
| # Save command line output |
| output_file = open(str(target[0]), 'w') |
| output_file.write(output) |
| output_file.close() |
| |
| return retcode |
| |
| |
| def generate(env): |
| # NOTE: SCons requires the use of this name, which fails gpylint. |
| """SCons entry point for this tool.""" |
| |
| # Add the builder and tell it which build environment variables we use. |
| action = SCons.Script.Action( |
| CommandOutputBuilder, |
| 'Output "$COMMAND_OUTPUT_CMDLINE" to $TARGET', |
| varlist=[ |
| 'COMMAND_OUTPUT_CMDLINE', |
| 'COMMAND_OUTPUT_RUN_DIR', |
| 'COMMAND_OUTPUT_TIMEOUT', |
| 'COMMAND_OUTPUT_TIMEOUT_ERRORLEVEL', |
| # We use COMMAND_OUTPUT_ECHO also, but that doesn't change the |
| # command being run or its output. |
| ], ) |
| builder = SCons.Script.Builder(action = action) |
| env.Append(BUILDERS={'CommandOutput': builder}) |
| |
| # Default command line is to run the first input |
| env['COMMAND_OUTPUT_CMDLINE'] = '$SOURCE' |
| |
| # TODO(rspangler): add a pseudo-builder which takes an additional command |
| # line as an argument. |