Commit 1d601c6c authored by Tobias Hunger's avatar Tobias Hunger Committed by Brad King
Browse files

server-mode: Introduce cmServerConnection

Use it to split pipe and stdin/out handling out of cmServer itself.

The server will shut down when it looses its connection to the client.
This has the nice property that a crashing client will cause the server
to terminate as the OS will close the connection on behave of the client.
parent 2c2ffd38
Pipeline #27366 passed with stage
......@@ -49,12 +49,16 @@ Operation
Start :manual:`cmake(1)` in the server command mode, supplying the path to
the build directory to process::
cmake -E server
cmake -E server (--debug|--pipe <NAMED_PIPE>)
The server will start up and reply with an hello message on stdout::
The server will communicate using stdin/stdout (with the ``--debug`` parameter)
or using a named pipe (with the ``--pipe <NAMED_PIPE>`` parameter).
When connecting to the server (via named pipe or by starting it in ``--debug``
mode), the server will reply with a hello message::
[== CMake Server ==[
{"supportedProtocolVersions":[{"major":0,"minor":1}],"type":"hello"}
{"supportedProtocolVersions":[{"major":1,"minor":0}],"type":"hello"}
]== CMake Server ==]
Messages sent to and from the process are wrapped in magic strings::
......@@ -65,7 +69,8 @@ Messages sent to and from the process are wrapped in magic strings::
}
]== CMake Server ==]
The server is now ready to accept further requests via stdin.
The server is now ready to accept further requests via the named pipe
or stdin.
Debugging
......
......@@ -789,6 +789,7 @@ target_link_libraries(cmake CMakeLib)
if(CMake_HAVE_SERVER_MODE)
add_library(CMakeServerLib
cmServer.cxx cmServer.h
cmServerConnection.cxx cmServerConnection.h
cmServerProtocol.cxx cmServerProtocol.h
)
target_link_libraries(CMakeServerLib CMakeLib)
......
......@@ -13,6 +13,7 @@
#include "cmServer.h"
#include "cmServerConnection.h"
#include "cmServerProtocol.h"
#include "cmSystemTools.h"
#include "cmVersionMacros.h"
......@@ -40,57 +41,6 @@ static const std::string kMESSAGE_TYPE = "message";
static const std::string kSTART_MAGIC = "[== CMake Server ==[";
static const std::string kEND_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);
}
class cmServer::DebugInfo
{
public:
......@@ -105,9 +55,11 @@ public:
uint64_t StartTime;
};
cmServer::cmServer(bool supportExperimental)
: SupportExperimental(supportExperimental)
cmServer::cmServer(cmServerConnection* conn, bool supportExperimental)
: Connection(conn)
, SupportExperimental(supportExperimental)
{
this->Connection->SetServer(this);
// Register supported protocols:
this->RegisterProtocol(new cmServerProtocol1_0);
}
......@@ -118,18 +70,15 @@ cmServer::~cmServer()
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;
}
delete this->Connection;
}
void cmServer::PopOne()
{
this->Writing = false;
if (this->Queue.empty()) {
return;
}
......@@ -173,39 +122,6 @@ void cmServer::PopOne()
}
}
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 == kSTART_MAGIC) {
this->JsonData.clear();
continue;
}
if (line == kEND_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)
{
if (protocol->IsExperimental() && !this->SupportExperimental) {
......@@ -245,6 +161,12 @@ void cmServer::PrintHello() const
this->WriteJsonObject(hello, nullptr);
}
void cmServer::QueueRequest(const std::string& request)
{
this->Queue.push_back(request);
this->PopOne();
}
void cmServer::reportProgress(const char* msg, float progress, void* data)
{
const cmServerRequest* request = static_cast<const cmServerRequest*>(data);
......@@ -312,43 +234,16 @@ cmServerResponse cmServer::SetProtocolVersion(const cmServerRequest& request)
return request.Reply(Json::objectValue);
}
bool cmServer::Serve()
bool cmServer::Serve(std::string* errorMessage)
{
if (this->SupportedProtocols.empty()) {
*errorMessage =
"No protocol versions defined. Maybe you need --experimental?";
return false;
}
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);
return true;
return Connection->ProcessEvents(errorMessage);
}
void cmServer::WriteJsonObject(const Json::Value& jsonValue,
......@@ -385,10 +280,8 @@ void cmServer::WriteJsonObject(const Json::Value& jsonValue,
}
}
this->Writing = true;
write_data(this->OutputStream, std::string("\n") + kSTART_MAGIC +
std::string("\n") + result + kEND_MAGIC + std::string("\n"),
on_stdout_write);
Connection->WriteData(std::string("\n") + kSTART_MAGIC + std::string("\n") +
result + kEND_MAGIC + std::string("\n"));
}
cmServerProtocol* cmServer::FindMatchingProtocol(
......
......@@ -24,6 +24,7 @@
#include <string>
#include <vector>
class cmServerConnection;
class cmServerProtocol;
class cmServerRequest;
class cmServerResponse;
......@@ -33,18 +34,18 @@ class cmServer
public:
class DebugInfo;
cmServer(bool supportExperimental);
cmServer(cmServerConnection* conn, bool supportExperimental);
~cmServer();
bool Serve();
// for callbacks:
void PopOne();
void handleData(std::string const& data);
bool Serve(std::string* errorMessage);
private:
void RegisterProtocol(cmServerProtocol* protocol);
// Callbacks from cmServerConnection:
void PopOne();
void QueueRequest(const std::string& request);
static void reportProgress(const char* msg, float progress, void* data);
static void reportMessage(const char* msg, const char* title, bool& cancel,
void* data);
......@@ -69,6 +70,7 @@ private:
static cmServerProtocol* FindMatchingProtocol(
const std::vector<cmServerProtocol*>& protocols, int major, int minor);
cmServerConnection* Connection = nullptr;
const bool SupportExperimental;
cmServerProtocol* Protocol = nullptr;
......@@ -94,4 +96,5 @@ private:
mutable bool Writing = false;
friend class cmServerRequest;
friend class cmServerConnection;
};
/*============================================================================
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 "cmServerConnection.h"
#include <cmServer.h>
#include <assert.h>
namespace {
static const std::string kSTART_MAGIC = "[== CMake Server ==[";
static const std::string kEND_MAGIC = "]== CMake Server ==]";
struct write_req_t
{
uv_write_t req;
uv_buf_t buf;
};
void on_alloc_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf)
{
(void)(handle);
char* rawBuffer = new char[suggested_size];
*buf = uv_buf_init(rawBuffer, static_cast<unsigned int>(suggested_size));
}
void on_read(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf)
{
auto conn = reinterpret_cast<cmServerConnection*>(stream->data);
if (nread >= 0) {
conn->ReadData(std::string(buf->base, buf->base + nread));
} else {
conn->HandleEof();
}
delete[](buf->base);
}
void on_write(uv_write_t* req, int status)
{
(void)(status);
auto conn = reinterpret_cast<cmServerConnection*>(req->data);
// Free req and buffer
write_req_t* wr = reinterpret_cast<write_req_t*>(req);
delete[](wr->buf.base);
delete wr;
conn->ProcessNextRequest();
}
void on_new_connection(uv_stream_t* stream, int status)
{
(void)(status);
auto conn = reinterpret_cast<cmServerConnection*>(stream->data);
conn->Connect(stream);
}
} // namespace
class LoopGuard
{
public:
LoopGuard(cmServerConnection* connection)
: Connection(connection)
{
Connection->mLoop = uv_default_loop();
}
~LoopGuard()
{
uv_loop_close(Connection->mLoop);
Connection->mLoop = nullptr;
}
private:
cmServerConnection* Connection;
};
cmServerConnection::cmServerConnection()
{
}
cmServerConnection::~cmServerConnection()
{
}
void cmServerConnection::SetServer(cmServer* s)
{
this->Server = s;
}
bool cmServerConnection::ProcessEvents(std::string* errorMessage)
{
assert(this->Server);
errorMessage->clear();
this->RawReadBuffer.clear();
this->RequestBuffer.clear();
LoopGuard guard(this);
(void)(guard);
if (!this->mLoop) {
*errorMessage = "Internal Error: Failed to create event loop.";
return false;
}
if (!DoSetup(errorMessage)) {
return false;
}
if (uv_run(this->mLoop, UV_RUN_DEFAULT) != 0) {
*errorMessage = "Internal Error: Event loop stopped in unclean state.";
return false;
}
// These need to be cleaned up by now:
assert(!this->ReadStream);
assert(!this->WriteStream);
this->RawReadBuffer.clear();
this->RequestBuffer.clear();
return true;
}
void cmServerConnection::ReadData(const std::string& data)
{
this->RawReadBuffer += data;
for (;;) {
auto needle = this->RawReadBuffer.find('\n');
if (needle == std::string::npos) {
return;
}
std::string line = this->RawReadBuffer.substr(0, needle);
const auto ls = line.size();
if (ls > 1 && line.at(ls - 1) == '\r')
line.erase(ls - 1, 1);
this->RawReadBuffer.erase(this->RawReadBuffer.begin(),
this->RawReadBuffer.begin() +
static_cast<long>(needle) + 1);
if (line == kSTART_MAGIC) {
this->RequestBuffer.clear();
continue;
}
if (line == kEND_MAGIC) {
this->Server->QueueRequest(this->RequestBuffer);
this->RequestBuffer.clear();
} else {
this->RequestBuffer += line;
this->RequestBuffer += "\n";
}
}
}
void cmServerConnection::HandleEof()
{
this->TearDown();
}
void cmServerConnection::WriteData(const std::string& data)
{
assert(this->WriteStream);
auto ds = data.size();
write_req_t* req = new write_req_t;
req->req.data = this;
req->buf = uv_buf_init(new char[ds], static_cast<unsigned int>(ds));
memcpy(req->buf.base, data.c_str(), ds);
uv_write(reinterpret_cast<uv_write_t*>(req),
static_cast<uv_stream_t*>(this->WriteStream), &req->buf, 1,
on_write);
}
void cmServerConnection::ProcessNextRequest()
{
Server->PopOne();
}
void cmServerConnection::SendGreetings()
{
Server->PrintHello();
}
bool cmServerStdIoConnection::DoSetup(std::string* errorMessage)
{
(void)(errorMessage);
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);
Input.tty.data = this;
this->ReadStream = 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);
Output.tty.data = this;
this->WriteStream = 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);
Input.pipe.data = this;
this->ReadStream = reinterpret_cast<uv_stream_t*>(&this->Input.pipe);
uv_pipe_init(this->Loop(), &this->Output.pipe, 0);
uv_pipe_open(&this->Output.pipe, 1);
Output.pipe.data = this;
this->WriteStream = reinterpret_cast<uv_stream_t*>(&this->Output.pipe);
}
SendGreetings();
uv_read_start(this->ReadStream, on_alloc_buffer, on_read);
return true;
}
void cmServerStdIoConnection::TearDown()
{
uv_close(reinterpret_cast<uv_handle_t*>(this->ReadStream), nullptr);
this->ReadStream = nullptr;
uv_close(reinterpret_cast<uv_handle_t*>(this->WriteStream), nullptr);
this->WriteStream = nullptr;
}
cmServerPipeConnection::cmServerPipeConnection(const std::string& name)
: PipeName(name)
{
this->ServerPipe.data = nullptr;
this->ClientPipe.data = nullptr;
}
bool cmServerPipeConnection::DoSetup(std::string* errorMessage)
{
uv_pipe_init(this->Loop(), &this->ServerPipe, 0);
this->ServerPipe.data = this;
int r;
if ((r = uv_pipe_bind(&this->ServerPipe, this->PipeName.c_str())) != 0) {
*errorMessage = std::string("Internal Error with ") + this->PipeName +
": " + uv_err_name(r);
return false;
}
auto serverStream = reinterpret_cast<uv_stream_t*>(&this->ServerPipe);
serverStream->data = this;
if ((r = uv_listen(serverStream, 1, on_new_connection)) != 0) {
*errorMessage = std::string("Internal Error with ") + this->PipeName +
": " + uv_err_name(r);
return false;
}
return true;
}
void cmServerPipeConnection::TearDown()
{
if (this->WriteStream->data) {
uv_close(reinterpret_cast<uv_handle_t*>(this->WriteStream), nullptr);
this->WriteStream->data = nullptr;
}
uv_close(reinterpret_cast<uv_handle_t*>(&this->ServerPipe), nullptr);
this->WriteStream = nullptr;
this->ReadStream = nullptr;
}
void cmServerPipeConnection::Connect(uv_stream_t* server)
{
if (this->ClientPipe.data == this) {
// Accept and close all pipes but the first:
uv_pipe_t rejectPipe;
uv_pipe_init(this->Loop(), &rejectPipe, 0);
auto rejecter = reinterpret_cast<uv_stream_t*>(&rejectPipe);
uv_accept(server, rejecter);
uv_close(reinterpret_cast<uv_handle_t*>(rejecter), nullptr);
return;
}
uv_pipe_init(this->Loop(), &this->ClientPipe, 0);
this->ClientPipe.data = this;
auto client = reinterpret_cast<uv_stream_t*>(&this->ClientPipe);
if (uv_accept(server, client) != 0) {
uv_close(reinterpret_cast<uv_handle_t*>(client), nullptr);
return;
}
this->ReadStream = client;
this->WriteStream = client;
uv_read_start(this->ReadStream, on_alloc_buffer, on_read);
this->SendGreetings();
}
/*============================================================================
CMake - Cross Platform Makefile Generator
Copyright 2015 Stephen Kelly <steveire@gmail.com>
Copyright 2016 Tobias Hunger <tobias.hunger@qt.io>