com.android.build.gradle.external.cmake.server.ServerProtocolV1.java Source code

Java tutorial

Introduction

Here is the source code for com.android.build.gradle.external.cmake.server.ServerProtocolV1.java

Source

/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * 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.android.build.gradle.external.cmake.server;

import com.android.annotations.NonNull;
import com.android.annotations.VisibleForTesting;
import com.android.build.gradle.external.cmake.server.receiver.InteractiveMessage;
import com.android.build.gradle.external.cmake.server.receiver.InteractiveProgress;
import com.android.build.gradle.external.cmake.server.receiver.ServerReceiver;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Implementation of version 1 of Cmake server for Cmake versions 3.7.1. Cmake server or a long
 * running mode which allows a client to configure and request buildsystem information generated by
 * Cmake. More info: https://cmake.org/cmake/help/v3.7/manual/cmake-server.7.html
 */
public class ServerProtocolV1 implements Server {
    // Messages sent to and from the Cmake server are wrapped in the header and footer strings.
    public static final String CMAKE_SERVER_HEADER_MSG = "[== \"CMake Server\" ==[";
    public static final String CMAKE_SERVER_FOOTER_MSG = "]== \"CMake Server\" ==]";

    // When configuring a given project, Cmake server reports progress and these could contain
    // compiler information. The compiler info is contained within these prefix/suffix messages.
    // Note: These progress messages will be used as a fallback to determine the compiler info only
    // when compile_commands.json file is not present.
    public static final String CMAKE_SERVER_C_COMPILER_PREFIX = "Check for working C compiler: ";
    public static final String CMAKE_SERVER_CXX_COMPILER_PREFIX = "Check for working CXX compiler: ";
    public static final String CMAKE_SERVER_C_COMPILER_SUFFIX = " -- works";

    // Reader and writers to communicate with Cmake server.
    private BufferedReader input;
    private BufferedWriter output;
    // Cmake's install path.
    private final File cmakeInstallPath;
    // Messages, signals etc received from Cmake server.
    private final ServerReceiver serverReceiver;
    // Cached hello result, used to get Cmake server versions.
    private HelloResult helloResult = null;
    // Indicates if we are connected to the Cmake server.
    private boolean connected = false;
    // Indicates if we have configured the given project.
    private boolean configured = false;
    // Indicates if we have computed the given project.
    private boolean computed = false;

    // Interactive messages received when configuring the project.
    private List<InteractiveMessage> configureMessages;
    // Process builder used primarily for testing.
    Process process = null;

    ServerProtocolV1(@NonNull File cmakeInstallPath, @NonNull ServerReceiver serverReceiver) {
        this.cmakeInstallPath = cmakeInstallPath;
        this.serverReceiver = serverReceiver;
    }

    /**
     * This constructor is used only for testing purpose, to pass mock process, buffered
     * input/output etc.
     */
    @VisibleForTesting
    ServerProtocolV1(@NonNull File cmakeInstallPath, @NonNull ServerReceiver serverReceiver, Process process,
            BufferedReader input, BufferedWriter output) {
        this.cmakeInstallPath = cmakeInstallPath;
        this.serverReceiver = serverReceiver;
        this.process = process;
        this.input = input;
        this.output = output;
    }

    @Override
    public void finalize() {
        try {
            disconnect();
        } catch (IOException e) {
            diagnostic("Error when disconnecting from Cmake server: %s", e.toString());
        }
    }

    @Override
    public boolean connect() throws IOException {
        init();
        helloResult = decodeResponse(HelloResult.class);
        connected = ServerUtils.isHelloResultValid(helloResult);
        return connected;
    }

    @Override
    public void disconnect() throws IOException {
        if (input != null) {
            input.close();
            input = null;
        }
        if (output != null) {
            output.close();
            output = null;
        }

        if (process != null) {
            process.destroy();
            process = null;
        }

        connected = false;
        configured = false;
        computed = false;
        configureMessages = null;
        helloResult = null;
    }

    @Override
    public boolean isConnected() {
        return connected;
    }

