blob: 9776f7b531922ef64c078b327e3162de4a3f51ab [file] [log] [blame]
/*
* 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();
}
}