summaryrefslogtreecommitdiffstats
path: root/site_scons/site_tools/command_output.py
diff options
context:
space:
mode:
Diffstat (limited to 'site_scons/site_tools/command_output.py')
-rw-r--r--site_scons/site_tools/command_output.py130
1 files changed, 117 insertions, 13 deletions
diff --git a/site_scons/site_tools/command_output.py b/site_scons/site_tools/command_output.py
index ce702b7..795f3eb 100644
--- a/site_scons/site_tools/command_output.py
+++ b/site_scons/site_tools/command_output.py
@@ -32,11 +32,69 @@
import os
-import SCons.Script
+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)
-def RunCommand(cmdargs, cwdir=None, env=None, echo_output=True):
+ 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:
@@ -45,28 +103,62 @@ def RunCommand(cmdargs, cwdir=None, env=None, echo_output=True):
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
- # Need to poll the child process, since the stdout pipe can fill.
+ 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()
- new_out = child.stdout.read()
- if echo_output:
- print new_out,
- child_out.append(new_out)
+ 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
@@ -100,8 +192,13 @@ def CommandOutputBuilder(target, source, env):
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'])
+ 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')
@@ -116,10 +213,17 @@ def generate(env):
"""SCons entry point for this tool."""
# Add the builder and tell it which build environment variables we use.
- action = SCons.Script.Action(CommandOutputBuilder, varlist=[
- 'COMMAND_OUTPUT_CMDLINE',
- 'COMMAND_OUTPUT_RUN_DIR',
- ])
+ 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})