    @NonNull
    @Override
    public List<ProtocolVersion> getSupportedVersion() {
        if (helloResult == null || helloResult.supportedProtocolVersions == null) {
            return null;
        }
        List<ProtocolVersion> result = new ArrayList<>();
        for (ProtocolVersion protocolVersion : helloResult.supportedProtocolVersions) {
            if (protocolVersion.major == 1 && protocolVersion.minor == 0) {
                result.add(protocolVersion);
                break;
            }
        }
        return result;
    }

    @NonNull
    @Override
    public HandshakeResult handshake(@NonNull HandshakeRequest handshakeRequest) throws IOException {
        if (!connected) {
            throw new RuntimeException("Not connected to Cmake server.");
        }
        writeMessage(new GsonBuilder().setPrettyPrinting().create().toJson(handshakeRequest));
        return decodeResponse(HandshakeResult.class);
    }

    @NonNull
    @Override
    public ConfigureCommandResult configure(@NonNull String... cacheArguments) throws IOException {
        if (!connected) {
            throw new RuntimeException("Not connected to Cmake server.");
        }

        ConfigureRequest configureRequest = new ConfigureRequest();

        // Insert a blank element to work around a bug in Cmake 3.7.1 where the first element is
        // ignored.
        configureRequest.cacheArguments = new String[cacheArguments.length + 1];
        configureRequest.cacheArguments[0] = "";
        System.arraycopy(cacheArguments, 0, configureRequest.cacheArguments, 1, cacheArguments.length);

        writeMessage(new GsonBuilder().setPrettyPrinting().create().toJson(configureRequest));
        configureMessages = new ArrayList<>();
        ConfigureResult configureResult = decodeResponse(ConfigureResult.class, configureMessages);
        configured = ServerUtils.isConfigureResultValid(configureResult);

        return new ConfigureCommandResult(configureResult,
                !configureMessages.isEmpty() ? getInteractiveMessagesAsString(configureMessages) : "");
    }

    @NonNull
    @Override
    public ComputeResult compute() throws IOException {
        if (!connected) {
            throw new RuntimeException("Not connected to Cmake server.");
        }

        if (!configured) {
            throw new RuntimeException("Cmake server has not been configured successfully, unable to compute.");
        }

        writeMessage("{\"type\":\"compute\"}");
        ComputeResult computeResult = decodeResponse(ComputeResult.class);
        computed = ServerUtils.isComputedResultValid(computeResult);
        return computeResult;
    }

    @NonNull
    @Override
    public CodeModel codemodel() throws IOException {
        if (!connected) {
            throw new RuntimeException("Not connected to Cmake server.");
        }

        if (!computed) {
            throw new RuntimeException("Need to compute before requesting for codemodel.");
        }

        writeMessage("{\"type\":\"codemodel\"}");
        return decodeResponse(CodeModel.class);
    }

    @NonNull
    @Override
    public CacheResult cache() throws IOException {
        if (!connected) {
            throw new RuntimeException("Not connected to Cmake server.");
        }

        CacheRequest request = new CacheRequest();
        writeMessage(new GsonBuilder().setPrettyPrinting().create().toJson(request));
        return decodeResponse(CacheResult.class);
    }

    @NonNull
    @Override
    public GlobalSettings globalSettings() throws IOException {
        if (!connected) {
            throw new RuntimeException("Not connected to Cmake server.");
        }

        writeMessage("{\"type\":\"globalSettings\"}");
        return decodeResponse(GlobalSettings.class);
    }

    @NonNull
    @Override
    public String getCCompilerExecutable() {
        final String prefixMessage = "Check for working C compiler: ";
        final String suffixMessage = " -- works";

        return hackyGetLangExecutable(prefixMessage, suffixMessage);
    }

    @NonNull
    @Override
    public String getCppCompilerExecutable() {
        final String prefixMessage = "Check for working CXX compiler: ";
        final String suffixMessage = " -- works";

        return hackyGetLangExecutable(prefixMessage, suffixMessage);
    }

