diff options
author | Brad King <brad.king@kitware.com> | 2016-09-19 13:36:34 (GMT) |
---|---|---|
committer | CMake Topic Stage <kwrobot@kitware.com> | 2016-09-19 13:36:34 (GMT) |
commit | 5c87b92b1b7888ee032e3c2a75f35f1f94f4dfa5 (patch) | |
tree | ab19ef186570cb3637eab19d19bef17207a1b748 | |
parent | 419ad0510193eb2b2e227095a4dd167cd9b3df80 (diff) | |
parent | 7263667c24cecf4bb155fc0cbf687dee8b57f5f7 (diff) | |
download | CMake-5c87b92b1b7888ee032e3c2a75f35f1f94f4dfa5.zip CMake-5c87b92b1b7888ee032e3c2a75f35f1f94f4dfa5.tar.gz CMake-5c87b92b1b7888ee032e3c2a75f35f1f94f4dfa5.tar.bz2 |
Merge topic 'cmake-server-basic'
7263667c Help: Add notes for topic 'cmake-server-basic'
5adde4e7 cmake-server: Add documentation
b63c1f6c cmake-server: Add unit test
d341d077 cmake-server: Implement ServerProtocol 1.0
b13d3e0d cmake-server: Bare-bones server implementation
cd049f01 cmake-server: Report server mode availablitily in Capabilities
-rw-r--r-- | CMakeLists.txt | 12 | ||||
-rw-r--r-- | Help/index.rst | 1 | ||||
-rw-r--r-- | Help/manual/cmake-server.7.rst | 188 | ||||
-rw-r--r-- | Help/manual/cmake.1.rst | 3 | ||||
-rw-r--r-- | Help/release/dev/cmake-server-basic.rst | 6 | ||||
-rw-r--r-- | Source/CMakeLists.txt | 11 | ||||
-rw-r--r-- | Source/cmServer.cxx | 355 | ||||
-rw-r--r-- | Source/cmServer.h | 85 | ||||
-rw-r--r-- | Source/cmServerProtocol.cxx | 264 | ||||
-rw-r--r-- | Source/cmServerProtocol.h | 115 | ||||
-rw-r--r-- | Source/cmake.cxx | 12 | ||||
-rw-r--r-- | Source/cmake.h | 4 | ||||
-rw-r--r-- | Source/cmcmd.cxx | 24 | ||||
-rw-r--r-- | Tests/CMakeLists.txt | 9 | ||||
-rw-r--r-- | Tests/RunCMake/CommandLine/E_server-arg-result.txt | 1 | ||||
-rw-r--r-- | Tests/RunCMake/CommandLine/E_server-arg-stderr.txt | 1 | ||||
-rw-r--r-- | Tests/RunCMake/CommandLine/RunCMakeTest.cmake | 1 | ||||
-rw-r--r-- | Tests/Server/CMakeLists.txt | 23 | ||||
-rw-r--r-- | Tests/Server/cmakelib.py | 126 | ||||
-rw-r--r-- | Tests/Server/empty.cpp | 5 | ||||
-rw-r--r-- | Tests/Server/server-test.py | 82 | ||||
-rw-r--r-- | Tests/Server/tc_handshake.json | 71 |
22 files changed, 1388 insertions, 11 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index c8bd063..2ec8b57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -702,6 +702,18 @@ endif() # setup some Testing support (a macro defined in this file) CMAKE_SETUP_TESTING() +# Check whether to build server mode or not: +set(CMake_HAVE_SERVER_MODE 0) +if(NOT CMake_TEST_EXTERNAL_CMAKE AND NOT CMAKE_BOOTSTRAP AND CMAKE_USE_LIBUV) + list(FIND CMAKE_CXX_COMPILE_FEATURES cxx_auto_type CMake_HAVE_CXX_AUTO_TYPE) + list(FIND CMAKE_CXX_COMPILE_FEATURES cxx_range_for CMake_HAVE_CXX_RANGE_FOR) + if(CMake_HAVE_CXX_AUTO_TYPE AND CMake_HAVE_CXX_RANGE_FOR) + if(CMake_HAVE_CXX_MAKE_UNIQUE) + set(CMake_HAVE_SERVER_MODE 1) + endif() + endif() +endif() + if(NOT CMake_TEST_EXTERNAL_CMAKE) if(NOT CMake_VERSION_IS_RELEASE) if(CMAKE_C_COMPILER_ID STREQUAL "GNU" AND diff --git a/Help/index.rst b/Help/index.rst index 2d3f156..97cd107 100644 --- a/Help/index.rst +++ b/Help/index.rst @@ -32,6 +32,7 @@ Reference Manuals /manual/cmake-generator-expressions.7 /manual/cmake-generators.7 /manual/cmake-language.7 + /manual/cmake-server.7 /manual/cmake-modules.7 /manual/cmake-packages.7 /manual/cmake-policies.7 diff --git a/Help/manual/cmake-server.7.rst b/Help/manual/cmake-server.7.rst new file mode 100644 index 0000000..fd0c9ee --- /dev/null +++ b/Help/manual/cmake-server.7.rst @@ -0,0 +1,188 @@ +.. cmake-manual-description: CMake Server + +cmake-server(7) +*************** + +.. only:: html + + .. contents:: + +Introduction +============ + +:manual:`cmake(1)` is capable of providing semantic information about +CMake code it executes to generate a buildsystem. If executed with +the ``-E server`` command line options, it starts in a long running mode +and allows a client to request the available information via a JSON protocol. + +The protocol is designed to be useful to IDEs, refactoring tools, and +other tools which have a need to understand the buildsystem in entirety. + +A single :manual:`cmake-buildsystem(7)` may describe buildsystem contents +and build properties which differ based on +:manual:`generation-time context <cmake-generator-expressions(7)>` +including: + +* The Platform (eg, Windows, APPLE, Linux). +* The build configuration (eg, Debug, Release, Coverage). +* The Compiler (eg, MSVC, GCC, Clang) and compiler version. +* The language of the source files compiled. +* Available compile features (eg CXX variadic templates). +* CMake policies. + +The protocol aims to provide information to tooling to satisfy several +needs: + +#. Provide a complete and easily parsed source of all information relevant + to the tooling as it relates to the source code. There should be no need + for tooling to parse generated buildsystems to access include directories + or compile definitions for example. +#. Semantic information about the CMake buildsystem itself. +#. Provide a stable interface for reading the information in the CMake cache. +#. Information for determining when cmake needs to be re-run as a result of + file changes. + + +Operation +========= + +Start :manual:`cmake(1)` in the server command mode, supplying the path to +the build directory to process:: + + cmake -E server + +The server will start up and reply with an hello message on stdout:: + + [== CMake Server ==[ + {"supportedProtocolVersions":[{"major":0,"minor":1}],"type":"hello"} + ]== CMake Server ==] + +Messages sent to and from the process are wrapped in magic strings:: + + [== CMake Server ==[ + { + ... some JSON message ... + } + ]== CMake Server ==] + +The server is now ready to accept further requests via stdin. + + +Protocol API +============ + + +General Message Layout +---------------------- + +All messages need to have a "type" value, which identifies the type of +message that is passed back or forth. E.g. the initial message sent by the +server is of type "hello". Messages without a type will generate an response +of type "error". + +All requests sent to the server may contain a "cookie" value. This value +will he handed back unchanged in all responses triggered by the request. + +All responses will contain a value "inReplyTo", which may be empty in +case of parse errors, but will contain the type of the request message +in all other cases. + + +Type "reply" +^^^^^^^^^^^^ + +This type is used by the server to reply to requests. + +The message may -- depending on the type of the original request -- +contain values. + +Example:: + + [== CMake Server ==[ + {"cookie":"zimtstern","inReplyTo":"handshake","type":"reply"} + ]== CMake Server ==] + + +Type "error" +^^^^^^^^^^^^ + +This type is used to return an error condition to the client. It will +contain an "errorMessage". + +Example:: + + [== CMake Server ==[ + {"cookie":"","errorMessage":"Protocol version not supported.","inReplyTo":"handshake","type":"error"} + ]== CMake Server ==] + + +Type "progress" +^^^^^^^^^^^^^^^ + +When the server is busy for a long time, it is polite to send back replies of +type "progress" to the client. These will contain a "progressMessage" with a +string describing the action currently taking place as well as +"progressMinimum", "progressMaximum" and "progressCurrent" with integer values +describing the range of progess. + +Messages of type "progress" will be followed by more "progress" messages or with +a message of type "reply" or "error" that complete the request. + +"progress" messages may not be emitted after the "reply" or "error" message for +the request that triggered the responses was delivered. + + +Specific Message Types +---------------------- + + +Type "hello" +^^^^^^^^^^^^ + +The initial message send by the cmake server on startup is of type "hello". +This is the only message ever sent by the server that is not of type "reply", +"progress" or "error". + +It will contain "supportedProtocolVersions" with an array of server protocol +versions supported by the cmake server. These are JSON objects with "major" and +"minor" keys containing non-negative integer values. + +Example:: + + [== CMake Server ==[ + {"supportedProtocolVersions":[{"major":0,"minor":1}],"type":"hello"} + ]== CMake Server ==] + + +Type "handshake" +^^^^^^^^^^^^^^^^ + +The first request that the client may send to the server is of type "handshake". + +This request needs to pass one of the "supportedProtocolVersions" of the "hello" +type response received earlier back to the server in the "protocolVersion" field. + +Each protocol version may request additional attributes to be present. + +Protocol version 1.0 requires the following attributes to be set: + + * "sourceDirectory" with a path to the sources + * "buildDirectory" with a path to the build directory + * "generator" with the generator name + * "extraGenerator" (optional!) with the extra generator to be used. + +Example:: + + [== CMake Server ==[ + {"cookie":"zimtstern","type":"handshake","protocolVersion":{"major":0}, + "sourceDirectory":"/home/code/cmake", "buildDirectory":"/tmp/testbuild", + "generator":"Ninja"} + ]== CMake Server ==] + +which will result in a response type "reply":: + + [== CMake Server ==[ + {"cookie":"zimtstern","inReplyTo":"handshake","type":"reply"} + ]== CMake Server ==] + +indicating that the server is ready for action. diff --git a/Help/manual/cmake.1.rst b/Help/manual/cmake.1.rst index 2ccc6be..063aea1 100644 --- a/Help/manual/cmake.1.rst +++ b/Help/manual/cmake.1.rst @@ -273,6 +273,9 @@ Available commands are: ``rename <oldname> <newname>`` Rename a file or directory (on one volume). +``server`` + Launch :manual:`cmake-server(7)` mode. + ``sleep <number>...`` Sleep for given number of seconds. diff --git a/Help/release/dev/cmake-server-basic.rst b/Help/release/dev/cmake-server-basic.rst new file mode 100644 index 0000000..0b97660 --- /dev/null +++ b/Help/release/dev/cmake-server-basic.rst @@ -0,0 +1,6 @@ +cmake-server-basic +------------------ + +* A new :manual:`cmake-server(7)` mode was added to provide semantic + information about a CMake-generated buildsystem to clients through + a JSON protocol. diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 39773e1..a2dead6 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -786,6 +786,17 @@ add_executable(cmake cmakemain.cxx cmcmd.cxx cmcmd.h ${MANIFEST_FILE}) list(APPEND _tools cmake) target_link_libraries(cmake CMakeLib) +if(CMake_HAVE_SERVER_MODE) + add_library(CMakeServerLib + cmServer.cxx cmServer.h + cmServerProtocol.cxx cmServerProtocol.h + ) + target_link_libraries(CMakeServerLib CMakeLib) + set_property(SOURCE cmcmd.cxx APPEND PROPERTY COMPILE_DEFINITIONS HAVE_SERVER_MODE=1) + + target_link_libraries(cmake CMakeServerLib) +endif() + # Build CTest executable add_executable(ctest ctest.cxx ${MANIFEST_FILE}) list(APPEND _tools ctest) diff --git a/Source/cmServer.cxx b/Source/cmServer.cxx new file mode 100644 index 0000000..123b6a4 --- /dev/null +++ b/Source/cmServer.cxx @@ -0,0 +1,355 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2015 Stephen Kelly <steveire@gmail.com> + Copyright 2016 Tobias Hunger <tobias.hunger@qt.io> + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#include "cmServer.h" + +#include "cmServerProtocol.h" +#include "cmVersionMacros.h" +#include "cmake.h" + +#if defined(CMAKE_BUILD_WITH_CMAKE) +#include "cm_jsoncpp_reader.h" +#include "cm_jsoncpp_value.h" +#endif + +const char kTYPE_KEY[] = "type"; +const char kCOOKIE_KEY[] = "cookie"; +const char REPLY_TO_KEY[] = "inReplyTo"; +const char ERROR_MESSAGE_KEY[] = "errorMessage"; + +const char ERROR_TYPE[] = "error"; +const char REPLY_TYPE[] = "reply"; +const char PROGRESS_TYPE[] = "progress"; + +const char START_MAGIC[] = "[== CMake Server ==["; +const char END_MAGIC[] = "]== CMake Server ==]"; + +typedef struct +{ + uv_write_t req; + uv_buf_t buf; +} write_req_t; + +void alloc_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) +{ + (void)handle; + *buf = uv_buf_init(static_cast<char*>(malloc(suggested_size)), + static_cast<unsigned int>(suggested_size)); +} + +void free_write_req(uv_write_t* req) +{ + write_req_t* wr = reinterpret_cast<write_req_t*>(req); + free(wr->buf.base); + free(wr); +} + +void on_stdout_write(uv_write_t* req, int status) +{ + (void)status; + auto server = reinterpret_cast<cmServer*>(req->data); + free_write_req(req); + server->PopOne(); +} + +void write_data(uv_stream_t* dest, std::string content, uv_write_cb cb) +{ + write_req_t* req = static_cast<write_req_t*>(malloc(sizeof(write_req_t))); + req->req.data = dest->data; + req->buf = uv_buf_init(static_cast<char*>(malloc(content.size())), + static_cast<unsigned int>(content.size())); + memcpy(req->buf.base, content.c_str(), content.size()); + uv_write(reinterpret_cast<uv_write_t*>(req), static_cast<uv_stream_t*>(dest), + &req->buf, 1, cb); +} + +void read_stdin(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) +{ + if (nread > 0) { + auto server = reinterpret_cast<cmServer*>(stream->data); + std::string result = std::string(buf->base, buf->base + nread); + server->handleData(result); + } + + if (buf->base) + free(buf->base); +} + +cmServer::cmServer() +{ + // Register supported protocols: + this->RegisterProtocol(new cmServerProtocol1_0); +} + +cmServer::~cmServer() +{ + if (!this->Protocol) // Daemon was never fully started! + return; + + uv_close(reinterpret_cast<uv_handle_t*>(this->InputStream), NULL); + uv_close(reinterpret_cast<uv_handle_t*>(this->OutputStream), NULL); + uv_loop_close(this->Loop); + + for (cmServerProtocol* p : this->SupportedProtocols) { + delete p; + } +} + +void cmServer::PopOne() +{ + this->Writing = false; + if (this->Queue.empty()) { + return; + } + + Json::Reader reader; + Json::Value value; + const std::string input = this->Queue.front(); + this->Queue.erase(this->Queue.begin()); + + if (!reader.parse(input, value)) { + this->WriteParseError("Failed to parse JSON input."); + return; + } + + const cmServerRequest request(this, value[kTYPE_KEY].asString(), + value[kCOOKIE_KEY].asString(), value); + + if (request.Type == "") { + cmServerResponse response(request); + response.SetError("No type given in request."); + this->WriteResponse(response); + return; + } + + this->WriteResponse(this->Protocol ? this->Protocol->Process(request) + : this->SetProtocolVersion(request)); +} + +void cmServer::handleData(const std::string& data) +{ + this->DataBuffer += data; + + for (;;) { + auto needle = this->DataBuffer.find('\n'); + + if (needle == std::string::npos) { + return; + } + std::string line = this->DataBuffer.substr(0, needle); + const auto ls = line.size(); + if (ls > 1 && line.at(ls - 1) == '\r') + line.erase(ls - 1, 1); + this->DataBuffer.erase(this->DataBuffer.begin(), + this->DataBuffer.begin() + needle + 1); + if (line == START_MAGIC) { + this->JsonData.clear(); + continue; + } + if (line == END_MAGIC) { + this->Queue.push_back(this->JsonData); + this->JsonData.clear(); + if (!this->Writing) { + this->PopOne(); + } + } else { + this->JsonData += line; + this->JsonData += "\n"; + } + } +} + +void cmServer::RegisterProtocol(cmServerProtocol* protocol) +{ + auto version = protocol->ProtocolVersion(); + assert(version.first >= 0); + assert(version.second >= 0); + auto it = std::find_if(this->SupportedProtocols.begin(), + this->SupportedProtocols.end(), + [version](cmServerProtocol* p) { + return p->ProtocolVersion() == version; + }); + if (it == this->SupportedProtocols.end()) + this->SupportedProtocols.push_back(protocol); +} + +void cmServer::PrintHello() const +{ + Json::Value hello = Json::objectValue; + hello[kTYPE_KEY] = "hello"; + + Json::Value& protocolVersions = hello["supportedProtocolVersions"] = + Json::arrayValue; + + for (auto const& proto : this->SupportedProtocols) { + auto version = proto->ProtocolVersion(); + Json::Value tmp = Json::objectValue; + tmp["major"] = version.first; + tmp["minor"] = version.second; + protocolVersions.append(tmp); + } + + this->WriteJsonObject(hello); +} + +cmServerResponse cmServer::SetProtocolVersion(const cmServerRequest& request) +{ + if (request.Type != "handshake") + return request.ReportError("Waiting for type \"handshake\"."); + + Json::Value requestedProtocolVersion = request.Data["protocolVersion"]; + if (requestedProtocolVersion.isNull()) + return request.ReportError( + "\"protocolVersion\" is required for \"handshake\"."); + + if (!requestedProtocolVersion.isObject()) + return request.ReportError("\"protocolVersion\" must be a JSON object."); + + Json::Value majorValue = requestedProtocolVersion["major"]; + if (!majorValue.isInt()) + return request.ReportError("\"major\" must be set and an integer."); + + Json::Value minorValue = requestedProtocolVersion["minor"]; + if (!minorValue.isNull() && !minorValue.isInt()) + return request.ReportError("\"minor\" must be unset or an integer."); + + const int major = majorValue.asInt(); + const int minor = minorValue.isNull() ? -1 : minorValue.asInt(); + if (major < 0) + return request.ReportError("\"major\" must be >= 0."); + if (!minorValue.isNull() && minor < 0) + return request.ReportError("\"minor\" must be >= 0 when set."); + + this->Protocol = + this->FindMatchingProtocol(this->SupportedProtocols, major, minor); + if (!this->Protocol) { + return request.ReportError("Protocol version not supported."); + } + + std::string errorMessage; + if (!this->Protocol->Activate(request, &errorMessage)) { + this->Protocol = CM_NULLPTR; + return request.ReportError("Failed to activate protocol version: " + + errorMessage); + } + return request.Reply(Json::objectValue); +} + +void cmServer::Serve() +{ + assert(!this->SupportedProtocols.empty()); + assert(!this->Protocol); + + this->Loop = uv_default_loop(); + + if (uv_guess_handle(1) == UV_TTY) { + uv_tty_init(this->Loop, &this->Input.tty, 0, 1); + uv_tty_set_mode(&this->Input.tty, UV_TTY_MODE_NORMAL); + this->Input.tty.data = this; + InputStream = reinterpret_cast<uv_stream_t*>(&this->Input.tty); + + uv_tty_init(this->Loop, &this->Output.tty, 1, 0); + uv_tty_set_mode(&this->Output.tty, UV_TTY_MODE_NORMAL); + this->Output.tty.data = this; + OutputStream = reinterpret_cast<uv_stream_t*>(&this->Output.tty); + } else { + uv_pipe_init(this->Loop, &this->Input.pipe, 0); + uv_pipe_open(&this->Input.pipe, 0); + this->Input.pipe.data = this; + InputStream = reinterpret_cast<uv_stream_t*>(&this->Input.pipe); + + uv_pipe_init(this->Loop, &this->Output.pipe, 0); + uv_pipe_open(&this->Output.pipe, 1); + this->Output.pipe.data = this; + OutputStream = reinterpret_cast<uv_stream_t*>(&this->Output.pipe); + } + + this->PrintHello(); + + uv_read_start(this->InputStream, alloc_buffer, read_stdin); + + uv_run(this->Loop, UV_RUN_DEFAULT); +} + +void cmServer::WriteJsonObject(const Json::Value& jsonValue) const +{ + Json::FastWriter writer; + + std::string result = std::string("\n") + std::string(START_MAGIC) + + std::string("\n") + writer.write(jsonValue) + std::string(END_MAGIC) + + std::string("\n"); + + this->Writing = true; + write_data(this->OutputStream, result, on_stdout_write); +} + +cmServerProtocol* cmServer::FindMatchingProtocol( + const std::vector<cmServerProtocol*>& protocols, int major, int minor) +{ + cmServerProtocol* bestMatch = nullptr; + for (auto protocol : protocols) { + auto version = protocol->ProtocolVersion(); + if (major != version.first) + continue; + if (minor == version.second) + return protocol; + if (!bestMatch || bestMatch->ProtocolVersion().second < version.second) + bestMatch = protocol; + } + return minor < 0 ? bestMatch : nullptr; +} + +void cmServer::WriteProgress(const cmServerRequest& request, int min, + int current, int max, + const std::string& message) const +{ + assert(min <= current && current <= max); + assert(message.length() != 0); + + Json::Value obj = Json::objectValue; + obj[kTYPE_KEY] = PROGRESS_TYPE; + obj[REPLY_TO_KEY] = request.Type; + obj[kCOOKIE_KEY] = request.Cookie; + obj["progressMessage"] = message; + obj["progressMinimum"] = min; + obj["progressMaximum"] = max; + obj["progressCurrent"] = current; + + this->WriteJsonObject(obj); +} + +void cmServer::WriteParseError(const std::string& message) const +{ + Json::Value obj = Json::objectValue; + obj[kTYPE_KEY] = ERROR_TYPE; + obj[ERROR_MESSAGE_KEY] = message; + obj[REPLY_TO_KEY] = ""; + obj[kCOOKIE_KEY] = ""; + + this->WriteJsonObject(obj); +} + +void cmServer::WriteResponse(const cmServerResponse& response) const +{ + assert(response.IsComplete()); + + Json::Value obj = response.Data(); + obj[kCOOKIE_KEY] = response.Cookie; + obj[kTYPE_KEY] = response.IsError() ? ERROR_TYPE : REPLY_TYPE; + obj[REPLY_TO_KEY] = response.Type; + if (response.IsError()) { + obj[ERROR_MESSAGE_KEY] = response.ErrorMessage(); + } + + this->WriteJsonObject(obj); +} diff --git a/Source/cmServer.h b/Source/cmServer.h new file mode 100644 index 0000000..0ef1e17 --- /dev/null +++ b/Source/cmServer.h @@ -0,0 +1,85 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2015 Stephen Kelly <steveire@gmail.com> + Copyright 2016 Tobias Hunger <tobias.hunger@qt.io> + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#pragma once + +#include "cmListFileCache.h" +#include "cmState.h" + +#if defined(CMAKE_BUILD_WITH_CMAKE) +#include "cm_jsoncpp_value.h" +#include "cm_uv.h" +#endif + +#include <string> +#include <vector> + +class cmServerProtocol; +class cmServerRequest; +class cmServerResponse; + +class cmServer +{ +public: + cmServer(); + ~cmServer(); + + void Serve(); + + // for callbacks: + void PopOne(); + void handleData(std::string const& data); + +private: + void RegisterProtocol(cmServerProtocol* protocol); + + // Handle requests: + cmServerResponse SetProtocolVersion(const cmServerRequest& request); + + void PrintHello() const; + + // Write responses: + void WriteProgress(const cmServerRequest& request, int min, int current, + int max, const std::string& message) const; + void WriteResponse(const cmServerResponse& response) const; + void WriteParseError(const std::string& message) const; + + void WriteJsonObject(Json::Value const& jsonValue) const; + + static cmServerProtocol* FindMatchingProtocol( + const std::vector<cmServerProtocol*>& protocols, int major, int minor); + + cmServerProtocol* Protocol = nullptr; + std::vector<cmServerProtocol*> SupportedProtocols; + std::vector<std::string> Queue; + + std::string DataBuffer; + std::string JsonData; + + uv_loop_t* Loop = nullptr; + + typedef union + { + uv_tty_t tty; + uv_pipe_t pipe; + } InOutUnion; + + InOutUnion Input; + InOutUnion Output; + uv_stream_t* InputStream = nullptr; + uv_stream_t* OutputStream = nullptr; + + mutable bool Writing = false; + + friend class cmServerRequest; +}; diff --git a/Source/cmServerProtocol.cxx b/Source/cmServerProtocol.cxx new file mode 100644 index 0000000..c3a4d8e --- /dev/null +++ b/Source/cmServerProtocol.cxx @@ -0,0 +1,264 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2016 Tobias Hunger <tobias.hunger@qt.io> + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#include "cmServerProtocol.h" + +#include "cmExternalMakefileProjectGenerator.h" +#include "cmServer.h" +#include "cmSystemTools.h" +#include "cmake.h" + +#if defined(CMAKE_BUILD_WITH_CMAKE) +#include "cm_jsoncpp_reader.h" +#include "cm_jsoncpp_value.h" +#endif + +namespace { +// Vocabulary: + +const std::string kBUILD_DIRECTORY_KEY = "buildDirectory"; +const std::string kCOOKIE_KEY = "cookie"; +const std::string kEXTRA_GENERATOR_KEY = "extraGenerator"; +const std::string kGENERATOR_KEY = "generator"; +const std::string kSOURCE_DIRECTORY_KEY = "sourceDirectory"; +const std::string kTYPE_KEY = "type"; + +} // namespace + +cmServerRequest::cmServerRequest(cmServer* server, const std::string& t, + const std::string& c, const Json::Value& d) + : Type(t) + , Cookie(c) + , Data(d) + , m_Server(server) +{ +} + +void cmServerRequest::ReportProgress(int min, int current, int max, + const std::string& message) const +{ + this->m_Server->WriteProgress(*this, min, current, max, message); +} + +cmServerResponse cmServerRequest::Reply(const Json::Value& data) const +{ + cmServerResponse response(*this); + response.SetData(data); + return response; +} + +cmServerResponse cmServerRequest::ReportError(const std::string& message) const +{ + cmServerResponse response(*this); + response.SetError(message); + return response; +} + +cmServerResponse::cmServerResponse(const cmServerRequest& request) + : Type(request.Type) + , Cookie(request.Cookie) +{ +} + +void cmServerResponse::SetData(const Json::Value& data) +{ + assert(this->m_Payload == PAYLOAD_UNKNOWN); + if (!data[kCOOKIE_KEY].isNull() || !data[kTYPE_KEY].isNull()) { + this->SetError("Response contains cookie or type field."); + return; + } + this->m_Payload = PAYLOAD_DATA; + this->m_Data = data; +} + +void cmServerResponse::SetError(const std::string& message) +{ + assert(this->m_Payload == PAYLOAD_UNKNOWN); + this->m_Payload = PAYLOAD_ERROR; + this->m_ErrorMessage = message; +} + +bool cmServerResponse::IsComplete() const +{ + return this->m_Payload != PAYLOAD_UNKNOWN; +} + +bool cmServerResponse::IsError() const +{ + assert(this->m_Payload != PAYLOAD_UNKNOWN); + return this->m_Payload == PAYLOAD_ERROR; +} + +std::string cmServerResponse::ErrorMessage() const +{ + if (this->m_Payload == PAYLOAD_ERROR) + return this->m_ErrorMessage; + else + return std::string(); +} + +Json::Value cmServerResponse::Data() const +{ + assert(this->m_Payload != PAYLOAD_UNKNOWN); + return this->m_Data; +} + +bool cmServerProtocol::Activate(const cmServerRequest& request, + std::string* errorMessage) +{ + this->m_CMakeInstance = std::make_unique<cmake>(); + const bool result = this->DoActivate(request, errorMessage); + if (!result) + this->m_CMakeInstance = CM_NULLPTR; + return result; +} + +cmake* cmServerProtocol::CMakeInstance() const +{ + return this->m_CMakeInstance.get(); +} + +bool cmServerProtocol::DoActivate(const cmServerRequest& /*request*/, + std::string* /*errorMessage*/) +{ + return true; +} + +std::pair<int, int> cmServerProtocol1_0::ProtocolVersion() const +{ + return std::make_pair(1, 0); +} + +bool cmServerProtocol1_0::DoActivate(const cmServerRequest& request, + std::string* errorMessage) +{ + std::string sourceDirectory = request.Data[kSOURCE_DIRECTORY_KEY].asString(); + const std::string buildDirectory = + request.Data[kBUILD_DIRECTORY_KEY].asString(); + std::string generator = request.Data[kGENERATOR_KEY].asString(); + std::string extraGenerator = request.Data[kEXTRA_GENERATOR_KEY].asString(); + + if (buildDirectory.empty()) { + if (errorMessage) + *errorMessage = + std::string("\"") + kBUILD_DIRECTORY_KEY + "\" is missing."; + return false; + } + cmake* cm = CMakeInstance(); + if (cmSystemTools::PathExists(buildDirectory)) { + if (!cmSystemTools::FileIsDirectory(buildDirectory)) { + if (errorMessage) + *errorMessage = std::string("\"") + kBUILD_DIRECTORY_KEY + + "\" exists but is not a directory."; + return false; + } + + const std::string cachePath = cm->FindCacheFile(buildDirectory); + if (cm->LoadCache(cachePath)) { + cmState* state = cm->GetState(); + + // Check generator: + const std::string cachedGenerator = + std::string(state->GetCacheEntryValue("CMAKE_GENERATOR")); + if (cachedGenerator.empty() && generator.empty()) { + if (errorMessage) + *errorMessage = + std::string("\"") + kGENERATOR_KEY + "\" is required but unset."; + return false; + } + if (generator.empty()) { + generator = cachedGenerator; + } + if (generator != cachedGenerator) { + if (errorMessage) + *errorMessage = std::string("\"") + kGENERATOR_KEY + + "\" set but incompatible with configured generator."; + return false; + } + + // check extra generator: + const std::string cachedExtraGenerator = + std::string(state->GetCacheEntryValue("CMAKE_EXTRA_GENERATOR")); + if (!cachedExtraGenerator.empty() && !extraGenerator.empty() && + cachedExtraGenerator != extraGenerator) { + if (errorMessage) + *errorMessage = std::string("\"") + kEXTRA_GENERATOR_KEY + + "\" is set but incompatible with configured extra generator."; + return false; + } + if (extraGenerator.empty()) { + extraGenerator = cachedExtraGenerator; + } + + // check sourcedir: + const std::string cachedSourceDirectory = + std::string(state->GetCacheEntryValue("CMAKE_HOME_DIRECTORY")); + if (!cachedSourceDirectory.empty() && !sourceDirectory.empty() && + cachedSourceDirectory != sourceDirectory) { + if (errorMessage) + *errorMessage = std::string("\"") + kSOURCE_DIRECTORY_KEY + + "\" is set but incompatible with configured source directory."; + return false; + } + if (sourceDirectory.empty()) { + sourceDirectory = cachedSourceDirectory; + } + } + } + + if (sourceDirectory.empty()) { + if (errorMessage) + *errorMessage = std::string("\"") + kSOURCE_DIRECTORY_KEY + + "\" is unset but required."; + return false; + } + if (!cmSystemTools::FileIsDirectory(sourceDirectory)) { + if (errorMessage) + *errorMessage = + std::string("\"") + kSOURCE_DIRECTORY_KEY + "\" is not a directory."; + return false; + } + if (generator.empty()) { + if (errorMessage) + *errorMessage = + std::string("\"") + kGENERATOR_KEY + "\" is unset but required."; + return false; + } + + const std::string fullGeneratorName = + cmExternalMakefileProjectGenerator::CreateFullGeneratorName( + generator, extraGenerator); + + cmGlobalGenerator* gg = cm->CreateGlobalGenerator(fullGeneratorName); + if (!gg) { + if (errorMessage) + *errorMessage = + std::string("Could not set up the requested combination of \"") + + kGENERATOR_KEY + "\" and \"" + kEXTRA_GENERATOR_KEY + "\""; + return false; + } + + cm->SetGlobalGenerator(gg); + cm->SetHomeDirectory(sourceDirectory); + cm->SetHomeOutputDirectory(buildDirectory); + + this->m_State = STATE_ACTIVE; + return true; +} + +const cmServerResponse cmServerProtocol1_0::Process( + const cmServerRequest& request) +{ + assert(this->m_State >= STATE_ACTIVE); + + return request.ReportError("Unknown command!"); +} diff --git a/Source/cmServerProtocol.h b/Source/cmServerProtocol.h new file mode 100644 index 0000000..33183e9 --- /dev/null +++ b/Source/cmServerProtocol.h @@ -0,0 +1,115 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2016 Tobias Hunger <tobias.hunger@qt.io> + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#pragma once + +#include "cmListFileCache.h" + +#if defined(CMAKE_BUILD_WITH_CMAKE) +#include "cm_jsoncpp_writer.h" +#endif + +#include <memory> +#include <string> + +class cmake; +class cmServer; + +class cmServerRequest; + +class cmServerResponse +{ +public: + explicit cmServerResponse(const cmServerRequest& request); + + void SetData(const Json::Value& data); + void SetError(const std::string& message); + + bool IsComplete() const; + bool IsError() const; + std::string ErrorMessage() const; + Json::Value Data() const; + + const std::string Type; + const std::string Cookie; + +private: + enum PayLoad + { + PAYLOAD_UNKNOWN, + PAYLOAD_ERROR, + PAYLOAD_DATA + }; + PayLoad m_Payload = PAYLOAD_UNKNOWN; + std::string m_ErrorMessage; + Json::Value m_Data; +}; + +class cmServerRequest +{ +public: + void ReportProgress(int min, int current, int max, + const std::string& message) const; + + cmServerResponse Reply(const Json::Value& data) const; + cmServerResponse ReportError(const std::string& message) const; + + const std::string Type; + const std::string Cookie; + const Json::Value Data; + +private: + cmServerRequest(cmServer* server, const std::string& t, const std::string& c, + const Json::Value& d); + + cmServer* m_Server; + + friend class cmServer; +}; + +class cmServerProtocol +{ +public: + virtual ~cmServerProtocol() {} + + virtual std::pair<int, int> ProtocolVersion() const = 0; + virtual const cmServerResponse Process(const cmServerRequest& request) = 0; + + bool Activate(const cmServerRequest& request, std::string* errorMessage); + +protected: + cmake* CMakeInstance() const; + // Implement protocol specific activation tasks here. Called from Activate(). + virtual bool DoActivate(const cmServerRequest& request, + std::string* errorMessage); + +private: + std::unique_ptr<cmake> m_CMakeInstance; +}; + +class cmServerProtocol1_0 : public cmServerProtocol +{ +public: + std::pair<int, int> ProtocolVersion() const override; + const cmServerResponse Process(const cmServerRequest& request) override; + +private: + bool DoActivate(const cmServerRequest& request, + std::string* errorMessage) override; + + enum State + { + STATE_INACTIVE, + STATE_ACTIVE + }; + State m_State = STATE_INACTIVE; +}; diff --git a/Source/cmake.cxx b/Source/cmake.cxx index 112a5f7..0c84283 100644 --- a/Source/cmake.cxx +++ b/Source/cmake.cxx @@ -234,7 +234,7 @@ cmake::~cmake() } #if defined(CMAKE_BUILD_WITH_CMAKE) -Json::Value cmake::ReportCapabilitiesJson() const +Json::Value cmake::ReportCapabilitiesJson(bool haveServerMode) const { Json::Value obj = Json::objectValue; // Version information: @@ -280,22 +280,18 @@ Json::Value cmake::ReportCapabilitiesJson() const generators.append(i->second); } obj["generators"] = generators; + obj["serverMode"] = haveServerMode; -#if defined(HAVE_SERVER_MODE) && HAVE_SERVER_MODE - obj["serverMode"] = true; -#else - obj["serverMode"] = false; -#endif return obj; } #endif -std::string cmake::ReportCapabilities() const +std::string cmake::ReportCapabilities(bool haveServerMode) const { std::string result; #if defined(CMAKE_BUILD_WITH_CMAKE) Json::FastWriter writer; - result = writer.write(this->ReportCapabilitiesJson()); + result = writer.write(this->ReportCapabilitiesJson(haveServerMode)); #else result = "Not supported"; #endif diff --git a/Source/cmake.h b/Source/cmake.h index 6095a59..a21c9ca 100644 --- a/Source/cmake.h +++ b/Source/cmake.h @@ -123,9 +123,9 @@ public: ~cmake(); #if defined(CMAKE_BUILD_WITH_CMAKE) - Json::Value ReportCapabilitiesJson() const; + Json::Value ReportCapabilitiesJson(bool haveServerMode) const; #endif - std::string ReportCapabilities() const; + std::string ReportCapabilities(bool haveServerMode) const; static const char* GetCMakeFilesDirectory() { return "/CMakeFiles"; } static const char* GetCMakeFilesDirectoryPostSlash() diff --git a/Source/cmcmd.cxx b/Source/cmcmd.cxx index 900bba0..c09ea8b 100644 --- a/Source/cmcmd.cxx +++ b/Source/cmcmd.cxx @@ -23,6 +23,10 @@ #include "cm_auto_ptr.hxx" #include "cmake.h" +#if defined(HAVE_SERVER_MODE) && HAVE_SERVER_MODE +#include "cmServer.h" +#endif + #if defined(CMAKE_BUILD_WITH_CMAKE) #include "cmDependsFortran.h" // For -E cmake_copy_f90_mod callback. #endif @@ -91,6 +95,7 @@ void CMakeCommandUsage(const char* program) << " remove_directory dir - remove a directory and its contents\n" << " rename oldname newname - rename a file or directory " "(on one volume)\n" + << " server - start cmake in server mode\n" << " sleep <number>... - sleep for given number of seconds\n" << " tar [cxt][vf][zjJ] file.tar [file/dir1 file/dir2 ...]\n" << " - create or extract a tar or zip archive\n" @@ -527,7 +532,11 @@ int cmcmd::ExecuteCMakeCommand(std::vector<std::string>& args) return 1; } cmake cm; - std::cout << cm.ReportCapabilities(); +#if defined(HAVE_SERVER_MODE) && HAVE_SERVER_MODE + std::cout << cm.ReportCapabilities(true); +#else + std::cout << cm.ReportCapabilities(false); +#endif return 0; } @@ -903,6 +912,19 @@ int cmcmd::ExecuteCMakeCommand(std::vector<std::string>& args) #endif } return 0; + } else if (args[1] == "server") { + if (args.size() > 2) { + cmSystemTools::Error("Too many arguments to start server mode"); + return 1; + } +#if defined(HAVE_SERVER_MODE) && HAVE_SERVER_MODE + cmServer server; + server.Serve(); + return 0; +#else + cmSystemTools::Error("CMake was not built with server mode enabled"); + return 1; +#endif } #if defined(CMAKE_BUILD_WITH_CMAKE) diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 97770ed..8cf1faa 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -2722,6 +2722,15 @@ ${CMake_BINARY_DIR}/bin/cmake -DDIR=dev -P ${CMake_SOURCE_DIR}/Utilities/Release ADD_TEST_MACRO(CMakeCommands.target_compile_definitions target_compile_definitions) ADD_TEST_MACRO(CMakeCommands.target_compile_options target_compile_options) + if(CMake_HAVE_SERVER_MODE) + # The cmake server-mode test requires python for a simple client. + find_package(PythonInterp QUIET) + if(PYTHON_EXECUTABLE) + set(Server_BUILD_OPTIONS -DPYTHON_EXECUTABLE:FILEPATH=${PYTHON_EXECUTABLE}) + ADD_TEST_MACRO(Server Server) + endif() + endif() + configure_file( "${CMake_SOURCE_DIR}/Tests/CTestTestCrash/test.cmake.in" "${CMake_BINARY_DIR}/Tests/CTestTestCrash/test.cmake" diff --git a/Tests/RunCMake/CommandLine/E_server-arg-result.txt b/Tests/RunCMake/CommandLine/E_server-arg-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/CommandLine/E_server-arg-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/CommandLine/E_server-arg-stderr.txt b/Tests/RunCMake/CommandLine/E_server-arg-stderr.txt new file mode 100644 index 0000000..7877c01 --- /dev/null +++ b/Tests/RunCMake/CommandLine/E_server-arg-stderr.txt @@ -0,0 +1 @@ +^CMake Error: Too many arguments to start server mode$ diff --git a/Tests/RunCMake/CommandLine/RunCMakeTest.cmake b/Tests/RunCMake/CommandLine/RunCMakeTest.cmake index 6ae47a8..9f76ad9 100644 --- a/Tests/RunCMake/CommandLine/RunCMakeTest.cmake +++ b/Tests/RunCMake/CommandLine/RunCMakeTest.cmake @@ -12,6 +12,7 @@ run_cmake_command(E_capabilities ${CMAKE_COMMAND} -E capabilities) run_cmake_command(E_capabilities-arg ${CMAKE_COMMAND} -E capabilities --extra-arg) run_cmake_command(E_echo_append ${CMAKE_COMMAND} -E echo_append) run_cmake_command(E_rename-no-arg ${CMAKE_COMMAND} -E rename) +run_cmake_command(E_server-arg ${CMAKE_COMMAND} -E server --extra-arg) run_cmake_command(E_touch_nocreate-no-arg ${CMAKE_COMMAND} -E touch_nocreate) run_cmake_command(E_time ${CMAKE_COMMAND} -E time ${CMAKE_COMMAND} -E echo "hello world") diff --git a/Tests/Server/CMakeLists.txt b/Tests/Server/CMakeLists.txt new file mode 100644 index 0000000..8daf12a --- /dev/null +++ b/Tests/Server/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.4) +project(Server CXX) + +find_package(PythonInterp REQUIRED) + +macro(do_test bsname file) + execute_process(COMMAND ${PYTHON_EXECUTABLE} + "${CMAKE_SOURCE_DIR}/server-test.py" + "${CMAKE_COMMAND}" + "${CMAKE_SOURCE_DIR}/${file}" + "${CMAKE_SOURCE_DIR}" + "${CMAKE_BINARY_DIR}" + RESULT_VARIABLE test_result + ) + + if (NOT test_result EQUAL 0) + message(SEND_ERROR "TEST FAILED") + endif() +endmacro() + +do_test("test_handshake" "tc_handshake.json") + +add_executable(Server empty.cpp) diff --git a/Tests/Server/cmakelib.py b/Tests/Server/cmakelib.py new file mode 100644 index 0000000..48ebc89 --- /dev/null +++ b/Tests/Server/cmakelib.py @@ -0,0 +1,126 @@ +import sys, subprocess, json + +termwidth = 150 + +print_communication = True + +def ordered(obj): + if isinstance(obj, dict): + return sorted((k, ordered(v)) for k, v in obj.items()) + if isinstance(obj, list): + return sorted(ordered(x) for x in obj) + else: + return obj + +def col_print(title, array): + print + print + print(title) + + indentwidth = 4 + indent = " " * indentwidth + + if not array: + print(indent + "<None>") + return + + padwidth = 2 + + maxitemwidth = len(max(array, key=len)) + + numCols = max(1, int((termwidth - indentwidth + padwidth) / (maxitemwidth + padwidth))) + + numRows = len(array) // numCols + 1 + + pad = " " * padwidth + + for index in range(numRows): + print(indent + pad.join(item.ljust(maxitemwidth) for item in array[index::numRows])) + +def waitForRawMessage(cmakeCommand): + stdoutdata = "" + payload = "" + while not cmakeCommand.poll(): + stdoutdataLine = cmakeCommand.stdout.readline() + if stdoutdataLine: + stdoutdata += stdoutdataLine.decode('utf-8') + else: + break + begin = stdoutdata.find("[== CMake Server ==[\n") + end = stdoutdata.find("]== CMake Server ==]") + + if (begin != -1 and end != -1): + begin += len("[== CMake Server ==[\n") + payload = stdoutdata[begin:end] + if print_communication: + print("\nSERVER>", json.loads(payload), "\n") + return json.loads(payload) + +def writeRawData(cmakeCommand, content): + writeRawData.counter += 1 + payload = """ +[== CMake Server ==[ +%s +]== CMake Server ==] +""" % content + + rn = ( writeRawData.counter % 2 ) == 0 + + if rn: + payload = payload.replace('\n', '\r\n') + + if print_communication: + print("\nCLIENT>", content, "(Use \\r\\n:", rn, ")\n") + cmakeCommand.stdin.write(payload.encode('utf-8')) + cmakeCommand.stdin.flush() +writeRawData.counter = 0 + +def writePayload(cmakeCommand, obj): + writeRawData(cmakeCommand, json.dumps(obj)) + +def initProc(cmakeCommand): + cmakeCommand = subprocess.Popen([cmakeCommand, "-E", "server"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + + packet = waitForRawMessage(cmakeCommand) + if packet == None: + print("Not in server mode") + sys.exit(1) + + if packet['type'] != 'hello': + print("No hello message") + sys.exit(1) + + return cmakeCommand + +def waitForMessage(cmakeCommand, expected): + data = ordered(expected) + packet = ordered(waitForRawMessage(cmakeCommand)) + + if packet != data: + sys.exit(-1) + return packet + +def waitForReply(cmakeCommand, originalType, cookie): + packet = waitForRawMessage(cmakeCommand) + if packet['cookie'] != cookie or packet['type'] != 'reply' or packet['inReplyTo'] != originalType: + sys.exit(1) + +def waitForError(cmakeCommand, originalType, cookie, message): + packet = waitForRawMessage(cmakeCommand) + if packet['cookie'] != cookie or packet['type'] != 'error' or packet['inReplyTo'] != originalType or packet['errorMessage'] != message: + sys.exit(1) + +def waitForProgress(cmakeCommand, originalType, cookie, current, message): + packet = waitForRawMessage(cmakeCommand) + if packet['cookie'] != cookie or packet['type'] != 'progress' or packet['inReplyTo'] != originalType or packet['progressCurrent'] != current or packet['progressMessage'] != message: + sys.exit(1) + +def handshake(cmakeCommand, major, minor): + version = { 'major': major } + if minor >= 0: + version['minor'] = minor + + writePayload(cmakeCommand, { 'type': 'handshake', 'protocolVersion': version, 'cookie': 'TEST_HANDSHAKE' }) + waitForReply(cmakeCommand, 'handshake', 'TEST_HANDSHAKE') diff --git a/Tests/Server/empty.cpp b/Tests/Server/empty.cpp new file mode 100644 index 0000000..766b775 --- /dev/null +++ b/Tests/Server/empty.cpp @@ -0,0 +1,5 @@ + +int main() +{ + return 0; +} diff --git a/Tests/Server/server-test.py b/Tests/Server/server-test.py new file mode 100644 index 0000000..e0a3b3b --- /dev/null +++ b/Tests/Server/server-test.py @@ -0,0 +1,82 @@ +import sys, cmakelib, json + +debug = True + +cmakeCommand = sys.argv[1] +testFile = sys.argv[2] +sourceDir = sys.argv[3] +buildDir = sys.argv[4] + +print("SourceDir: ", sourceDir, " -- BuildDir: ", buildDir) + +proc = cmakelib.initProc(cmakeCommand) + +with open(testFile) as f: + testText = f.read() + testText = testText.replace('%BUILDDIR%', buildDir) + testText = testText.replace('%SOURCEDIR%', sourceDir) + testData = json.loads(testText) + +buildDir = sys.argv[3] +sourceDir = sys.argv[4] + +for obj in testData: + if 'sendRaw' in obj: + data = obj['sendRaw'] + if debug: print("Sending raw:", data) + cmakelib.writeRawData(proc, data) + elif 'send' in obj: + data = obj['send'] + if debug: print("Sending:", json.dumps(data)) + cmakelib.writePayload(proc, data) + elif 'recv' in obj: + data = obj['recv'] + if debug: print("Waiting for:", json.dumps(data)) + cmakelib.waitForMessage(proc, data) + elif 'reply' in obj: + data = obj['reply'] + if debug: print("Waiting for reply:", json.dumps(data)) + originalType = "" + cookie = "" + if 'cookie' in data: cookie = data['cookie'] + if 'type' in data: originalType = data['type'] + cmakelib.waitForReply(proc, originalType, cookie) + elif 'error' in obj: + data = obj['error'] + if debug: print("Waiting for error:", json.dumps(data)) + originalType = "" + cookie = "" + message = "" + if 'cookie' in data: cookie = data['cookie'] + if 'type' in data: originalType = data['type'] + if 'message' in data: message = data['message'] + cmakelib.waitForError(proc, originalType, cookie, message) + elif 'progress' in obj: + data = obj['progress'] + if debug: print("Waiting for progress:", json.dumps(data)) + originalType = '' + cookie = "" + current = 0 + message = "" + if 'cookie' in data: cookie = data['cookie'] + if 'type' in data: originalType = data['type'] + if 'current' in data: current = data['current'] + if 'message' in data: message = data['message'] + cmakelib.waitForProgress(proc, originalType, cookie, current, message) + elif 'handshake' in obj: + data = obj['handshake'] + if debug: print("Doing handshake:", json.dumps(data)) + major = -1 + minor = -1 + if 'major' in data: major = data['major'] + if 'minor' in data: minor = data['minor'] + cmakelib.handshake(proc, major, minor) + elif 'message' in obj: + print("MESSAGE:", obj["message"]) + else: + print("Unknown command:", json.dumps(obj)) + sys.exit(2) + + print("Completed") + +sys.exit(0) diff --git a/Tests/Server/tc_handshake.json b/Tests/Server/tc_handshake.json new file mode 100644 index 0000000..5261581 --- /dev/null +++ b/Tests/Server/tc_handshake.json @@ -0,0 +1,71 @@ +[ +{ "message": "Testing basic message handling:" }, + +{ "sendRaw": "Sometext"}, +{ "recv": {"cookie":"","errorMessage":"Failed to parse JSON input.","inReplyTo":"","type":"error"} }, + +{ "message": "Testing invalid json input"}, +{ "send": { "test": "sometext" } }, +{ "recv": {"cookie":"","errorMessage":"No type given in request.","inReplyTo":"","type":"error"} }, + +{ "send": {"test": "sometext","cookie":"monster"} }, +{ "recv": {"cookie":"monster","errorMessage":"No type given in request.","inReplyTo":"","type":"error"} }, + +{ "message": "Testing handshake" }, +{ "send": {"type": "sometype","cookie":"monster2"} }, +{ "recv": {"cookie":"monster2","errorMessage":"Waiting for type \"handshake\".","inReplyTo":"sometype","type":"error"} }, + +{ "send": {"type": "handshake"} }, +{ "recv": {"cookie":"","errorMessage":"\"protocolVersion\" is required for \"handshake\".","inReplyTo":"handshake","type":"error"} }, + +{ "send": {"type": "handshake","foo":"bar"} }, +{ "recv": {"cookie":"","errorMessage":"\"protocolVersion\" is required for \"handshake\".","inReplyTo":"handshake","type":"error"} }, + +{ "send": {"type": "handshake","protocolVersion":"bar"} }, +{ "recv": {"cookie":"","errorMessage":"\"protocolVersion\" must be a JSON object.","inReplyTo":"handshake","type":"error"} }, + +{ "send": {"type": "handshake","protocolVersion":{}} }, +{ "recv": {"cookie":"","errorMessage":"\"major\" must be set and an integer.","inReplyTo":"handshake","type":"error"} }, + +{ "send": {"type": "handshake","protocolVersion":{"major":"foo"}} }, +{ "recv": {"cookie":"","errorMessage":"\"major\" must be set and an integer.","inReplyTo":"handshake","type":"error"} }, + +{ "send": {"type": "handshake","protocolVersion":{"major":1, "minor":"foo"}} }, +{ "recv": {"cookie":"","errorMessage":"\"minor\" must be unset or an integer.","inReplyTo":"handshake","type":"error"} }, + +{ "send": {"type": "handshake","protocolVersion":{"major":-1, "minor":-1}} }, +{ "recv": {"cookie":"","errorMessage":"\"major\" must be >= 0.","inReplyTo":"handshake","type":"error"} }, + +{ "send": {"type": "handshake","protocolVersion":{"major":10, "minor":-1}} }, +{ "recv": {"cookie":"","errorMessage":"\"minor\" must be >= 0 when set.","inReplyTo":"handshake","type":"error"} }, + +{ "send": {"type": "handshake","protocolVersion":{"major":10000}} }, +{ "recv": {"cookie":"","errorMessage":"Protocol version not supported.","inReplyTo":"handshake","type":"error"} }, + +{ "send": {"type": "handshake","protocolVersion":{"major":1, "minor":10000}} }, +{ "recv": {"cookie":"","errorMessage":"Protocol version not supported.","inReplyTo":"handshake","type":"error"} }, + +{ "send": {"cookie":"zimtstern","type": "handshake","protocolVersion":{"major":1}} }, +{ "recv": {"cookie":"zimtstern","inReplyTo":"handshake","type":"error","errorMessage":"Failed to activate protocol version: \"buildDirectory\" is missing."} }, + +{ "message": "Testing protocol version specific options (1.0):" }, +{ "send": {"cookie":"zimtstern","type": "handshake","protocolVersion":{"major":1},"sourceDirectory":"/tmp/src"} }, +{ "recv": {"cookie":"zimtstern","inReplyTo":"handshake","type":"error","errorMessage":"Failed to activate protocol version: \"buildDirectory\" is missing."} }, + +{ "send": {"cookie":"zimtstern","type": "handshake","protocolVersion":{"major":1},"sourceDirectory":"/tmp/src","buildDirectory":"/tmp/build"} }, +{ "recv": {"cookie":"zimtstern","inReplyTo":"handshake","type":"error","errorMessage":"Failed to activate protocol version: \"sourceDirectory\" is not a directory."} }, + +{ "send": {"cookie":"zimtstern","type": "handshake","protocolVersion":{"major":1},"sourceDirectory":".","buildDirectory":"/tmp/build","extraGenerator":"CodeBlocks"} }, +{ "recv": {"cookie":"zimtstern","inReplyTo":"handshake","type":"error","errorMessage":"Failed to activate protocol version: \"generator\" is unset but required."} }, + +{ "send": {"cookie":"zimtstern","type": "handshake","protocolVersion":{"major":1},"sourceDirectory":".","buildDirectory":"/tmp/build","generator":"XXXX","extraGenerator":"CodeBlocks"} }, +{ "recv": {"cookie":"zimtstern","inReplyTo":"handshake","type":"error","errorMessage":"Failed to activate protocol version: Could not set up the requested combination of \"generator\" and \"extraGenerator\""} }, + +{ "send": {"cookie":"zimtstern","type": "handshake","protocolVersion":{"major":1},"sourceDirectory":".","buildDirectory":"/tmp/build","generator":"Ninja","extraGenerator":"XXXX"} }, +{ "recv": {"cookie":"zimtstern","inReplyTo":"handshake","type":"error","errorMessage":"Failed to activate protocol version: Could not set up the requested combination of \"generator\" and \"extraGenerator\""} }, + +{ "send": {"cookie":"zimtstern","type": "handshake","protocolVersion":{"major":1},"sourceDirectory":".","buildDirectory":"/tmp/build","generator":"Ninja","extraGenerator":"CodeBlocks"} }, +{ "recv": {"cookie":"zimtstern","inReplyTo":"handshake","type":"reply"} }, + +{ "message": "Everything ok." } +] |