Java tutorial
/* * 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(); } }