com.google.devtools.build.lib.bazel.dash.DashModule.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.lib.bazel.dash.DashModule.java

Source

// 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) {
        }
    }
}