blob: e7b0ad6c6966f51df4255e013b21233b965ee17a [file] [log] [blame]
// Copyright 2016 The Bazel Authors. 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.bazel.repository.skylark;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.shell.AbnormalTerminationException;
import com.google.devtools.build.lib.shell.BadExitStatusException;
import com.google.devtools.build.lib.shell.Command;
import com.google.devtools.build.lib.shell.CommandException;
import com.google.devtools.build.lib.shell.CommandResult;
import com.google.devtools.build.lib.shell.TerminationStatus;
import com.google.devtools.build.lib.skylarkbuildapi.repository.SkylarkExecutionResultApi;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.util.io.DelegatingOutErr;
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.devtools.build.lib.util.io.RecordingOutErr;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* A structure callable from Skylark that stores the result of repository_ctx.execute() method. It
* contains the standard output stream content, the standard error stream content and the execution
* return code.
*/
@Immutable
final class SkylarkExecutionResult implements SkylarkExecutionResultApi {
private final int returnCode;
private final String stdout;
private final String stderr;
SkylarkExecutionResult(int returnCode, String stdout, String stderr) {
this.returnCode = returnCode;
this.stdout = stdout;
this.stderr = stderr;
}
@Override
public int getReturnCode() {
return returnCode;
}
@Override
public String getStdout() {
return stdout;
}
@Override
public String getStderr() {
return stderr;
}
/**
* Returns a Builder that can be used to execute a command and build an execution result.
*
* @param environment pass through the list of environment variables from the client to be passed
* to the execution environment.
*/
public static Builder builder(Map<String, String> environment) {
return new Builder(environment);
}
/**
* A Builder class to build a {@link SkylarkExecutionResult} object by executing a command.
*/
static final class Builder {
private final List<String> args = new ArrayList<>();
private File directory = null;
private final Map<String, String> envBuilder = Maps.newLinkedHashMap();
private long timeout = -1;
private boolean executed = false;
private boolean quiet;
private Builder(Map<String, String> environment) {
envBuilder.putAll(environment);
}
/**
* Adds arguments to the list of arguments to pass to the command. The first argument is
* expected to be the binary to execute. The subsequent arguments are the arguments passed to
* the binary.
*
* <p>Each argument can be either a string or a {@link SkylarkPath}, passing another argument
* will fail when executing the command.
*/
Builder addArguments(Iterable<?> args) throws EvalException {
for (Object arg : args) {
// We might have skylark path, do conversion.
if (!(arg instanceof String || arg instanceof SkylarkPath)) {
throw new EvalException(
Location.BUILTIN,
"Argument " + this.args.size() + " of execute is neither a path nor a string.");
}
this.args.add(arg.toString());
}
return this;
}
/**
* Set the path to the directory to execute the result process. This method must be called
* before calling {@link #execute()}.
*/
Builder setDirectory(File path) throws EvalException {
this.directory = path;
return this;
}
/**
* Add an environment variables to be added to the list of environment variables. For all
* key <code>k</code> of <code>variables</code>, the resulting process will have the variable
* <code>k=variables.get(k)</code> defined.
*/
Builder addEnvironmentVariables(Map<String, String> variables) {
this.envBuilder.putAll(variables);
return this;
}
/**
* Sets the timeout, in milliseconds, after which the executed command will be terminated.
*/
Builder setTimeout(long timeout) {
Preconditions.checkArgument(timeout > 0, "Timeout must be a positive number.");
this.timeout = timeout;
return this;
}
Builder setQuiet(boolean quiet) {
this.quiet = quiet;
return this;
}
private static String toString(ByteArrayOutputStream stream) {
try {
return new String(stream.toByteArray(), UTF_8);
} catch (IllegalStateException e) {
return "";
}
}
/** Execute the command specified by {@link #addArguments(Iterable)}. */
SkylarkExecutionResult execute() throws EvalException, InterruptedException {
Preconditions.checkArgument(timeout > 0, "Timeout must be set prior to calling execute().");
Preconditions.checkArgument(!args.isEmpty(), "No command specified.");
Preconditions.checkState(!executed, "Command was already executed, cannot re-use builder.");
Preconditions.checkNotNull(directory, "Directory must be set before calling execute().");
executed = true;
DelegatingOutErr delegator = new DelegatingOutErr();
RecordingOutErr recorder = new RecordingOutErr();
// TODO(dmarting): if a lot of data is sent to stdout, this will use all the memory and
// Bazel will crash. Maybe we should use custom output streams that throw an appropriate
// exception when reaching a specific size.
delegator.addSink(recorder);
if (!quiet) {
delegator.addSink(OutErr.create(System.err, System.err));
}
try {
String[] argsArray = new String[args.size()];
for (int i = 0; i < args.size(); i++) {
argsArray[i] = args.get(i);
}
Command command = new Command(argsArray, envBuilder, directory, Duration.ofMillis(timeout));
CommandResult result =
command.execute(delegator.getOutputStream(), delegator.getErrorStream());
return new SkylarkExecutionResult(
result.getTerminationStatus().getExitCode(),
recorder.outAsLatin1(),
recorder.errAsLatin1());
} catch (BadExitStatusException e) {
return new SkylarkExecutionResult(
e.getResult().getTerminationStatus().getExitCode(), recorder.outAsLatin1(),
recorder.errAsLatin1());
} catch (AbnormalTerminationException e) {
TerminationStatus status = e.getResult().getTerminationStatus();
if (status.timedOut()) {
// Signal a timeout by an exit code outside the normal range
return new SkylarkExecutionResult(256, "", e.getMessage());
} else if (status.exited()) {
return new SkylarkExecutionResult(
status.getExitCode(),
toString(e.getResult().getStdoutStream()),
toString(e.getResult().getStderrStream()));
} else if (status.getTerminatingSignal() == 15) {
// We have a bit of a problem here: we cannot distingusih between the case where
// the SIGTERM was sent by something that the calling rule wants to legitimately handle,
// and the case where it was sent by bazel to abort the build, e.g., because something
// else failed.
//
// We just assume the latter to correctly handle aborts, accepting that rule authors have
// to write their rules without relying on the ability to handle termination by signal 15.
throw new InterruptedException();
} else {
return new SkylarkExecutionResult(
status.getRawExitCode(),
toString(e.getResult().getStdoutStream()),
toString(e.getResult().getStderrStream()));
}
} catch (CommandException e) {
// 256 is outside of the standard range for exit code on Unixes. We are not guaranteed that
// on all system it would be outside of the standard range.
return new SkylarkExecutionResult(256, "", e.getMessage());
}
}
}
}