1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
|
#!/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.
|