| // Copyright 2015 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.dash; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.eventbus.Subscribe; |
| import com.google.common.io.ByteStreams; |
| import com.google.devtools.build.lib.bazel.dash.DashProtos.BuildData; |
| import com.google.devtools.build.lib.bazel.dash.DashProtos.BuildData.CommandLine.Option; |
| import com.google.devtools.build.lib.bazel.dash.DashProtos.BuildData.EnvironmentVar; |
| import com.google.devtools.build.lib.bazel.dash.DashProtos.BuildData.Target.TestData; |
| import com.google.devtools.build.lib.bazel.dash.DashProtos.Log; |
| import com.google.devtools.build.lib.events.Event; |
| import com.google.devtools.build.lib.events.Reporter; |
| import com.google.devtools.build.lib.packages.Target; |
| import com.google.devtools.build.lib.pkgcache.TargetParsingCompleteEvent; |
| import com.google.devtools.build.lib.rules.test.TestResult; |
| import com.google.devtools.build.lib.runtime.BlazeModule; |
| import com.google.devtools.build.lib.runtime.Command; |
| import com.google.devtools.build.lib.runtime.CommandEnvironment; |
| import com.google.devtools.build.lib.runtime.CommandStartEvent; |
| import com.google.devtools.build.lib.runtime.GotOptionsEvent; |
| import com.google.devtools.common.options.OptionsBase; |
| import com.google.devtools.common.options.OptionsParser.UnparsedOptionValueDescription; |
| import com.google.devtools.common.options.OptionsProvider; |
| import com.google.protobuf.ByteString; |
| |
| import org.apache.http.HttpEntity; |
| import org.apache.http.HttpHeaders; |
| import org.apache.http.HttpStatus; |
| import org.apache.http.StatusLine; |
| import org.apache.http.client.HttpClient; |
| import org.apache.http.client.methods.HttpPost; |
| import org.apache.http.entity.ByteArrayEntity; |
| import org.apache.http.impl.client.DefaultHttpClient; |
| import org.apache.http.params.BasicHttpParams; |
| import org.apache.http.params.HttpConnectionParams; |
| import org.apache.http.params.HttpParams; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.attribute.PosixFilePermission; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.ThreadFactory; |
| |
| /** |
| * Dashboard for a build. |
| */ |
| public class DashModule extends BlazeModule { |
| private static final int ONE_MB = 1024 * 1024; |
| private static final NoOpSender NO_OP_SENDER = new NoOpSender(); |
| |
| private static final String DASH_SECRET_HEADER = "bazel-dash-secret"; |
| |
| private final ExecutorService executorService; |
| |
| private Sendable sender; |
| private CommandEnvironment env; |
| private BuildData optionsBuildData; |
| |
| public DashModule() { |
| // Make sure sender != null before we hop on the event bus. |
| sender = NO_OP_SENDER; |
| executorService = Executors.newFixedThreadPool(5, |
| new ThreadFactory() { |
| @Override |
| public Thread newThread(Runnable runnable) { |
| Thread thread = Executors.defaultThreadFactory().newThread(runnable); |
| thread.setDaemon(true); |
| return thread; |
| } |
| }); |
| } |
| |
| @Override |
| public void beforeCommand(Command command, CommandEnvironment env) { |
| this.env = env; |
| env.getEventBus().register(this); |
| } |
| |
| @Override |
| public void afterCommand() { |
| this.sender = NO_OP_SENDER; |
| this.env = null; |
| this.optionsBuildData = null; |
| } |
| |
| @Override |
| public Iterable<Class<? extends OptionsBase>> getCommandOptions(Command command) { |
| return "build".equals(command.name()) |
| ? ImmutableList.<Class<? extends OptionsBase>>of(DashOptions.class) |
| : ImmutableList.<Class<? extends OptionsBase>>of(); |
| } |
| |
| @Override |
| public void handleOptions(OptionsProvider optionsProvider) { |
| DashOptions options = optionsProvider.getOptions(DashOptions.class); |
| try { |
| sender = (options == null || !options.useDash) |
| ? NO_OP_SENDER |
| : new Sender(options.url, options.secret, env, executorService); |
| } catch (SenderException e) { |
| env.getReporter().handle(e.toEvent()); |
| sender = NO_OP_SENDER; |
| } |
| if (optionsBuildData != null) { |
| sender.send("options", optionsBuildData); |
| } |
| optionsBuildData = null; |
| } |
| |
| @Subscribe |
| public void gotOptions(GotOptionsEvent event) { |
| BuildData.Builder builder = BuildData.newBuilder(); |
| BuildData.CommandLine.Builder cmdLineBuilder = BuildData.CommandLine.newBuilder(); |
| for (UnparsedOptionValueDescription option : |
| event.getStartupOptions().asListOfUnparsedOptions()) { |
| cmdLineBuilder.addStartupOptions(getOption(option)); |
| } |
| |
| for (UnparsedOptionValueDescription option : event.getOptions().asListOfUnparsedOptions()) { |
| if (option.getName().equals("client_env")) { |
| String env[] = option.getUnparsedValue().split("="); |
| if (env.length == 1) { |
| builder.addClientEnv( |
| EnvironmentVar.newBuilder().setName(env[0]).setValue("true").build()); |
| } else if (env.length == 2) { |
| builder.addClientEnv( |
| EnvironmentVar.newBuilder().setName(env[0]).setValue(env[1]).build()); |
| } |
| } else { |
| cmdLineBuilder.addOptions(getOption(option)); |
| } |
| } |
| |
| for (String residue : event.getOptions().getResidue()) { |
| cmdLineBuilder.addResidue(residue); |
| } |
| builder.setCommandLine(cmdLineBuilder.build()); |
| |
| // This can be called before handleOptions, so the BuildData is stored until we know if it |
| // should be sent somewhere. |
| optionsBuildData = builder.build(); |
| } |
| |
| @Subscribe |
| public void commandStartEvent(CommandStartEvent event) { |
| BuildData.Builder builder = BuildData.newBuilder() |
| .setBuildId(event.getCommandId().toString()) |
| .setCommandName(event.getCommandName()) |
| .setWorkingDir(event.getWorkingDirectory().getPathString()); |
| sender.send("start", builder.build()); |
| } |
| |
| @Subscribe |
| public void parsingComplete(TargetParsingCompleteEvent event) { |
| BuildData.Builder builder = BuildData.newBuilder(); |
| for (Target target : event.getTargets()) { |
| builder.addTargetsBuilder() |
| .setLabel(target.getLabel().toString()) |
| .setRuleKind(target.getTargetKind()).build(); |
| } |
| sender.send("targets", builder.build()); |
| } |
| |
| @Subscribe |
| public void testFinished(TestResult result) { |
| BuildData.Builder builder = BuildData.newBuilder(); |
| BuildData.Target.Builder targetBuilder = BuildData.Target.newBuilder(); |
| targetBuilder.setLabel(result.getLabel()); |
| TestData.Builder testDataBuilder = TestData.newBuilder(); |
| testDataBuilder.setPassed(result.getData().getTestPassed()); |
| if (!result.getData().getTestPassed()) { |
| testDataBuilder.setLog(getLog(result.getTestLogPath().toString())); |
| } |
| targetBuilder.setTestData(testDataBuilder); |
| builder.addTargets(targetBuilder); |
| sender.send("test", builder.build()); |
| } |
| |
| private Log getLog(String logPath) { |
| Log.Builder builder = Log.newBuilder().setPath(logPath); |
| File log = new File(logPath); |
| try { |
| long fileSize = Files.size(log.toPath()); |
| if (fileSize > ONE_MB) { |
| fileSize = ONE_MB; |
| builder.setTruncated(true); |
| } |
| byte buffer[] = new byte[(int) fileSize]; |
| try (FileInputStream in = new FileInputStream(log)) { |
| ByteStreams.readFully(in, buffer); |
| } |
| builder.setContents(ByteString.copyFrom(buffer)); |
| } catch (IOException e) { |
| env |
| .getReporter() |
| .getOutErr() |
| .printOutLn("Error reading log file " + logPath + ": " + e.getMessage()); |
| // TODO(kchodorow): add this info to the proto and send. |
| } |
| return builder.build(); |
| } |
| |
| @Override |
| public void blazeShutdown() { |
| executorService.shutdownNow(); |
| } |
| |
| private BuildData.CommandLine.Option getOption(UnparsedOptionValueDescription option) { |
| Option.Builder optionBuilder = Option.newBuilder(); |
| optionBuilder.setName(option.getName()); |
| if (option.getSource() != null) { |
| optionBuilder.setSource(option.getSource()); |
| } |
| Object value = option.getUnparsedValue(); |
| if (value != null) { |
| if (value instanceof Iterable<?>) { |
| for (Object v : ((Iterable<?>) value)) { |
| if (v != null) { |
| optionBuilder.addValue(v.toString()); |
| } |
| } |
| } else { |
| optionBuilder.addValue(value.toString()); |
| } |
| } |
| return optionBuilder.build(); |
| } |
| |
| private interface Sendable { |
| void send(final String suffix, final BuildData message); |
| } |
| |
| private static class SenderException extends Exception { |
| SenderException(String message, Throwable ex) { |
| super(message, ex); |
| } |
| |
| SenderException(String message) { |
| super(message); |
| } |
| |
| Event toEvent() { |
| if (getCause() != null) { |
| return Event.error(getMessage() + ": " + getCause().getMessage()); |
| } else { |
| return Event.error(getMessage()); |
| } |
| } |
| } |
| |
| private static class Sender implements Sendable { |
| private final URL url; |
| private final String buildId; |
| private final String secret; |
| private final Reporter reporter; |
| private final ExecutorService executorService; |
| |
| public Sender(String url, String secret, |
| CommandEnvironment env, ExecutorService executorService) throws SenderException { |
| this.reporter = env.getReporter(); |
| this.secret = readSecret(secret, reporter); |
| try { |
| this.url = new URL(url); |
| if (!this.secret.isEmpty()) { |
| if (!(this.url.getProtocol().equals("https") || this.url.getHost().equals("localhost") |
| || this.url.getHost().matches("^127.0.0.[0-9]+$"))) { |
| reporter.handle(Event.warn("Using authentication over unsecure channel, " |
| + "consider using HTTPS.")); |
| } |
| } |
| } catch (MalformedURLException e) { |
| throw new SenderException("Invalid server url " + url, e); |
| } |
| this.buildId = env.getCommandId().toString(); |
| this.executorService = executorService; |
| sendMessage("test", null); // test connecting to the server. |
| reporter.handle(Event.info("Results are being streamed to " + url + "/result/" + buildId)); |
| } |
| |
| private static String readSecret(String secretFile, Reporter reporter) throws SenderException { |
| if (secretFile.isEmpty()) { |
| return ""; |
| } |
| Path secretPath = new File(secretFile).toPath(); |
| if (!Files.isReadable(secretPath)) { |
| throw new SenderException("Secret file " + secretFile + " doesn't exists or is unreadable"); |
| } |
| try { |
| if (Files.getPosixFilePermissions(secretPath).contains(PosixFilePermission.OTHERS_READ) |
| || Files.getPosixFilePermissions(secretPath).contains(PosixFilePermission.GROUP_READ)) { |
| reporter.handle(Event.warn("Secret file " + secretFile + " is readable by non-owner. " |
| + "It is recommended to set its permission to 0600 (read-write only by the owner).")); |
| } |
| return new String(Files.readAllBytes(secretPath), StandardCharsets.UTF_8).trim(); |
| } catch (IOException e) { |
| throw new SenderException("Invalid secret file " + secretFile, e); |
| } |
| } |
| |
| private void sendMessage(final String suffix, final HttpEntity message) |
| throws SenderException { |
| HttpParams httpParams = new BasicHttpParams(); |
| HttpConnectionParams.setConnectionTimeout(httpParams, 5000); |
| HttpConnectionParams.setSoTimeout(httpParams, 5000); |
| HttpClient httpClient = new DefaultHttpClient(httpParams); |
| |
| HttpPost httppost = new HttpPost(url + "/" + suffix + "/" + buildId); |
| if (message != null) { |
| httppost.setHeader(HttpHeaders.CONTENT_TYPE, "application/x-protobuf"); |
| httppost.setEntity(message); |
| } |
| if (!secret.isEmpty()) { |
| httppost.setHeader(DASH_SECRET_HEADER, secret); |
| } |
| StatusLine status; |
| try { |
| status = httpClient.execute(httppost).getStatusLine(); |
| } catch (IOException e) { |
| throw new SenderException("Error sending results to " + url, e); |
| } |
| if (status.getStatusCode() == HttpStatus.SC_FORBIDDEN) { |
| throw new SenderException("Permission denied while sending results to " + url |
| + ". Did you specified --dash_secret?"); |
| } |
| } |
| |
| @Override |
| public void send(final String suffix, final BuildData message) { |
| executorService.submit(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| sendMessage(suffix, new ByteArrayEntity(message.toByteArray())); |
| } catch (SenderException ex) { |
| reporter.handle(ex.toEvent()); |
| } |
| } |
| |
| }); |
| } |
| } |
| |
| private static class NoOpSender implements Sendable { |
| public NoOpSender() { |
| } |
| |
| @Override |
| public void send(String suffix, BuildData message) { |
| } |
| } |
| } |