diff options
author | Han-Wen Nienhuys <hanwen@google.com> | 2015-02-25 16:45:20 +0100 |
---|---|---|
committer | Han-Wen Nienhuys <hanwen@google.com> | 2015-02-25 16:45:20 +0100 |
commit | d08b27fa9701fecfdb69e1b0d1ac2459efc2129b (patch) | |
tree | 5d50963026239ca5aebfb47ea5b8db7e814e57c8 /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')
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'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; + } +} |