Debugging: Add code to print backtrace for guest on SIGSEGV
[nativeclient.git] / site_scons / site_tools / command_output.py
blob88b8c39c2ca0e4afc88eef716a2a2c712493097a
1 #!/usr/bin/python2.4
2 # Copyright 2009, Google Inc.
3 # All rights reserved.
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are
7 # met:
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.
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.
31 """Command output builder for SCons."""
34 import os
35 import signal
36 import subprocess
37 import sys
38 import threading
39 import time
40 import SCons.Script
43 # TODO: Move KillProcessTree() and RunCommand() into their own module, since
44 # they're used by other tools.
47 def KillProcessTree(pid):
48 """Kills the process and all of its child processes.
50 Args:
51 pid: process to kill.
53 Raises:
54 OSError: Unsupported OS.
55 """
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()
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=subprocess.PIPE)
70 ps_out = ps_task.communicate()[0]
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
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)
92 else:
93 raise OSError('Unsupported OS for KillProcessTree()')
96 def RunCommand(cmdargs, cwdir=None, env=None, echo_output=True, timeout=None,
97 timeout_errorlevel=14):
98 """Runs an external command.
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.
110 Returns:
111 The integer errorlevel from the command.
112 The combined stdout and stderr as a string.
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
125 def _ReadThread():
126 """Thread worker function to read output from child process.
128 Necessary since there is no cross-platform way of doing non-blocking
129 reads of the output pipe.
131 read_run = True
132 while read_run:
133 time.sleep(.1) # So we don't poll too frequently
134 # Need to have a delay of 1 cycle between child completing and
135 # thread exit, to pick up the final output from the child.
136 if child_retcode is not None:
137 read_run = False
138 new_out = child.stdout.read()
139 if new_out:
140 if echo_output:
141 print new_out,
142 child_out.append(new_out)
144 read_thread = threading.Thread(target=_ReadThread)
145 read_thread.setDaemon(True)
146 read_thread.start()
148 # Wait for child to exit or timeout
149 while child_retcode is None:
150 time.sleep(.1) # So we don't poll too frequently
151 child_retcode = child.poll()
152 if timeout and child_retcode is None:
153 elapsed = time.time() - start_time
154 if elapsed > timeout:
155 print '*** RunCommand() timeout:', cmdargs
156 KillProcessTree(child.pid)
157 child_retcode = timeout_errorlevel
159 # Wait a bit for worker thread to pick up final output and die. No need to
160 # worry if it's still alive at the end of this, since it's a daemon thread
161 # and won't block python from exiting. (And since it's blocked, it doesn't
162 # chew up CPU.)
163 read_thread.join(5)
165 if echo_output:
166 print # end last line of output
167 return child_retcode, ''.join(child_out)
170 def CommandOutputBuilder(target, source, env):
171 """Command output builder.
173 Args:
174 self: Environment in which to build
175 target: List of target nodes
176 source: List of source nodes
178 Returns:
179 None or 0 if successful; nonzero to indicate failure.
181 Runs the command specified in the COMMAND_OUTPUT_CMDLINE environment variable
182 and stores its output in the first target file. Additional target files
183 should be specified if the command creates additional output files.
185 Runs the command in the COMMAND_OUTPUT_RUN_DIR subdirectory.
187 env = env.Clone()
189 cmdline = env.subst('$COMMAND_OUTPUT_CMDLINE', target=target, source=source)
190 cwdir = env.subst('$COMMAND_OUTPUT_RUN_DIR', target=target, source=source)
191 if cwdir:
192 cwdir = os.path.normpath(cwdir)
193 env.AppendENVPath('PATH', cwdir)
194 env.AppendENVPath('LD_LIBRARY_PATH', cwdir)
195 else:
196 cwdir = None
197 cmdecho = env.get('COMMAND_OUTPUT_ECHO', True)
198 timeout = env.get('COMMAND_OUTPUT_TIMEOUT')
199 timeout_errorlevel = env.get('COMMAND_OUTPUT_TIMEOUT_ERRORLEVEL')
201 retcode, output = RunCommand(cmdline, cwdir=cwdir, env=env['ENV'],
202 echo_output=cmdecho, timeout=timeout,
203 timeout_errorlevel=timeout_errorlevel)
205 # Save command line output
206 output_file = open(str(target[0]), 'w')
207 output_file.write(output)
208 output_file.close()
210 return retcode
213 def generate(env):
214 # NOTE: SCons requires the use of this name, which fails gpylint.
215 """SCons entry point for this tool."""
217 # Add the builder and tell it which build environment variables we use.
218 action = SCons.Script.Action(
219 CommandOutputBuilder,
220 'Output "$COMMAND_OUTPUT_CMDLINE" to $TARGET',
221 varlist=[
222 'COMMAND_OUTPUT_CMDLINE',
223 'COMMAND_OUTPUT_RUN_DIR',
224 'COMMAND_OUTPUT_TIMEOUT',
225 'COMMAND_OUTPUT_TIMEOUT_ERRORLEVEL',
226 # We use COMMAND_OUTPUT_ECHO also, but that doesn't change the
227 # command being run or its output.
228 ], )
229 builder = SCons.Script.Builder(action = action)
230 env.Append(BUILDERS={'CommandOutput': builder})
232 # Default command line is to run the first input
233 env['COMMAND_OUTPUT_CMDLINE'] = '$SOURCE'
235 # TODO: Add a pseudo-builder which takes an additional command line as an
236 # argument.