aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/shell/Command.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/shell/Command.java')
-rw-r--r--src/main/java/com/google/devtools/build/lib/shell/Command.java960
1 files changed, 960 insertions, 0 deletions
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);
+ }
+ }
+}