aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/shell
diff options
context:
space:
mode:
authorGravatar Han-Wen Nienhuys <hanwen@google.com>2015-02-25 16:45:20 +0100
committerGravatar Han-Wen Nienhuys <hanwen@google.com>2015-02-25 16:45:20 +0100
commitd08b27fa9701fecfdb69e1b0d1ac2459efc2129b (patch)
tree5d50963026239ca5aebfb47ea5b8db7e814e57c8 /src/main/java/com/google/devtools/build/lib/shell
Update from Google.
-- MOE_MIGRATED_REVID=85702957
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/shell')
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/AbnormalTerminationException.java52
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/BadExitStatusException.java36
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/Command.java960
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/CommandException.java48
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/CommandResult.java116
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/Consumers.java359
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/ExecFailedException.java28
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/FutureCommandResult.java40
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/InputStreamSink.java133
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/Killable.java31
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/KillableObserver.java49
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/LogUtil.java54
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/ProcessKillable.java36
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/Shell.java132
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/ShellUtils.java145
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/SimpleKillableObserver.java60
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/TerminationStatus.java162
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/TimeoutKillableObserver.java102
18 files changed, 2543 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/shell/AbnormalTerminationException.java b/src/main/java/com/google/devtools/build/lib/shell/AbnormalTerminationException.java
new file mode 100644
index 0000000000..30562c6359
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/AbnormalTerminationException.java
@@ -0,0 +1,52 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Thrown when a command's execution terminates abnormally -- for example,
+ * if it is killed, or if it terminates with a non-zero exit status.
+ */
+public class AbnormalTerminationException extends CommandException {
+
+ private final CommandResult result;
+
+ public AbnormalTerminationException(final Command command,
+ final CommandResult result,
+ final String message) {
+ super(command, message);
+ this.result = result;
+ }
+
+ public AbnormalTerminationException(final Command command,
+ final CommandResult result,
+ final Throwable cause) {
+ super(command, cause);
+ this.result = result;
+ }
+
+ public AbnormalTerminationException(final Command command,
+ final CommandResult result,
+ final String message,
+ final Throwable cause) {
+ super(command, message, cause);
+ this.result = result;
+ }
+
+ public CommandResult getResult() {
+ return result;
+ }
+
+ private static final long serialVersionUID = 2L;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/BadExitStatusException.java b/src/main/java/com/google/devtools/build/lib/shell/BadExitStatusException.java
new file mode 100644
index 0000000000..324007a0cb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/BadExitStatusException.java
@@ -0,0 +1,36 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Thrown when a command's execution terminates with a non-zero exit status.
+ */
+public final class BadExitStatusException extends AbnormalTerminationException {
+
+ public BadExitStatusException(final Command command,
+ final CommandResult result,
+ final String message) {
+ super(command, result, message);
+ }
+
+ public BadExitStatusException(final Command command,
+ final CommandResult result,
+ final String message,
+ final Throwable cause) {
+ super(command, result, message, cause);
+ }
+
+ private static final long serialVersionUID = 1L;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/Command.java b/src/main/java/com/google/devtools/build/lib/shell/Command.java
new file mode 100644
index 0000000000..ab4a7fc971
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/Command.java
@@ -0,0 +1,960 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <p>Represents an executable command, including its arguments and
+ * runtime environment (environment variables, working directory). This class
+ * lets a caller execute a command, get its results, and optionally try to kill
+ * the task during execution.</p>
+ *
+ * <p>The use of "shell" in the full name of this class is a misnomer. In
+ * terms of the way its arguments are interpreted, this class is closer to
+ * {@code execve(2)} than to {@code system(3)}. No Bourne shell is executed.
+ *
+ * <p>The most basic use-case for this class is as follows:
+ * <pre>
+ * String[] args = { "/bin/du", "-s", directory };
+ * CommandResult result = new Command(args).execute();
+ * String output = new String(result.getStdout());
+ * </pre>
+ * which writes the output of the {@code du(1)} command into {@code output}.
+ * More complex cases might inspect the stderr stream, kill the subprocess
+ * asynchronously, feed input to its standard input, handle the exceptions
+ * thrown if the command fails, or print the termination status (exit code or
+ * signal name).
+ *
+ * <h4>Invoking the Bourne shell</h4>
+ *
+ * <p>Perhaps the most common command invoked programmatically is the UNIX
+ * shell, {@code /bin/sh}. Because the shell is a general-purpose programming
+ * language, care must be taken to ensure that variable parts of the shell
+ * command (e.g. strings entered by the user) do not contain shell
+ * metacharacters, as this poses a correctness and/or security risk.
+ *
+ * <p>To execute a shell command directly, use the following pattern:
+ * <pre>
+ * String[] args = { "/bin/sh", "-c", shellCommand };
+ * CommandResult result = new Command(args).execute();
+ * </pre>
+ * {@code shellCommand} is a complete Bourne shell program, possibly containing
+ * all kinds of unescaped metacharacters. For example, here's a shell command
+ * that enumerates the working directories of all processes named "foo":
+ * <pre>ps auxx | grep foo | awk '{print $1}' |
+ * while read pid; do readlink /proc/$pid/cwd; done</pre>
+ * It is the responsibility of the caller to ensure that this string means what
+ * they intend.
+ *
+ * <p>Consider the risk posed by allowing the "foo" part of the previous
+ * command to be some arbitrary (untrusted) string called {@code processName}:
+ * <pre>
+ * // WARNING: unsafe!
+ * String shellCommand = "ps auxx | grep " + processName + " | awk '{print $1}' | "
+ * + "while read pid; do readlink /proc/$pid/cwd; done";</pre>
+ * </pre>
+ * Passing this string to {@link Command} is unsafe because if the string
+ * {@processName} contains shell metacharacters, the meaning of the command can
+ * be arbitrarily changed; consider:
+ * <pre>String processName = ". ; rm -fr $HOME & ";</pre>
+ *
+ * <p>To defend against this possibility, it is essential to properly quote the
+ * variable portions of the shell command so that shell metacharacters are
+ * escaped. Use {@link ShellUtils#shellEscape} for this purpose:
+ * <pre>
+ * // Safe.
+ * String shellCommand = "ps auxx | grep " + ShellUtils.shellEscape(processName)
+ * + " | awk '{print $1}' | while read pid; do readlink /proc/$pid/cwd; done";
+ * </pre>
+ *
+ * <p>Tip: if you are only invoking a single known command, and no shell
+ * features (e.g. $PATH lookup, output redirection, pipelines, etc) are needed,
+ * call it directly without using a shell, as in the {@code du(1)} example
+ * above.
+ *
+ * <h4>Other features</h4>
+ *
+ * <p>A caller can optionally specify bytes to be written to the process's
+ * "stdin". The returned {@link CommandResult} object gives the caller access to
+ * the exit status, as well as output from "stdout" and "stderr". To use
+ * this class with processes that generate very large amounts of input/output,
+ * consider
+ * {@link #execute(InputStream, KillableObserver, OutputStream, OutputStream)}
+ * and
+ * {@link #execute(byte[], KillableObserver, OutputStream, OutputStream)}.
+ * </p>
+ *
+ * <p>This class ensures that stdout and stderr streams are read promptly,
+ * avoiding potential deadlock if the output is large. See <a
+ * href="http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html"> When
+ * <code>Runtime.exec()</code> won't</a>.</p>
+ *
+ * <p>This class is immutable and therefore thread-safe.</p>
+ */
+public final class Command {
+
+ private static final Logger log =
+ Logger.getLogger("com.google.devtools.build.lib.shell.Command");
+
+ /**
+ * Pass this value to {@link #execute(byte[])} to indicate that no input
+ * should be written to stdin.
+ */
+ public static final byte[] NO_INPUT = new byte[0];
+
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+ /**
+ * Pass this to {@link #execute(byte[], KillableObserver, boolean)} to
+ * indicate that you do not wish to observe / kill the underlying
+ * process.
+ */
+ public static final KillableObserver NO_OBSERVER = new KillableObserver() {
+ @Override
+ public void startObserving(final Killable killable) {
+ // do nothing
+ }
+ @Override
+ public void stopObserving(final Killable killable) {
+ // do nothing
+ }
+ };
+
+ private final ProcessBuilder processBuilder;
+
+ // Start of public API -----------------------------------------------------
+
+ /**
+ * Creates a new {@link Command} that will execute a command line that
+ * is described by a {@link ProcessBuilder}. Command line elements,
+ * environment, and working directory are taken from this object. The
+ * command line is executed exactly as given, without a shell.
+ *
+ * @param processBuilder {@link ProcessBuilder} describing command line
+ * to execute
+ */
+ public Command(final ProcessBuilder processBuilder) {
+ this(processBuilder.command().toArray(EMPTY_STRING_ARRAY),
+ processBuilder.environment(),
+ processBuilder.directory());
+ }
+
+ /**
+ * Creates a new {@link Command} for the given command line elements. The
+ * command line is executed exactly as given, without a shell.
+ * Subsequent calls to {@link #execute()} will use the JVM's working
+ * directory and environment.
+ *
+ * @param commandLineElements elements of raw command line to execute
+ * @throws IllegalArgumentException if commandLine is null or empty
+ */
+ /* TODO(bazel-team): Use varargs here
+ */
+ public Command(final String[] commandLineElements) {
+ this(commandLineElements, null, null);
+ }
+
+ /**
+ * <p>Creates a new {@link Command} for the given command line elements.
+ * Subsequent calls to {@link #execute()} will use the JVM's working
+ * directory and environment.</p>
+ *
+ * <p>Note: be careful when setting useShell to <code>true</code>; you
+ * may inadvertently expose a security hole. See
+ * {@link #Command(String, Map, File)}.</p>
+ *
+ * @param commandLineElements elements of raw command line to execute
+ * @param useShell if true, command is executed using a shell interpreter
+ * (e.g. <code>/bin/sh</code> on Linux); if false, command is executed
+ * exactly as given
+ * @throws IllegalArgumentException if commandLine is null or empty
+ */
+ public Command(final String[] commandLineElements, final boolean useShell) {
+ this(commandLineElements, useShell, null, null);
+ }
+
+ /**
+ * Creates a new {@link Command} for the given command line elements. The
+ * command line is executed exactly as given, without a shell. The given
+ * environment variables and working directory are used in subsequent
+ * calls to {@link #execute()}.
+ *
+ * @param commandLineElements elements of raw command line to execute
+ * @param environmentVariables environment variables to replace JVM's
+ * environment variables; may be null
+ * @param workingDirectory working directory for execution; if null, current
+ * working directory is used
+ * @throws IllegalArgumentException if commandLine is null or empty
+ */
+ public Command(final String[] commandLineElements,
+ final Map<String, String> environmentVariables,
+ final File workingDirectory) {
+ this(commandLineElements, false, environmentVariables, workingDirectory);
+ }
+
+ /**
+ * <p>Creates a new {@link Command} for the given command line elements. The
+ * given environment variables and working directory are used in subsequent
+ * calls to {@link #execute()}.</p>
+ *
+ * <p>Note: be careful when setting useShell to <code>true</code>; you
+ * may inadvertently expose a security hole. See
+ * {@link #Command(String, Map, File)}.</p>
+ *
+ * @param commandLineElements elements of raw command line to execute
+ * @param useShell if true, command is executed using a shell interpreter
+ * (e.g. <code>/bin/sh</code> on Linux); if false, command is executed
+ * exactly as given
+ * @param environmentVariables environment variables to replace JVM's
+ * environment variables; may be null
+ * @param workingDirectory working directory for execution; if null, current
+ * working directory is used
+ * @throws IllegalArgumentException if commandLine is null or empty
+ */
+ public Command(final String[] commandLineElements,
+ final boolean useShell,
+ final Map<String, String> environmentVariables,
+ final File workingDirectory) {
+ if (commandLineElements == null || commandLineElements.length == 0) {
+ throw new IllegalArgumentException("command line is null or empty");
+ }
+ this.processBuilder =
+ new ProcessBuilder(maybeAddShell(commandLineElements, useShell));
+ if (environmentVariables != null) {
+ // TODO(bazel-team) remove next line eventually; it is here to mimic old
+ // Runtime.exec() behavior
+ this.processBuilder.environment().clear();
+ this.processBuilder.environment().putAll(environmentVariables);
+ }
+ this.processBuilder.directory(workingDirectory);
+ }
+
+ private static String[] maybeAddShell(final String[] commandLineElements,
+ final boolean useShell) {
+ if (useShell) {
+ final StringBuilder builder = new StringBuilder();
+ for (final String element : commandLineElements) {
+ if (builder.length() > 0) {
+ builder.append(' ');
+ }
+ builder.append(element);
+ }
+ return Shell.getPlatformShell().shellify(builder.toString());
+ } else {
+ return commandLineElements;
+ }
+ }
+
+ /**
+ * @return raw command line elements to be executed
+ */
+ public String[] getCommandLineElements() {
+ final List<String> elements = processBuilder.command();
+ return elements.toArray(new String[elements.size()]);
+ }
+
+ /**
+ * @return (unmodifiable) {@link Map} view of command's environment variables
+ */
+ public Map<String, String> getEnvironmentVariables() {
+ return Collections.unmodifiableMap(processBuilder.environment());
+ }
+
+ /**
+ * @return working directory used for execution, or null if the current
+ * working directory is used
+ */
+ public File getWorkingDirectory() {
+ return processBuilder.directory();
+ }
+
+ /**
+ * Execute this command with no input to stdin. This call will block until the
+ * process completes or an error occurs.
+ *
+ * @return {@link CommandResult} representing result of the execution
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws AbnormalTerminationException if an {@link IOException} is
+ * encountered while reading from the process, or the process was terminated
+ * due to a signal.
+ * @throws BadExitStatusException if the process exits with a
+ * non-zero status
+ */
+ public CommandResult execute() throws CommandException {
+ return execute(NO_INPUT);
+ }
+
+ /**
+ * Execute this command with given input to stdin. This call will block until
+ * the process completes or an error occurs.
+ *
+ * @param stdinInput bytes to be written to process's stdin
+ * @return {@link CommandResult} representing result of the execution
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws AbnormalTerminationException if an {@link IOException} is
+ * encountered while reading from the process, or the process was terminated
+ * due to a signal.
+ * @throws BadExitStatusException if the process exits with a
+ * non-zero status
+ * @throws NullPointerException if stdin is null
+ */
+ public CommandResult execute(final byte[] stdinInput)
+ throws CommandException {
+ nullCheck(stdinInput, "stdinInput");
+ return doExecute(new ByteArrayInputSource(stdinInput),
+ NO_OBSERVER,
+ Consumers.createAccumulatingConsumers(),
+ /*killSubprocess=*/false, /*closeOutput=*/false).get();
+ }
+
+ /**
+ * <p>Execute this command with given input to stdin. This call will block
+ * until the process completes or an error occurs. Caller may specify
+ * whether the method should ignore stdout/stderr output. If the
+ * given number of milliseconds elapses before the command has
+ * completed, this method will attempt to kill the command.</p>
+ *
+ * @param stdinInput bytes to be written to process's stdin, or
+ * {@link #NO_INPUT} if no bytes should be written
+ * @param timeout number of milliseconds to wait for command completion
+ * before attempting to kill the command
+ * @param ignoreOutput if true, method will ignore stdout/stderr output
+ * and return value will not contain this data
+ * @return {@link CommandResult} representing result of the execution
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws AbnormalTerminationException if an {@link IOException} is
+ * encountered while reading from the process, or the process was terminated
+ * due to a signal.
+ * @throws BadExitStatusException if the process exits with a
+ * non-zero status
+ * @throws NullPointerException if stdin is null
+ */
+ public CommandResult execute(final byte[] stdinInput,
+ final long timeout,
+ final boolean ignoreOutput)
+ throws CommandException {
+ return execute(stdinInput,
+ new TimeoutKillableObserver(timeout),
+ ignoreOutput);
+ }
+
+ /**
+ * <p>Execute this command with given input to stdin. This call will block
+ * until the process completes or an error occurs. Caller may specify
+ * whether the method should ignore stdout/stderr output. The given {@link
+ * KillableObserver} may also terminate the process early while running.</p>
+ *
+ * @param stdinInput bytes to be written to process's stdin, or
+ * {@link #NO_INPUT} if no bytes should be written
+ * @param observer {@link KillableObserver} that should observe the running
+ * process, or {@link #NO_OBSERVER} if caller does not wish to kill
+ * the process
+ * @param ignoreOutput if true, method will ignore stdout/stderr output
+ * and return value will not contain this data
+ * @return {@link CommandResult} representing result of the execution
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws AbnormalTerminationException if the process is interrupted (or
+ * killed) before completion, if an {@link IOException} is encountered while
+ * reading from the process, or the process was terminated due to a signal.
+ * @throws BadExitStatusException if the process exits with a
+ * non-zero status
+ * @throws NullPointerException if stdin is null
+ */
+ public CommandResult execute(final byte[] stdinInput,
+ final KillableObserver observer,
+ final boolean ignoreOutput)
+ throws CommandException {
+ // supporting "null" here for backwards compatibility
+ final KillableObserver theObserver =
+ observer == null ? NO_OBSERVER : observer;
+ return doExecute(new ByteArrayInputSource(stdinInput),
+ theObserver,
+ ignoreOutput ? Consumers.createDiscardingConsumers()
+ : Consumers.createAccumulatingConsumers(),
+ /*killSubprocess=*/false, /*closeOutput=*/false).get();
+ }
+
+ /**
+ * <p>Execute this command with given input to stdin. This call blocks
+ * until the process completes or an error occurs. The caller provides
+ * {@link OutputStream} instances into which the process writes its
+ * stdout/stderr output; these streams are <em>not</em> closed when the
+ * process terminates. The given {@link KillableObserver} may also
+ * terminate the process early while running.</p>
+ *
+ * <p>Note that stdout and stderr are written concurrently. If these are
+ * aliased to each other, it is the caller's duty to ensure thread safety.
+ * </p>
+ *
+ * @param stdinInput bytes to be written to process's stdin, or
+ * {@link #NO_INPUT} if no bytes should be written
+ * @param observer {@link KillableObserver} that should observe the running
+ * process, or {@link #NO_OBSERVER} if caller does not wish to kill the
+ * process
+ * @param stdOut the process will write its standard output into this stream.
+ * E.g., you could pass {@link System#out} as <code>stdOut</code>.
+ * @param stdErr the process will write its standard error into this stream.
+ * E.g., you could pass {@link System#err} as <code>stdErr</code>.
+ * @return {@link CommandResult} representing result of the execution. Note
+ * that {@link CommandResult#getStdout()} and
+ * {@link CommandResult#getStderr()} will yield {@link IllegalStateException}
+ * in this case, as the output is written to <code>stdOut/stdErr</code>
+ * instead.
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws AbnormalTerminationException if the process is interrupted (or
+ * killed) before completion, if an {@link IOException} is encountered while
+ * reading from the process, or the process was terminated due to a signal.
+ * @throws BadExitStatusException if the process exits with a
+ * non-zero status
+ * @throws NullPointerException if any argument is null.
+ */
+ public CommandResult execute(final byte[] stdinInput,
+ final KillableObserver observer,
+ final OutputStream stdOut,
+ final OutputStream stdErr)
+ throws CommandException {
+ return execute(stdinInput, observer, stdOut, stdErr, false);
+ }
+
+ /**
+ * Like {@link #execute(byte[], KillableObserver, OutputStream, OutputStream)}
+ * but enables setting of the killSubprocessOnInterrupt attribute.
+ *
+ * @param killSubprocessOnInterrupt if set to true, the execution of
+ * this command is <i>interruptible</i>: in other words, if this thread is
+ * interrupted during a call to execute, the subprocess will be terminated
+ * and the call will return in a timely manner. If false, the subprocess
+ * will run to completion; this is the default value use by all other
+ * constructors. The thread's interrupted status is preserved in all cases,
+ * however.
+ */
+ public CommandResult execute(final byte[] stdinInput,
+ final KillableObserver observer,
+ final OutputStream stdOut,
+ final OutputStream stdErr,
+ final boolean killSubprocessOnInterrupt)
+ throws CommandException {
+ nullCheck(stdinInput, "stdinInput");
+ nullCheck(observer, "observer");
+ nullCheck(stdOut, "stdOut");
+ nullCheck(stdErr, "stdErr");
+ return doExecute(new ByteArrayInputSource(stdinInput),
+ observer,
+ Consumers.createStreamingConsumers(stdOut, stdErr),
+ killSubprocessOnInterrupt, false).get();
+ }
+
+ /**
+ * <p>Execute this command with given input to stdin; this stream is closed
+ * when the process terminates, and exceptions raised when closing this
+ * stream are ignored. This call blocks
+ * until the process completes or an error occurs. The caller provides
+ * {@link OutputStream} instances into which the process writes its
+ * stdout/stderr output; these streams are <em>not</em> closed when the
+ * process terminates. The given {@link KillableObserver} may also
+ * terminate the process early while running.</p>
+ *
+ * @param stdinInput The input to this process's stdin
+ * @param observer {@link KillableObserver} that should observe the running
+ * process, or {@link #NO_OBSERVER} if caller does not wish to kill the
+ * process
+ * @param stdOut the process will write its standard output into this stream.
+ * E.g., you could pass {@link System#out} as <code>stdOut</code>.
+ * @param stdErr the process will write its standard error into this stream.
+ * E.g., you could pass {@link System#err} as <code>stdErr</code>.
+ * @return {@link CommandResult} representing result of the execution. Note
+ * that {@link CommandResult#getStdout()} and
+ * {@link CommandResult#getStderr()} will yield {@link IllegalStateException}
+ * in this case, as the output is written to <code>stdOut/stdErr</code>
+ * instead.
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws AbnormalTerminationException if the process is interrupted (or
+ * killed) before completion, if an {@link IOException} is encountered while
+ * reading from the process, or the process was terminated due to a signal.
+ * @throws BadExitStatusException if the process exits with a
+ * non-zero status
+ * @throws NullPointerException if any argument is null.
+ */
+ public CommandResult execute(final InputStream stdinInput,
+ final KillableObserver observer,
+ final OutputStream stdOut,
+ final OutputStream stdErr)
+ throws CommandException {
+ nullCheck(stdinInput, "stdinInput");
+ nullCheck(observer, "observer");
+ nullCheck(stdOut, "stdOut");
+ nullCheck(stdErr, "stdErr");
+ return doExecute(new InputStreamInputSource(stdinInput),
+ observer,
+ Consumers.createStreamingConsumers(stdOut, stdErr),
+ /*killSubprocess=*/false, /*closeOutput=*/false).get();
+ }
+
+ /**
+ * <p>Execute this command with given input to stdin; this stream is closed
+ * when the process terminates, and exceptions raised when closing this
+ * stream are ignored. This call blocks
+ * until the process completes or an error occurs. The caller provides
+ * {@link OutputStream} instances into which the process writes its
+ * stdout/stderr output; these streams are closed when the process terminates
+ * if closeOut is set. The given {@link KillableObserver} may also
+ * terminate the process early while running.</p>
+ *
+ * @param stdinInput The input to this process's stdin
+ * @param observer {@link KillableObserver} that should observe the running
+ * process, or {@link #NO_OBSERVER} if caller does not wish to kill the
+ * process
+ * @param stdOut the process will write its standard output into this stream.
+ * E.g., you could pass {@link System#out} as <code>stdOut</code>.
+ * @param stdErr the process will write its standard error into this stream.
+ * E.g., you could pass {@link System#err} as <code>stdErr</code>.
+ * @param closeOut whether to close the output streams when the subprocess
+ * terminates.
+ * @return {@link CommandResult} representing result of the execution. Note
+ * that {@link CommandResult#getStdout()} and
+ * {@link CommandResult#getStderr()} will yield {@link IllegalStateException}
+ * in this case, as the output is written to <code>stdOut/stdErr</code>
+ * instead.
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws AbnormalTerminationException if the process is interrupted (or
+ * killed) before completion, if an {@link IOException} is encountered while
+ * reading from the process, or the process was terminated due to a signal.
+ * @throws BadExitStatusException if the process exits with a
+ * non-zero status
+ * @throws NullPointerException if any argument is null.
+ */
+ public CommandResult execute(final InputStream stdinInput,
+ final KillableObserver observer,
+ final OutputStream stdOut,
+ final OutputStream stdErr,
+ boolean closeOut)
+ throws CommandException {
+ nullCheck(stdinInput, "stdinInput");
+ nullCheck(observer, "observer");
+ nullCheck(stdOut, "stdOut");
+ nullCheck(stdErr, "stdErr");
+ return doExecute(new InputStreamInputSource(stdinInput),
+ observer,
+ Consumers.createStreamingConsumers(stdOut, stdErr),
+ false, closeOut).get();
+ }
+
+ /**
+ * <p>Executes this command with the given stdinInput, but does not
+ * wait for it to complete. The caller may choose to observe the status
+ * of the launched process by calling methods on the returned object.
+ *
+ * @param stdinInput bytes to be written to process's stdin, or
+ * {@link #NO_INPUT} if no bytes should be written
+ * @return An object that can be used to check if the process terminated and
+ * obtain the process results.
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws NullPointerException if stdin is null
+ */
+ public FutureCommandResult executeAsynchronously(final byte[] stdinInput)
+ throws CommandException {
+ return executeAsynchronously(stdinInput, NO_OBSERVER);
+ }
+
+ /**
+ * <p>Executes this command with the given input to stdin, but does
+ * not wait for it to complete. The caller may choose to observe the
+ * status of the launched process by calling methods on the returned
+ * object. This method performs the minimum cleanup after the
+ * process terminates: It closes the input stream, and it ignores
+ * exceptions that result from closing it. The given {@link
+ * KillableObserver} may also terminate the process early while
+ * running.</p>
+ *
+ * <p>Note that in this case the {@link KillableObserver} will be assigned
+ * to start observing the process via
+ * {@link KillableObserver#startObserving(Killable)} but will only be
+ * unassigned via {@link KillableObserver#stopObserving(Killable)}, if
+ * {@link FutureCommandResult#get()} is called. If the
+ * {@link KillableObserver} implementation used with this method will
+ * not work correctly without calls to
+ * {@link KillableObserver#stopObserving(Killable)} then a new instance
+ * should be used for each call to this method.</p>
+ *
+ * @param stdinInput bytes to be written to process's stdin, or
+ * {@link #NO_INPUT} if no bytes should be written
+ * @param observer {@link KillableObserver} that should observe the running
+ * process, or {@link #NO_OBSERVER} if caller does not wish to kill
+ * the process
+ * @return An object that can be used to check if the process terminated and
+ * obtain the process results.
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws NullPointerException if stdin is null
+ */
+ public FutureCommandResult executeAsynchronously(final byte[] stdinInput,
+ final KillableObserver observer)
+ throws CommandException {
+ // supporting "null" here for backwards compatibility
+ final KillableObserver theObserver =
+ observer == null ? NO_OBSERVER : observer;
+ nullCheck(stdinInput, "stdinInput");
+ return doExecute(new ByteArrayInputSource(stdinInput),
+ theObserver,
+ Consumers.createDiscardingConsumers(),
+ /*killSubprocess=*/false, /*closeOutput=*/false);
+ }
+
+ /**
+ * <p>Executes this command with the given input to stdin, but does
+ * not wait for it to complete. The caller may choose to observe the
+ * status of the launched process by calling methods on the returned
+ * object. This method performs the minimum cleanup after the
+ * process terminates: It closes the input stream, and it ignores
+ * exceptions that result from closing it. The caller provides
+ * {@link OutputStream} instances into which the process writes its
+ * stdout/stderr output; these streams are <em>not</em> closed when
+ * the process terminates. The given {@link KillableObserver} may
+ * also terminate the process early while running.</p>
+ *
+ * <p>Note that stdout and stderr are written concurrently. If these are
+ * aliased to each other, or if the caller continues to write to these
+ * streams, it is the caller's duty to ensure thread safety.
+ * </p>
+ *
+ * <p>Note that in this case the {@link KillableObserver} will be assigned
+ * to start observing the process via
+ * {@link KillableObserver#startObserving(Killable)} but will only be
+ * unassigned via {@link KillableObserver#stopObserving(Killable)}, if
+ * {@link FutureCommandResult#get()} is called. If the
+ * {@link KillableObserver} implementation used with this method will
+ * not work correctly without calls to
+ * {@link KillableObserver#stopObserving(Killable)} then a new instance
+ * should be used for each call to this method.</p>
+ *
+ * @param stdinInput The input to this process's stdin
+ * @param observer {@link KillableObserver} that should observe the running
+ * process, or {@link #NO_OBSERVER} if caller does not wish to kill
+ * the process
+ * @param stdOut the process will write its standard output into this stream.
+ * E.g., you could pass {@link System#out} as <code>stdOut</code>.
+ * @param stdErr the process will write its standard error into this stream.
+ * E.g., you could pass {@link System#err} as <code>stdErr</code>.
+ * @return An object that can be used to check if the process terminated and
+ * obtain the process results.
+ * @throws ExecFailedException if {@link Runtime#exec(String[])} fails for any
+ * reason
+ * @throws NullPointerException if stdin is null
+ */
+ public FutureCommandResult executeAsynchronously(final InputStream stdinInput,
+ final KillableObserver observer,
+ final OutputStream stdOut,
+ final OutputStream stdErr)
+ throws CommandException {
+ // supporting "null" here for backwards compatibility
+ final KillableObserver theObserver =
+ observer == null ? NO_OBSERVER : observer;
+ nullCheck(stdinInput, "stdinInput");
+ return doExecute(new InputStreamInputSource(stdinInput),
+ theObserver,
+ Consumers.createStreamingConsumers(stdOut, stdErr),
+ /*killSubprocess=*/false, /*closeOutput=*/false);
+ }
+
+ // End of public API -------------------------------------------------------
+
+ private void nullCheck(Object argument, String argumentName) {
+ if (argument == null) {
+ String message = argumentName + " argument must not be null.";
+ throw new NullPointerException(message);
+ }
+ }
+
+ private FutureCommandResult doExecute(final InputSource stdinInput,
+ final KillableObserver observer,
+ final Consumers.OutErrConsumers outErrConsumers,
+ final boolean killSubprocessOnInterrupt,
+ final boolean closeOutputStreams)
+ throws CommandException {
+
+ logCommand();
+
+ final Process process = startProcess();
+
+ outErrConsumers.logConsumptionStrategy();
+
+ outErrConsumers.registerInputs(process.getInputStream(),
+ process.getErrorStream(),
+ closeOutputStreams);
+
+ processInput(stdinInput, process);
+
+ // TODO(bazel-team): if the input stream is unbounded, observers will not get start
+ // notification in a timely manner!
+ final Killable processKillable = observeProcess(process, observer);
+
+ return new FutureCommandResult() {
+ @Override
+ public CommandResult get() throws AbnormalTerminationException {
+ return waitForProcessToComplete(process,
+ observer,
+ processKillable,
+ outErrConsumers,
+ killSubprocessOnInterrupt);
+ }
+
+ @Override
+ public boolean isDone() {
+ try {
+ // exitValue seems to be the only non-blocking call for
+ // checking process liveness.
+ process.exitValue();
+ return true;
+ } catch (IllegalThreadStateException e) {
+ return false;
+ }
+ }
+ };
+ }
+
+ private Process startProcess()
+ throws ExecFailedException {
+ try {
+ return processBuilder.start();
+ } catch (IOException ioe) {
+ throw new ExecFailedException(this, ioe);
+ }
+ }
+
+ private static interface InputSource {
+ void copyTo(OutputStream out) throws IOException;
+ boolean isEmpty();
+ String toLogString(String sourceName);
+ }
+
+ private static class ByteArrayInputSource implements InputSource {
+ private byte[] bytes;
+ ByteArrayInputSource(byte[] bytes){
+ this.bytes = bytes;
+ }
+ @Override
+ public void copyTo(OutputStream out) throws IOException {
+ out.write(bytes);
+ out.flush();
+ }
+ @Override
+ public boolean isEmpty() {
+ return bytes.length == 0;
+ }
+ @Override
+ public String toLogString(String sourceName) {
+ if (isEmpty()) {
+ return "No input to " + sourceName;
+ } else {
+ return "Input to " + sourceName + ": " +
+ LogUtil.toTruncatedString(bytes);
+ }
+ }
+ }
+
+ private static class InputStreamInputSource implements InputSource {
+ private InputStream inputStream;
+ InputStreamInputSource(InputStream inputStream){
+ this.inputStream = inputStream;
+ }
+ @Override
+ public void copyTo(OutputStream out) throws IOException {
+ byte[] buf = new byte[4096];
+ int r;
+ while ((r = inputStream.read(buf)) != -1) {
+ out.write(buf, 0, r);
+ out.flush();
+ }
+ }
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+ @Override
+ public String toLogString(String sourceName) {
+ return "Input to " + sourceName + " is a stream.";
+ }
+ }
+
+ private static void processInput(final InputSource stdinInput,
+ final Process process) {
+ if (log.isLoggable(Level.FINER)) {
+ log.finer(stdinInput.toLogString("stdin"));
+ }
+ try {
+ if (stdinInput.isEmpty()) {
+ return;
+ }
+ stdinInput.copyTo(process.getOutputStream());
+ } catch (IOException ioe) {
+ // Note: this is not an error! Perhaps the command just isn't hungry for
+ // our input and exited with success. Process.waitFor (later) will tell
+ // us.
+ //
+ // (Unlike out/err streams, which are read asynchronously, the input stream is written
+ // synchronously, in its entirety, before processInput returns. If the input is
+ // infinite, and is passed through e.g. "cat" subprocess and back into the
+ // ByteArrayOutputStream, that will eventually run out of memory, causing the output stream
+ // to be closed, "cat" to terminate with SIGPIPE, and processInput to receive an IOException.
+ } finally {
+ // if this statement is ever deleted, the process's outputStream
+ // must be closed elsewhere -- it is not closed automatically
+ Command.silentClose(process.getOutputStream());
+ }
+ }
+
+ private static Killable observeProcess(final Process process,
+ final KillableObserver observer) {
+ final Killable processKillable = new ProcessKillable(process);
+ observer.startObserving(processKillable);
+ return processKillable;
+ }
+
+ private CommandResult waitForProcessToComplete(
+ final Process process,
+ final KillableObserver observer,
+ final Killable processKillable,
+ final Consumers.OutErrConsumers outErr,
+ final boolean killSubprocessOnInterrupt)
+ throws AbnormalTerminationException {
+
+ log.finer("Waiting for process...");
+
+ TerminationStatus status =
+ waitForProcess(process, killSubprocessOnInterrupt);
+
+ observer.stopObserving(processKillable);
+
+ log.finer(status.toString());
+
+ try {
+ outErr.waitForCompletion();
+ } catch (IOException ioe) {
+ CommandResult noOutputResult =
+ new CommandResult(CommandResult.EMPTY_OUTPUT,
+ CommandResult.EMPTY_OUTPUT,
+ status);
+ if (status.success()) {
+ // If command was otherwise successful, throw an exception about this
+ throw new AbnormalTerminationException(this, noOutputResult, ioe);
+ } else {
+ // Otherwise, throw the more important exception -- command
+ // was not successful
+ String message = status
+ + "; also encountered an error while attempting to retrieve output";
+ throw status.exited()
+ ? new BadExitStatusException(this, noOutputResult, message, ioe)
+ : new AbnormalTerminationException(this,
+ noOutputResult, message, ioe);
+ }
+ }
+
+ CommandResult result = new CommandResult(outErr.getAccumulatedOut(),
+ outErr.getAccumulatedErr(),
+ status);
+ result.logThis();
+ if (status.success()) {
+ return result;
+ } else if (status.exited()) {
+ throw new BadExitStatusException(this, result, status.toString());
+ } else {
+ throw new AbnormalTerminationException(this, result, status.toString());
+ }
+ }
+
+ private static TerminationStatus waitForProcess(Process process,
+ boolean killSubprocessOnInterrupt) {
+ boolean wasInterrupted = false;
+ try {
+ while (true) {
+ try {
+ return new TerminationStatus(process.waitFor());
+ } catch (InterruptedException ie) {
+ wasInterrupted = true;
+ if (killSubprocessOnInterrupt) {
+ process.destroy();
+ }
+ }
+ }
+ } finally {
+ // Read this for detailed explanation:
+ // http://www-128.ibm.com/developerworks/java/library/j-jtp05236.html
+ if (wasInterrupted) {
+ Thread.currentThread().interrupt(); // preserve interrupted status
+ }
+ }
+ }
+
+ private void logCommand() {
+ if (!log.isLoggable(Level.FINE)) {
+ return;
+ }
+ log.fine(toDebugString());
+ }
+
+ /**
+ * A string representation of this command object which includes
+ * the arguments, the environment, and the working directory. Avoid
+ * relying on the specifics of this format. Note that the size
+ * of the result string will reflect the size of the command.
+ */
+ public String toDebugString() {
+ StringBuilder message = new StringBuilder(128);
+ message.append("Executing (without brackets):");
+ for (final String arg : processBuilder.command()) {
+ message.append(" [");
+ message.append(arg);
+ message.append(']');
+ }
+ message.append("; environment: ");
+ message.append(processBuilder.environment().toString());
+ final File workingDirectory = processBuilder.directory();
+ message.append("; working dir: ");
+ message.append(workingDirectory == null ?
+ "(current)" :
+ workingDirectory.toString());
+ return message.toString();
+ }
+
+ /**
+ * Close the <code>out</code> stream and log a warning if anything happens.
+ */
+ private static void silentClose(final OutputStream out) {
+ try {
+ out.close();
+ } catch (IOException ioe) {
+ String message = "Unexpected exception while closing output stream";
+ log.log(Level.WARNING, message, ioe);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/CommandException.java b/src/main/java/com/google/devtools/build/lib/shell/CommandException.java
new file mode 100644
index 0000000000..a11be977dd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/CommandException.java
@@ -0,0 +1,48 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Superclass of all exceptions that may be thrown during command execution.
+ * It exists to unify them. It also provides access to the command name
+ * and arguments for the failing command.
+ */
+public class CommandException extends Exception {
+
+ private final Command command;
+
+ /** Returns the command that failed. */
+ public Command getCommand() {
+ return command;
+ }
+
+ public CommandException(Command command, final String message) {
+ super(message);
+ this.command = command;
+ }
+
+ public CommandException(Command command, final Throwable cause) {
+ super(cause);
+ this.command = command;
+ }
+
+ public CommandException(Command command, final String message,
+ final Throwable cause) {
+ super(message, cause);
+ this.command = command;
+ }
+
+ private static final long serialVersionUID = 2L;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/CommandResult.java b/src/main/java/com/google/devtools/build/lib/shell/CommandResult.java
new file mode 100644
index 0000000000..185f91df6e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/CommandResult.java
@@ -0,0 +1,116 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.ByteArrayOutputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Encapsulates the results of a command execution, including exit status
+ * and output to stdout and stderr.
+ */
+public final class CommandResult {
+
+ private static final Logger log =
+ Logger.getLogger("com.google.devtools.build.lib.shell.Command");
+
+ private static final byte[] NO_BYTES = new byte[0];
+
+ static final ByteArrayOutputStream EMPTY_OUTPUT =
+ new ByteArrayOutputStream() {
+
+ @Override
+ public byte[] toByteArray() {
+ return NO_BYTES;
+ }
+ };
+
+ static final ByteArrayOutputStream NO_OUTPUT_COLLECTED =
+ new ByteArrayOutputStream(){
+
+ @Override
+ public byte[] toByteArray() {
+ throw new IllegalStateException("Output was not collected");
+ }
+ };
+
+ private final ByteArrayOutputStream stdout;
+ private final ByteArrayOutputStream stderr;
+ private final TerminationStatus terminationStatus;
+
+ CommandResult(final ByteArrayOutputStream stdout,
+ final ByteArrayOutputStream stderr,
+ final TerminationStatus terminationStatus) {
+ checkNotNull(stdout);
+ checkNotNull(stderr);
+ checkNotNull(terminationStatus);
+ this.stdout = stdout;
+ this.stderr = stderr;
+ this.terminationStatus = terminationStatus;
+ }
+
+ /**
+ * @return raw bytes that were written to stdout by the command, or
+ * null if caller did chose to ignore output
+ * @throws IllegalStateException if output was not collected
+ */
+ public byte[] getStdout() {
+ return stdout.toByteArray();
+ }
+
+ /**
+ * @return raw bytes that were written to stderr by the command, or
+ * null if caller did chose to ignore output
+ * @throws IllegalStateException if output was not collected
+ */
+ public byte[] getStderr() {
+ return stderr.toByteArray();
+ }
+
+ /**
+ * @return the result of Process.waitFor for the subprocess.
+ * @deprecated this returns the result of Process.waitFor, which is not
+ * precisely defined, and is not to be confused with the value passed to
+ * exit(2) by the subprocess. Use getTerminationStatus() instead.
+ */
+ @Deprecated
+ public int getExitStatus() {
+ return terminationStatus.getRawResult();
+ }
+
+ /**
+ * @return the termination status of the subprocess.
+ */
+ public TerminationStatus getTerminationStatus() {
+ return terminationStatus;
+ }
+
+ void logThis() {
+ if (!log.isLoggable(Level.FINER)) {
+ return;
+ }
+ log.finer(terminationStatus.toString());
+
+ if (stdout == NO_OUTPUT_COLLECTED) {
+ return;
+ }
+ log.finer("Stdout: " + LogUtil.toTruncatedString(stdout.toByteArray()));
+ log.finer("Stderr: " + LogUtil.toTruncatedString(stderr.toByteArray()));
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/Consumers.java b/src/main/java/com/google/devtools/build/lib/shell/Consumers.java
new file mode 100644
index 0000000000..3ed5b7e5ae
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/Consumers.java
@@ -0,0 +1,359 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.shell;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This class provides convenience methods for consuming (actively reading)
+ * output and error streams with different consumption policies:
+ * discarding ({@link #createDiscardingConsumers()},
+ * accumulating ({@link #createAccumulatingConsumers()},
+ * and streaming ({@link #createStreamingConsumers(OutputStream, OutputStream)}).
+ */
+class Consumers {
+
+ private static final Logger log =
+ Logger.getLogger("com.google.devtools.build.lib.shell.Command");
+
+ private Consumers() {}
+
+ private static final ExecutorService pool =
+ Executors.newCachedThreadPool(new AccumulatorThreadFactory());
+
+ static OutErrConsumers createDiscardingConsumers() {
+ return new OutErrConsumers(new DiscardingConsumer(),
+ new DiscardingConsumer());
+ }
+
+ static OutErrConsumers createAccumulatingConsumers() {
+ return new OutErrConsumers(new AccumulatingConsumer(),
+ new AccumulatingConsumer());
+ }
+
+ static OutErrConsumers createStreamingConsumers(OutputStream out,
+ OutputStream err) {
+ return new OutErrConsumers(new StreamingConsumer(out),
+ new StreamingConsumer(err));
+ }
+
+ static class OutErrConsumers {
+
+ private final OutputConsumer out;
+ private final OutputConsumer err;
+
+ private OutErrConsumers(final OutputConsumer out, final OutputConsumer err){
+ this.out = out;
+ this.err = err;
+ }
+
+ void registerInputs(InputStream outInput, InputStream errInput, boolean closeStreams){
+ out.registerInput(outInput, closeStreams);
+ err.registerInput(errInput, closeStreams);
+ }
+
+ void cancel() {
+ out.cancel();
+ err.cancel();
+ }
+
+ void waitForCompletion() throws IOException {
+ out.waitForCompletion();
+ err.waitForCompletion();
+ }
+
+ ByteArrayOutputStream getAccumulatedOut(){
+ return out.getAccumulatedOut();
+ }
+
+ ByteArrayOutputStream getAccumulatedErr() {
+ return err.getAccumulatedOut();
+ }
+
+ void logConsumptionStrategy() {
+ // The creation methods guarantee that the consumption strategy is
+ // the same for out and err - doesn't matter whether we call out or err,
+ // let's pick out.
+ out.logConsumptionStrategy();
+ }
+
+ }
+
+ /**
+ * This interface describes just one consumer, which consumes the
+ * InputStream provided by {@link #registerInput(InputStream, boolean)}.
+ * Implementations implement different consumption strategies.
+ */
+ private static interface OutputConsumer {
+ /**
+ * Returns whatever the consumer accumulated internally, or
+ * {@link CommandResult#NO_OUTPUT_COLLECTED} if it doesn't accumulate
+ * any output.
+ *
+ * @see AccumulatingConsumer
+ */
+ ByteArrayOutputStream getAccumulatedOut();
+
+ void logConsumptionStrategy();
+
+ void registerInput(InputStream in, boolean closeConsumer);
+
+ void cancel();
+
+ void waitForCompletion() throws IOException;
+ }
+
+ /**
+ * This consumer sends the input to a stream while consuming it.
+ */
+ private static class StreamingConsumer extends FutureConsumption
+ implements OutputConsumer {
+ private OutputStream out;
+
+ StreamingConsumer(OutputStream out) {
+ this.out = out;
+ }
+
+ @Override
+ public ByteArrayOutputStream getAccumulatedOut() {
+ return CommandResult.NO_OUTPUT_COLLECTED;
+ }
+
+ @Override
+ public void logConsumptionStrategy() {
+ log.finer("Output will be sent to streams provided by client");
+ }
+
+ @Override protected Runnable createConsumingAndClosingSink(InputStream in,
+ boolean closeConsumer) {
+ return new ClosingSink(in, out, closeConsumer);
+ }
+ }
+
+ /**
+ * This consumer sends the input to a {@link ByteArrayOutputStream}
+ * while consuming it. This accumulated stream can be obtained by
+ * calling {@link #getAccumulatedOut()}.
+ */
+ private static class AccumulatingConsumer extends FutureConsumption
+ implements OutputConsumer {
+ private ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ @Override
+ public ByteArrayOutputStream getAccumulatedOut() {
+ return out;
+ }
+
+ @Override
+ public void logConsumptionStrategy() {
+ log.finer("Output will be accumulated (promptly read off) and returned");
+ }
+
+ @Override public Runnable createConsumingAndClosingSink(InputStream in, boolean closeConsumer) {
+ return new ClosingSink(in, out);
+ }
+ }
+
+ /**
+ * This consumer just discards whatever it reads.
+ */
+ private static class DiscardingConsumer extends FutureConsumption
+ implements OutputConsumer {
+ private DiscardingConsumer() {
+ }
+
+ @Override
+ public ByteArrayOutputStream getAccumulatedOut() {
+ return CommandResult.NO_OUTPUT_COLLECTED;
+ }
+
+ @Override
+ public void logConsumptionStrategy() {
+ log.finer("Output will be ignored");
+ }
+
+ @Override public Runnable createConsumingAndClosingSink(InputStream in, boolean closeConsumer) {
+ return new ClosingSink(in);
+ }
+ }
+
+ /**
+ * A mixin that makes consumers active - this is where we kick of
+ * multithreading ({@link #registerInput(InputStream, boolean)}), cancel actions
+ * and wait for the consumers to complete.
+ */
+ private abstract static class FutureConsumption implements OutputConsumer {
+
+ private Future<?> future;
+
+ @Override
+ public void registerInput(InputStream in, boolean closeConsumer){
+ Runnable sink = createConsumingAndClosingSink(in, closeConsumer);
+ future = pool.submit(sink);
+ }
+
+ protected abstract Runnable createConsumingAndClosingSink(InputStream in, boolean close);
+
+ @Override
+ public void cancel() {
+ future.cancel(true);
+ }
+
+ @Override
+ public void waitForCompletion() throws IOException {
+ boolean wasInterrupted = false;
+ try {
+ while (true) {
+ try {
+ future.get();
+ break;
+ } catch (InterruptedException ie) {
+ wasInterrupted = true;
+ // continue waiting
+ } catch (ExecutionException ee) {
+ // Runnable threw a RuntimeException
+ Throwable nested = ee.getCause();
+ if (nested instanceof RuntimeException) {
+ final RuntimeException re = (RuntimeException) nested;
+ // The stream sink classes, unfortunately, tunnel IOExceptions
+ // out of run() in a RuntimeException. If that's the case,
+ // unpack and re-throw the IOException. Otherwise, re-throw
+ // this unexpected RuntimeException
+ final Throwable cause = re.getCause();
+ if (cause instanceof IOException) {
+ throw (IOException) cause;
+ } else {
+ throw re;
+ }
+ } else if (nested instanceof OutOfMemoryError) {
+ // OutOfMemoryError does not support exception chaining.
+ throw (OutOfMemoryError) nested;
+ } else if (nested instanceof Error) {
+ throw new Error("unhandled Error in worker thread", ee);
+ } else {
+ throw new RuntimeException("unknown execution problem", ee);
+ }
+ }
+ }
+ } finally {
+ // Read this for detailed explanation:
+ // http://www-128.ibm.com/developerworks/java/library/j-jtp05236.html
+ if (wasInterrupted) {
+ Thread.currentThread().interrupt(); // preserve interrupted status
+ }
+ }
+ }
+ }
+
+ /**
+ * Factory which produces threads with a 32K stack size.
+ */
+ private static class AccumulatorThreadFactory implements ThreadFactory {
+
+ private static final int THREAD_STACK_SIZE = 32 * 1024;
+
+ private static int threadInitNumber;
+
+ private static synchronized int nextThreadNum() {
+ return threadInitNumber++;
+ }
+
+ @Override
+ public Thread newThread(final Runnable runnable) {
+ final Thread t =
+ new Thread(null,
+ runnable,
+ "Command-Accumulator-Thread-" + nextThreadNum(),
+ THREAD_STACK_SIZE);
+ // Don't let this thread hold up JVM exit
+ t.setDaemon(true);
+ return t;
+ }
+
+ }
+
+ /**
+ * A sink that closes its input stream once its done.
+ */
+ private static class ClosingSink implements Runnable {
+
+ private final InputStream in;
+ private final OutputStream out;
+ private final Runnable sink;
+ private final boolean close;
+
+ /**
+ * Creates a sink that will pump InputStream <code>in</code>
+ * into OutputStream <code>out</code>.
+ */
+ ClosingSink(final InputStream in, OutputStream out) {
+ this(in, out, false);
+ }
+
+ /**
+ * Creates a sink that will read <code>in</code> and discard it.
+ */
+ ClosingSink(final InputStream in) {
+ this.sink = InputStreamSink.newRunnableSink(in);
+ this.in = in;
+ this.close = false;
+ this.out = null;
+ }
+
+ ClosingSink(final InputStream in, OutputStream out, boolean close){
+ this.sink = InputStreamSink.newRunnableSink(in, out);
+ this.in = in;
+ this.out = out;
+ this.close = close;
+ }
+
+
+ @Override
+ public void run() {
+ try {
+ sink.run();
+ } finally {
+ silentClose(in);
+ if (close && out != null) {
+ silentClose(out);
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Close the <code>in</code> stream and log a warning if anything happens.
+ */
+ private static void silentClose(final Closeable closeable) {
+ try {
+ closeable.close();
+ } catch (IOException ioe) {
+ String message = "Unexpected exception while closing input stream";
+ log.log(Level.WARNING, message, ioe);
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/ExecFailedException.java b/src/main/java/com/google/devtools/build/lib/shell/ExecFailedException.java
new file mode 100644
index 0000000000..24f42a603c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/ExecFailedException.java
@@ -0,0 +1,28 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Thrown when a command could not even be executed by the JVM --
+ * in particular, when {@link Runtime#exec(String[])} fails.
+ */
+public final class ExecFailedException extends CommandException {
+
+ public ExecFailedException(Command command, final Throwable cause) {
+ super(command, cause);
+ }
+
+ private static final long serialVersionUID = 2L;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/FutureCommandResult.java b/src/main/java/com/google/devtools/build/lib/shell/FutureCommandResult.java
new file mode 100644
index 0000000000..3e1f5c9a5f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/FutureCommandResult.java
@@ -0,0 +1,40 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Supplier of the command result which additionally allows to check if
+ * the command already terminated. Implementing full fledged Future would
+ * be a much harder undertaking, so a bare minimum that makes this class still
+ * useful for asynchronous command execution is implemented.
+ */
+public interface FutureCommandResult {
+ /**
+ * Returns the result of command execution. If the process is not finished
+ * yet (as reported by {@link #isDone()}, the call will block until that
+ * process terminates.
+ *
+ * @return non-null result of command execution
+ * @throws AbnormalTerminationException if command execution failed
+ */
+ CommandResult get() throws AbnormalTerminationException;
+
+ /**
+ * Returns true if the process terminated, the command result is available
+ * and the call to {@link #get()} will not block.
+ *
+ * @return true if the process terminated
+ */
+ boolean isDone();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/InputStreamSink.java b/src/main/java/com/google/devtools/build/lib/shell/InputStreamSink.java
new file mode 100644
index 0000000000..c35552b62b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/InputStreamSink.java
@@ -0,0 +1,133 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Provides sinks for input streams. Continuously read an input stream
+ * until the end-of-file is encountered. The stream may be redirected to
+ * an {@link OutputStream}, or discarded.
+ * <p>
+ * This class is useful for handing the {@code stdout} and {@code stderr}
+ * streams from a {@link Process} started with {@link Runtime#exec(String)}.
+ * If these streams are not consumed, the Process may block resulting in a
+ * deadlock.
+ *
+ * @see <a href="http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html">
+ * JavaWorld: When Runtime.exec() won&apos;t</a>
+ */
+public final class InputStreamSink {
+
+ /**
+ * Black hole into which bytes are sometimes discarded by {@link NullSink}.
+ * It is shared by all threads since the actual contents of the buffer
+ * are irrelevant.
+ */
+ private static final byte[] DISCARD = new byte[4096];
+
+ // Supresses default constructor; ensures non-instantiability
+ private InputStreamSink() {
+ }
+
+ /**
+ * A {@link Thread} which reads and discards data from an
+ * {@link InputStream}.
+ */
+ private static class NullSink implements Runnable {
+ private final InputStream in;
+
+ public NullSink(InputStream in) {
+ this.in = in;
+ }
+
+ @Override
+ public void run() {
+ try {
+ try {
+ // Attempt to just skip all input
+ do {
+ in.skip(Integer.MAX_VALUE);
+ } while (in.read() != -1); // Need to test for EOF
+ } catch (IOException ioe) {
+ // Some streams throw IOException when skip() is called;
+ // resort to reading off all input with read():
+ while (in.read(DISCARD) != -1) {
+ // no loop body
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * A {@link Thread} which reads data from an {@link InputStream},
+ * and translates it into an {@link OutputStream}.
+ */
+ private static class CopySink implements Runnable {
+
+ private final InputStream in;
+ private final OutputStream out;
+
+ public CopySink(InputStream in, OutputStream out) {
+ this.in = in;
+ this.out = out;
+ }
+
+ @Override
+ public void run() {
+ try {
+ byte[] buffer = new byte[2048];
+ int bytesRead;
+ while ((bytesRead = in.read(buffer)) >= 0) {
+ out.write(buffer, 0, bytesRead);
+ out.flush();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * Creates a {@link Runnable} which consumes the provided
+ * {@link InputStream} 'in', discarding its contents.
+ */
+ public static Runnable newRunnableSink(InputStream in) {
+ if (in == null) {
+ throw new NullPointerException("in");
+ }
+ return new NullSink(in);
+ }
+
+ /**
+ * Creates a {@link Runnable} which copies everything from 'in'
+ * to 'out'. 'out' will be written to and flushed after each
+ * read from 'in'. However, 'out' will not be closed.
+ */
+ public static Runnable newRunnableSink(InputStream in, OutputStream out) {
+ if (in == null) {
+ throw new NullPointerException("in");
+ }
+ if (out == null) {
+ throw new NullPointerException("out");
+ }
+ return new CopySink(in, out);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/Killable.java b/src/main/java/com/google/devtools/build/lib/shell/Killable.java
new file mode 100644
index 0000000000..66d1146b2c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/Killable.java
@@ -0,0 +1,31 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Implementations encapsulate a running process that can be killed.
+ * In particular, here, it is used to wrap up a {@link Process} object
+ * and expose it to a {@link KillableObserver}. It is wrapped in this way
+ * so that the actual {@link Process} object can't be altered by
+ * a {@link KillableObserver}.
+ */
+public interface Killable {
+
+ /**
+ * Kill this killable instance.
+ */
+ void kill();
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/KillableObserver.java b/src/main/java/com/google/devtools/build/lib/shell/KillableObserver.java
new file mode 100644
index 0000000000..62d9aa0588
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/KillableObserver.java
@@ -0,0 +1,49 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Implementations of this interface observe, and potentially kill,
+ * a {@link Killable} object. This is the mechanism by which "kill"
+ * functionality is exposed to callers in the
+ * {@link Command#execute(byte[], KillableObserver, boolean)} method.
+ *
+ */
+public interface KillableObserver {
+
+ /**
+ * <p>Begin observing the given {@link Killable}. This method must return
+ * promptly; until it returns, {@link Command#execute()} cannot complete.
+ * Implementations may wish to start a new {@link Thread} here to handle
+ * kill logic, and to interrupt or otherwise ask the thread to stop in the
+ * {@link #stopObserving(Killable)} method. See
+ * <a href="http://builder.com.com/5100-6370-5144546.html">
+ * Interrupting Java threads</a> for notes on how to implement this
+ * correctly.</p>
+ *
+ * <p>Implementations may or may not be able to observe more than
+ * one {@link Killable} at a time; see javadoc for details.</p>
+ *
+ * @param killable killable to observer
+ */
+ void startObserving(Killable killable);
+
+ /**
+ * Stop observing the given {@link Killable}, since it is
+ * no longer active.
+ */
+ void stopObserving(Killable killable);
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/LogUtil.java b/src/main/java/com/google/devtools/build/lib/shell/LogUtil.java
new file mode 100644
index 0000000000..ab646f66ae
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/LogUtil.java
@@ -0,0 +1,54 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Utilities for logging.
+ */
+class LogUtil {
+
+ private LogUtil() {}
+
+ private final static int TRUNCATE_STRINGS_AT = 150;
+
+ /**
+ * Make a string out of a byte array, and truncate it to a reasonable length.
+ * Useful for preventing logs from becoming excessively large.
+ */
+ static String toTruncatedString(final byte[] bytes) {
+ if(bytes == null || bytes.length == 0) {
+ return "";
+ }
+ /*
+ * Yes, we'll use the platform encoding here, and this is one of the rare
+ * cases where it makes sense. You want the logs to be encoded so that
+ * your platform tools (vi, emacs, cat) can render them, don't you?
+ * In practice, this means ISO-8859-1 or UTF-8, I guess.
+ */
+ try {
+ if (bytes.length > TRUNCATE_STRINGS_AT) {
+ return new String(bytes, 0, TRUNCATE_STRINGS_AT)
+ + "[... truncated. original size was " + bytes.length + " bytes.]";
+ }
+ return new String(bytes);
+ } catch (Exception e) {
+ /*
+ * In case encoding a binary string doesn't work for some reason, we
+ * don't want to bring a logging server down - do we? So we're paranoid.
+ */
+ return "IOUtil.toTruncatedString: " + e.getMessage();
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/ProcessKillable.java b/src/main/java/com/google/devtools/build/lib/shell/ProcessKillable.java
new file mode 100644
index 0000000000..5d0cb8f87e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/ProcessKillable.java
@@ -0,0 +1,36 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * {@link Killable} implementation which simply wraps a
+ * {@link Process} instance.
+ */
+final class ProcessKillable implements Killable {
+
+ private final Process process;
+
+ ProcessKillable(final Process process) {
+ this.process = process;
+ }
+
+ /**
+ * Calls {@link Process#destroy()}.
+ */
+ @Override
+ public void kill() {
+ process.destroy();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/Shell.java b/src/main/java/com/google/devtools/build/lib/shell/Shell.java
new file mode 100644
index 0000000000..2cae24ec15
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/Shell.java
@@ -0,0 +1,132 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+import java.util.logging.Logger;
+
+/**
+ * <p>Represents an OS shell, such as "cmd" on Windows or "sh" on Unix-like
+ * platforms. Currently, Linux and Windows XP are supported.</p>
+ *
+ * <p>This class encapsulates shell-specific logic, like how to
+ * create a command line that uses the shell to invoke another command.
+ */
+public abstract class Shell {
+
+ private static final Logger log =
+ Logger.getLogger("com.google.devtools.build.lib.shell.Shell");
+
+ private static final Shell platformShell;
+
+ static {
+ final String osName = System.getProperty("os.name");
+ if ("Linux".equals(osName)) {
+ platformShell = new SHShell();
+ } else if ("Windows XP".equals(osName)) {
+ platformShell = new WindowsCMDShell();
+ } else {
+ log.severe("OS not supported; will not be able to execute commands");
+ platformShell = null;
+ }
+ log.config("Loaded shell support '" + platformShell +
+ "' for OS '" + osName + "'");
+ }
+
+ private Shell() {
+ // do nothing
+ }
+
+ /**
+ * @return {@link Shell} subclass appropriate for the current platform
+ * @throws UnsupportedOperationException if no such subclass exists
+ */
+ public static Shell getPlatformShell() {
+ if (platformShell == null) {
+ throw new UnsupportedOperationException("OS is not supported");
+ }
+ return platformShell;
+ }
+
+ /**
+ * Creates a command line suitable for execution by
+ * {@link Runtime#exec(String[])} from the given command string,
+ * a command line which uses a shell appropriate for a particular
+ * platform to execute the command (e.g. "/bin/sh" on Linux).
+ *
+ * @param command command for which to create a command line
+ * @return String[] suitable for execution by
+ * {@link Runtime#exec(String[])}
+ */
+ public abstract String[] shellify(final String command);
+
+
+ /**
+ * Represents the <code>sh</code> shell commonly found on Unix-like
+ * operating systems, including Linux.
+ */
+ private static final class SHShell extends Shell {
+
+ /**
+ * <p>Returns a command line which uses <code>cmd</code> to execute
+ * the {@link Command}. Given the command <code>foo bar baz</code>,
+ * for example, this will return a String array corresponding
+ * to the command line:</p>
+ *
+ * <p><code>/bin/sh -c "foo bar baz"</code></p>
+ *
+ * <p>That is, it always returns a 3-element array.</p>
+ *
+ * @param command command for which to create a command line
+ * @return String[] suitable for execution by
+ * {@link Runtime#exec(String[])}
+ */
+ @Override public String[] shellify(final String command) {
+ if (command == null || command.length() == 0) {
+ throw new IllegalArgumentException("command is null or empty");
+ }
+ return new String[] { "/bin/sh", "-c", command };
+ }
+
+ }
+
+ /**
+ * Represents the Windows command shell <code>cmd</code>.
+ */
+ private static final class WindowsCMDShell extends Shell {
+
+ /**
+ * <p>Returns a command line which uses <code>cmd</code> to execute
+ * the {@link Command}. Given the command <code>foo bar baz</code>,
+ * for example, this will return a String array corresponding
+ * to the command line:</p>
+ *
+ * <p><code>cmd /S /C "foo bar baz"</code></p>
+ *
+ * <p>That is, it always returns a 4-element array.</p>
+ *
+ * @param command command for which to create a command line
+ * @return String[] suitable for execution by
+ * {@link Runtime#exec(String[])}
+ */
+ @Override public String[] shellify(final String command) {
+ if (command == null || command.length() == 0) {
+ throw new IllegalArgumentException("command is null or empty");
+ }
+ return new String[] { "cmd", "/S", "/C", command };
+ }
+
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/ShellUtils.java b/src/main/java/com/google/devtools/build/lib/shell/ShellUtils.java
new file mode 100644
index 0000000000..5157f34b05
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/ShellUtils.java
@@ -0,0 +1,145 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+import java.util.List;
+
+/**
+ * Utility functions for Bourne shell commands, including escaping and
+ * tokenizing.
+ */
+public abstract class ShellUtils {
+
+ private ShellUtils() {}
+
+ /**
+ * Characters that have no special meaning to the shell.
+ */
+ private static final String SAFE_PUNCTUATION = "@%-_+:,./";
+
+ /**
+ * Quotes a word so that it can be used, without further quoting,
+ * as an argument (or part of an argument) in a shell command.
+ */
+ public static String shellEscape(String word) {
+ int len = word.length();
+ if (len == 0) {
+ // Empty string is a special case: needs to be quoted to ensure that it gets
+ // treated as a separate argument.
+ return "''";
+ }
+ for (int ii = 0; ii < len; ii++) {
+ char c = word.charAt(ii);
+ // We do this positively so as to be sure we don't inadvertently forget
+ // any unsafe characters.
+ if (!Character.isLetterOrDigit(c) && SAFE_PUNCTUATION.indexOf(c) == -1) {
+ // replace() actually means "replace all".
+ return "'" + word.replace("'", "'\\''") + "'";
+ }
+ }
+ return word;
+ }
+
+ /**
+ * Given an argv array such as might be passed to execve(2), returns a string
+ * that can be copied and pasted into a Bourne shell for a similar effect.
+ */
+ public static String prettyPrintArgv(List<String> argv) {
+ StringBuilder buf = new StringBuilder();
+ for (String arg: argv) {
+ if (buf.length() > 0) {
+ buf.append(' ');
+ }
+ buf.append(shellEscape(arg));
+ }
+ return buf.toString();
+ }
+
+
+ /**
+ * Thrown by tokenize method if there is an error
+ */
+ public static class TokenizationException extends Exception {
+ TokenizationException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Populates the passed list of command-line options extracted from {@code
+ * optionString}, which is a string containing multiple options, delimited in
+ * a Bourne shell-like manner.
+ *
+ * @param options the list to be populated with tokens.
+ * @param optionString the string to be tokenized.
+ * @throws TokenizationException if there was an error (such as an
+ * unterminated quotation).
+ */
+ public static void tokenize(List<String> options, String optionString)
+ throws TokenizationException {
+ // See test suite for examples.
+ //
+ // Note: backslash escapes the following character, except within a
+ // single-quoted region where it is literal.
+
+ StringBuilder token = new StringBuilder();
+ boolean forceToken = false;
+ char quotation = '\0'; // NUL, '\'' or '"'
+ for (int ii = 0, len = optionString.length(); ii < len; ii++) {
+ char c = optionString.charAt(ii);
+ if (quotation != '\0') { // in quotation
+ if (c == quotation) { // end of quotation
+ quotation = '\0';
+ } else if (c == '\\' && quotation == '"') { // backslash in "-quotation
+ if (++ii == len) {
+ throw new TokenizationException("backslash at end of string");
+ }
+ c = optionString.charAt(ii);
+ if (c != '\\' && c != '"') {
+ token.append('\\');
+ }
+ token.append(c);
+ } else { // regular char, in quotation
+ token.append(c);
+ }
+ } else { // not in quotation
+ if (c == '\'' || c == '"') { // begin single/double quotation
+ quotation = c;
+ forceToken = true;
+ } else if (c == ' ' || c == '\t') { // space, not quoted
+ if (forceToken || token.length() > 0) {
+ options.add(token.toString());
+ token = new StringBuilder();
+ forceToken = false;
+ }
+ } else if (c == '\\') { // backslash, not quoted
+ if (++ii == len) {
+ throw new TokenizationException("backslash at end of string");
+ }
+ token.append(optionString.charAt(ii));
+ } else { // regular char, not quoted
+ token.append(c);
+ }
+ }
+ }
+ if (quotation != '\0') {
+ throw new TokenizationException("unterminated quotation");
+ }
+ if (forceToken || token.length() > 0) {
+ options.add(token.toString());
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/SimpleKillableObserver.java b/src/main/java/com/google/devtools/build/lib/shell/SimpleKillableObserver.java
new file mode 100644
index 0000000000..85794b8387
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/SimpleKillableObserver.java
@@ -0,0 +1,60 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * <p>A simple implementation of {@link KillableObserver} which can be told
+ * explicitly to kill its {@link Killable} by calling {@link #kill()}. This
+ * is the sort of functionality that callers might expect to find available
+ * on the {@link Command} class.</p>
+ *
+ * <p>Note that this class can only observe one {@link Killable} at a time;
+ * multiple instances should be used for concurrent calls to
+ * {@link Command#execute(byte[], KillableObserver, boolean)}.</p>
+ */
+public final class SimpleKillableObserver implements KillableObserver {
+
+ private Killable killable;
+
+ /**
+ * Does nothing except store a reference to the given {@link Killable}.
+ *
+ * @param killable {@link Killable} to kill
+ */
+ public synchronized void startObserving(final Killable killable) {
+ this.killable = killable;
+ }
+
+ /**
+ * Forgets reference to {@link Killable} provided to
+ * {@link #startObserving(Killable)}
+ */
+ public synchronized void stopObserving(final Killable killable) {
+ if (!this.killable.equals(killable)) {
+ throw new IllegalStateException("start/stopObservering called with " +
+ "different Killables");
+ }
+ this.killable = null;
+ }
+
+ /**
+ * Calls {@link Killable#kill()} on the saved {@link Killable}.
+ */
+ public synchronized void kill() {
+ if (killable != null) {
+ killable.kill();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/TerminationStatus.java b/src/main/java/com/google/devtools/build/lib/shell/TerminationStatus.java
new file mode 100644
index 0000000000..73616c4f5f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/TerminationStatus.java
@@ -0,0 +1,162 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+/**
+ * Represents the termination status of a command. {@link Process#waitFor} is
+ * not very precisely specified, so this class encapsulates the interpretation
+ * of values returned by it.
+ *
+ * Caveat: due to the lossy encoding, it's not always possible to accurately
+ * distinguish signal and exit cases. In particular, processes that exit with
+ * a value within the interval [129, 191] will be mistaken for having been
+ * terminated by a signal.
+ *
+ * Instances are immutable.
+ */
+public final class TerminationStatus {
+
+ private final int waitResult;
+
+ /**
+ * Values taken from the glibc strsignal(3) function.
+ */
+ private static final String[] SIGNAL_STRINGS = {
+ null,
+ "Hangup",
+ "Interrupt",
+ "Quit",
+ "Illegal instruction",
+ "Trace/breakpoint trap",
+ "Aborted",
+ "Bus error",
+ "Floating point exception",
+ "Killed",
+ "User defined signal 1",
+ "Segmentation fault",
+ "User defined signal 2",
+ "Broken pipe",
+ "Alarm clock",
+ "Terminated",
+ "Stack fault",
+ "Child exited",
+ "Continued",
+ "Stopped (signal)",
+ "Stopped",
+ "Stopped (tty input)",
+ "Stopped (tty output)",
+ "Urgent I/O condition",
+ "CPU time limit exceeded",
+ "File size limit exceeded",
+ "Virtual timer expired",
+ "Profiling timer expired",
+ "Window changed",
+ "I/O possible",
+ "Power failure",
+ "Bad system call",
+ };
+
+ private static String getSignalString(int signum) {
+ return signum > 0 && signum < SIGNAL_STRINGS.length
+ ? SIGNAL_STRINGS[signum]
+ : "Signal " + signum;
+ }
+
+ /**
+ * Construct a TerminationStatus instance from a Process waitFor code.
+ *
+ * @param waitResult the value returned by {@link java.lang.Process#waitFor}.
+ */
+ public TerminationStatus(int waitResult) {
+ this.waitResult = waitResult;
+ }
+
+ /**
+ * Returns the "raw" result returned by Process.waitFor.
+ */
+ int getRawResult() {
+ return waitResult;
+ }
+
+ /**
+ * Returns true iff the process exited with code 0.
+ */
+ public boolean success() {
+ return exited() && getExitCode() == 0;
+ }
+
+ // We're relying on undocumented behaviour of Process.waitFor, specifically
+ // that waitResult is the exit status when the process returns normally, or
+ // 128+signalnumber when the process is terminated by a signal. We further
+ // assume that value signal numbers fall in the interval [1, 63].
+ private static final int SIGNAL_1 = 128 + 1;
+ private static final int SIGNAL_63 = 128 + 63;
+
+ /**
+ * Returns true iff the process exited normally.
+ */
+ public boolean exited() {
+ return waitResult < SIGNAL_1 || waitResult > SIGNAL_63;
+ }
+
+ /**
+ * Returns the exit code of the subprocess. Undefined if exited() is false.
+ */
+ public int getExitCode() {
+ if (!exited()) {
+ throw new IllegalStateException("getExitCode() not defined");
+ }
+ return waitResult;
+ }
+
+ /**
+ * Returns the number of the signal that terminated the process. Undefined
+ * if exited() returns true.
+ */
+ public int getTerminatingSignal() {
+ if (exited()) {
+ throw new IllegalStateException("getTerminatingSignal() not defined");
+ }
+ return waitResult - SIGNAL_1 + 1;
+ }
+
+ /**
+ * Returns a short string describing the termination status.
+ * e.g. "Exit 1" or "Hangup".
+ */
+ public String toShortString() {
+ return exited()
+ ? ("Exit " + getExitCode())
+ : (getSignalString(getTerminatingSignal()));
+ }
+
+ @Override
+ public String toString() {
+ return exited()
+ ? ("Process exited with status " + getExitCode())
+ : ("Process terminated by signal " + getTerminatingSignal());
+ }
+
+ @Override
+ public int hashCode() {
+ return waitResult;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof TerminationStatus &&
+ ((TerminationStatus) other).waitResult == this.waitResult;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/shell/TimeoutKillableObserver.java b/src/main/java/com/google/devtools/build/lib/shell/TimeoutKillableObserver.java
new file mode 100644
index 0000000000..c2ed033fed
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/shell/TimeoutKillableObserver.java
@@ -0,0 +1,102 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.shell;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <p>{@link KillableObserver} implementation which will kill its observed
+ * {@link Killable} if it is still being observed after a given amount
+ * of time has elapsed.</p>
+ *
+ * <p>Note that this class can only observe one {@link Killable} at a time;
+ * multiple instances should be used for concurrent calls to
+ * {@link Command#execute(byte[], KillableObserver, boolean)}.</p>
+ */
+public final class TimeoutKillableObserver implements KillableObserver {
+
+ private static final Logger log =
+ Logger.getLogger(TimeoutKillableObserver.class.getCanonicalName());
+
+ private final long timeoutMS;
+ private Killable killable;
+ private SleeperThread sleeperThread;
+ private boolean timedOut;
+
+ // TODO(bazel-team): I'd like to use ThreadPool2, but it doesn't currently
+ // provide a way to interrupt a thread
+
+ public TimeoutKillableObserver(final long timeoutMS) {
+ this.timeoutMS = timeoutMS;
+ }
+
+ /**
+ * Starts a new {@link Thread} to wait for the timeout period. This is
+ * interrupted by the {@link #stopObserving(Killable)} method.
+ *
+ * @param killable killable to kill when the timeout period expires
+ */
+ @Override
+ public synchronized void startObserving(final Killable killable) {
+ this.timedOut = false;
+ this.killable = killable;
+ this.sleeperThread = new SleeperThread();
+ this.sleeperThread.start();
+ }
+
+ @Override
+ public synchronized void stopObserving(final Killable killable) {
+ if (!this.killable.equals(killable)) {
+ throw new IllegalStateException("start/stopObservering called with " +
+ "different Killables");
+ }
+ if (sleeperThread.isAlive()) {
+ sleeperThread.interrupt();
+ }
+ this.killable = null;
+ sleeperThread = null;
+ }
+
+ private final class SleeperThread extends Thread {
+ @Override public void run() {
+ try {
+ if (log.isLoggable(Level.FINE)) {
+ log.fine("Waiting for " + timeoutMS + "ms to kill process");
+ }
+ Thread.sleep(timeoutMS);
+ // timeout expired; kill it
+ synchronized (TimeoutKillableObserver.this) {
+ if (killable != null) {
+ log.fine("Killing process");
+ killable.kill();
+ timedOut = true;
+ }
+ }
+ } catch (InterruptedException ie) {
+ // continue -- process finished before timeout
+ log.fine("Wait interrupted since process finished; continuing...");
+ }
+ }
+ }
+
+ /**
+ * Returns true if the observed process was killed by this observer.
+ */
+ public synchronized boolean hasTimedOut() {
+ // synchronized needed for memory model visibility.
+ return timedOut;
+ }
+}