    @NonNull
    @Override
    public String getCmakePath() {
        return cmakeInstallPath.getAbsolutePath();
    }

    @NonNull
    public HelloResult getHelloResult() {
        return helloResult;
    }

    // Helper functions

    /**
     * Ideally, we should use compile_commands.json generated by Cmake to get C and Cxx compiler
     * information. If for whatever reason the file is not present (or not generated), we fall back
     * to check the progress messages generated Cmake server when configuring, to get the desired
     * information.
     *
     * @param prefixMessage - prefix string to search
     * @param suffixMessage - suffix string to search
     * @return C/CXX compiler
     */
    private String hackyGetLangExecutable(@NonNull String prefixMessage, @NonNull String suffixMessage) {
        if (configureMessages == null || configureMessages.isEmpty()) {
            return null;
        }

        for (InteractiveMessage message : configureMessages) {
            if (message.message == null || !message.message.startsWith(prefixMessage)
                    || !message.message.endsWith(suffixMessage)) {
                continue;
            }
            return message.message.substring(prefixMessage.length(),
                    message.message.length() - suffixMessage.length());
        }

        return null;
    }

    /**
     * Initializes the server.
     *
     * @throws IOException I/O failure
     */
    private void init() throws IOException {
        if (process == null) {
            ProcessBuilder processBuilder = getCmakeServerProcessBuilder();
            processBuilder.environment().putAll(new ProcessBuilder().environment());
            process = processBuilder.start();
        }

        if (input == null) {
            input = new BufferedReader(new InputStreamReader(process.getInputStream()));
        }
        if (output == null) {
            output = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
        }
    }

    /**
     * Prints diagnostic messages.
     *
     * @param format - diagnostic message format
     * @param args - diagnostic message arguments
     */
    private void diagnostic(String format, Object... args) {
        if (serverReceiver.getDiagnosticReceiver() != null) {
            serverReceiver.getDiagnosticReceiver().receive(String.format(format, args));
        }
    }

    /**
     * Constructs a Cmake server process builder.
     *
     * @return process builder
     */
    private ProcessBuilder getCmakeServerProcessBuilder() {
        final String cmakeBinPath = new File(this.cmakeInstallPath, "cmake").getPath();
        return new ProcessBuilder(cmakeBinPath, "-E", "server", "--experimental", "--debug");
    }

    /**
     * Decodes the responses received during Cmake server interactions for a given request.
     *
     * @param clazz Class object that represents the response class
     * @return decoded response
     * @throws IOException I/O failure
     */
    private <T> T decodeResponse(Class<T> clazz) throws IOException {
        return decodeResponse(clazz, null);
    }

    private <T> T decodeResponse(Class<T> clazz, List<InteractiveMessage> interactiveMessages) throws IOException {
        Gson gson = new GsonBuilder().create();
        String message = readMessage();
        String messageType = gson.fromJson(message, TypeOfMessage.class).type;

        final List supportedTypes = Arrays.asList("message", "progress", "signal");
        // Process supported interactive messages.
        // For a given command, the CMake server would respond with message types
        // 0 or more of (message | progress | signal)
        // and finally terminates with a message with message types
        // (hello | reply | error)
        // More info:
        // https://cmake.org/cmake/help/v3.7/manual/cmake-server.7.html#general-message-layout
        while (supportedTypes.contains(messageType)) {
            switch (messageType) {
            case "message":
                if (serverReceiver.getMessageReceiver() != null) {
                    InteractiveMessage interactiveMessage = gson.fromJson(message, InteractiveMessage.class);
                    serverReceiver.getMessageReceiver().receive(interactiveMessage);
                    // Record the interactive messages only if need be.
                    if (interactiveMessages != null) {
                        serverReceiver.getMessageReceiver().receive(interactiveMessage);
                        interactiveMessages.add(interactiveMessage);
                    }
                }
                break;
            case "progress":
                if (serverReceiver.getProgressReceiver() != null) {
                    serverReceiver.getProgressReceiver().receive(gson.fromJson(message, InteractiveProgress.class));
                    break;
                }
                break;
            case "signal":
                if (serverReceiver.getProgressReceiver() != null) {
                    serverReceiver.getProgressReceiver().receive(gson.fromJson(message, InteractiveProgress.class));
                    break;
                }
            }
            message = readMessage();
            messageType = gson.fromJson(message, TypeOfMessage.class).type;
        }

        // Process the final message.
        switch (messageType) {
        case "hello":
        case "reply":
            if (serverReceiver.getDeserializationMonitor() != null) {
                serverReceiver.getDeserializationMonitor().receive(message, clazz);
            }
            return gson.fromJson(message, clazz);
        case "error":
            if (serverReceiver.getMessageReceiver() != null) {
                InteractiveMessage interactiveMessage = gson.fromJson(message, InteractiveMessage.class);
                serverReceiver.getMessageReceiver().receive(interactiveMessage);
            }
            return null;
        default:
            throw new RuntimeException("Unsupported message type " + messageType + " received from CMake server.");
        }
    }

