فهرست منبع

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
Brad King 9 سال پیش
والد
کامیت
5c87b92b1b

+ 12 - 0
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

+ 1 - 0
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

+ 188 - 0
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.

+ 3 - 0
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.
 

+ 6 - 0
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.

+ 11 - 0
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)

+ 355 - 0
Source/cmServer.cxx

@@ -0,0 +1,355 @@
+/*============================================================================
+  CMake - Cross Platform Makefile Generator
+  Copyright 2015 Stephen Kelly <[email protected]>
+  Copyright 2016 Tobias Hunger <[email protected]>
+
+  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);
+}

+ 85 - 0
Source/cmServer.h

@@ -0,0 +1,85 @@
+/*============================================================================
+  CMake - Cross Platform Makefile Generator
+  Copyright 2015 Stephen Kelly <[email protected]>
+  Copyright 2016 Tobias Hunger <[email protected]>
+
+  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;
+};

+ 264 - 0
Source/cmServerProtocol.cxx

@@ -0,0 +1,264 @@
+/*============================================================================
+  CMake - Cross Platform Makefile Generator
+  Copyright 2016 Tobias Hunger <[email protected]>
+
+  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!");
+}

+ 115 - 0
Source/cmServerProtocol.h

@@ -0,0 +1,115 @@
+/*============================================================================
+  CMake - Cross Platform Makefile Generator
+  Copyright 2016 Tobias Hunger <[email protected]>
+
+  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;
+};

+ 4 - 8
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

+ 2 - 2
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()

+ 23 - 1
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)

+ 9 - 0
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"

+ 1 - 0
Tests/RunCMake/CommandLine/E_server-arg-result.txt

@@ -0,0 +1 @@
+1

+ 1 - 0
Tests/RunCMake/CommandLine/E_server-arg-stderr.txt

@@ -0,0 +1 @@
+^CMake Error: Too many arguments to start server mode$

+ 1 - 0
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")

+ 23 - 0
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)

+ 126 - 0
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')

+ 5 - 0
Tests/Server/empty.cpp

@@ -0,0 +1,5 @@
+
+int main()
+{
+  return 0;
+}

+ 82 - 0
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)

+ 71 - 0
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." }
+]