    /**
     * Appends the messages from the given list of InteractiveMessage to return it as a single
     * string.
     *
     * @param interactiveMessages - list of interactive messages received from Cmake server
     * @return A single string with all the messages from interactive messages.
     */
    private static String getInteractiveMessagesAsString(List<InteractiveMessage> interactiveMessages) {
        StringBuilder result = new StringBuilder();
        for (InteractiveMessage interactiveMessage : interactiveMessages) {
            result.append(interactiveMessage.message).append("\n");
        }

        return result.toString();
    }

    /**
     * Reads a line from Cmake server
     *
     * @return a line read from Cmake servers response
     * @throws IOException I/O failure
     */
    private String readLine() throws IOException {
        final String line = input.readLine();
        diagnostic(line + "\n");
        return line;
    }

    /**
     * Writes a string to Cmake server
     *
     * @throws IOException I/O failure
     */
    private void writeLine(String message) throws IOException {
        diagnostic("%s\n", message);
        output.write(message);
        output.newLine();
    }

    /**
     * Reads until the expected string is found. Skip unexpected (or non-conforming) messages if
     * need be until the expected string is found. Note: The CMake server sometimes writes
     * non-conforming messages (by deviating from the general message layout: https://goo.gl/d4XMmB)
     * to stdout, these are harmless (i.e., they don't break the build) and hence need to be
     * ignored.
     */
    private void readExpected(@NonNull String expectedString) throws IOException {
        String line = readLine();
        while (!line.equals(expectedString)) {
            // Skip a blank line if there is one.
            if (!line.isEmpty() && serverReceiver.getDiagnosticReceiver() != null) {
                serverReceiver.getDiagnosticReceiver().receive(line);
            }
            line = readLine();
        }
    }

    /**
     * Reads a message send by CMake server. CMake Server sends the messages wrapped within a
     * defined header and footer string, this function reads everything inbetween the header and
     * footer and returns it. General message layout we expect from CMake Server:
     *
     * <p>[non-conforming messages from CMake Server]
     *
     * <p>[== "CMake Server" ==[
     *
     * <p>InteractiveMessage
     *
     * <p>[non-conforming messages from CMake Server]
     *
     * <p>]== "CMake Server" ==]
     *
     * @return The string contained within the header and footer
     * @throws IOException I/O failure
     */
    private String readMessage() throws IOException {
        readExpected(CMAKE_SERVER_HEADER_MSG);
        final String line = readLine();
        readExpected(CMAKE_SERVER_FOOTER_MSG);
        return line;
    }

    /**
     * Writes a message wrapped within the header and footer.
     *
     * @param message string to be sent to Cmake server
     * @throws IOException I/O failure
     */
    private void writeMessage(String message) throws IOException {
        writeLine(CMAKE_SERVER_HEADER_MSG);
        writeLine(message);
        writeLine(CMAKE_SERVER_FOOTER_MSG);
        output.flush();
    }
}