You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
571 lines
15 KiB
571 lines
15 KiB
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
|
file Copyright.txt or https://cmake.org/licensing for details. */
|
|
#include "cmServer.h"
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <csignal>
|
|
#include <cstdint>
|
|
#include <iostream>
|
|
#include <mutex>
|
|
#include <utility>
|
|
|
|
#include <cm/memory>
|
|
#include <cm/shared_mutex>
|
|
|
|
#include <cm3p/json/reader.h>
|
|
#include <cm3p/json/writer.h>
|
|
|
|
#include "cmsys/FStream.hxx"
|
|
|
|
#include "cmConnection.h"
|
|
#include "cmFileMonitor.h"
|
|
#include "cmJsonObjectDictionary.h"
|
|
#include "cmServerDictionary.h"
|
|
#include "cmServerProtocol.h"
|
|
#include "cmSystemTools.h"
|
|
#include "cmake.h"
|
|
|
|
void on_signal(uv_signal_t* signal, int signum)
|
|
{
|
|
auto conn = static_cast<cmServerBase*>(signal->data);
|
|
conn->OnSignal(signum);
|
|
}
|
|
|
|
static void on_walk_to_shutdown(uv_handle_t* handle, void* arg)
|
|
{
|
|
(void)arg;
|
|
assert(uv_is_closing(handle));
|
|
if (!uv_is_closing(handle)) {
|
|
uv_close(handle, &cmEventBasedConnection::on_close);
|
|
}
|
|
}
|
|
|
|
class cmServer::DebugInfo
|
|
{
|
|
public:
|
|
DebugInfo()
|
|
: StartTime(uv_hrtime())
|
|
{
|
|
}
|
|
|
|
bool PrintStatistics = false;
|
|
|
|
std::string OutputFile;
|
|
uint64_t StartTime;
|
|
};
|
|
|
|
cmServer::cmServer(cmConnection* conn, bool supportExperimental)
|
|
: cmServerBase(conn)
|
|
, SupportExperimental(supportExperimental)
|
|
{
|
|
// Register supported protocols:
|
|
this->RegisterProtocol(cm::make_unique<cmServerProtocol1>());
|
|
}
|
|
|
|
cmServer::~cmServer()
|
|
{
|
|
Close();
|
|
}
|
|
|
|
void cmServer::ProcessRequest(cmConnection* connection,
|
|
const std::string& input)
|
|
{
|
|
Json::Reader reader;
|
|
Json::Value value;
|
|
if (!reader.parse(input, value)) {
|
|
this->WriteParseError(connection, "Failed to parse JSON input.");
|
|
return;
|
|
}
|
|
|
|
std::unique_ptr<DebugInfo> debug;
|
|
Json::Value debugValue = value["debug"];
|
|
if (!debugValue.isNull()) {
|
|
debug = cm::make_unique<DebugInfo>();
|
|
debug->OutputFile = debugValue["dumpToFile"].asString();
|
|
debug->PrintStatistics = debugValue["showStats"].asBool();
|
|
}
|
|
|
|
const cmServerRequest request(this, connection, value[kTYPE_KEY].asString(),
|
|
value[kCOOKIE_KEY].asString(), value);
|
|
|
|
if (request.Type.empty()) {
|
|
cmServerResponse response(request);
|
|
response.SetError("No type given in request.");
|
|
this->WriteResponse(connection, response, nullptr);
|
|
return;
|
|
}
|
|
|
|
cmSystemTools::SetMessageCallback(
|
|
[&request](const std::string& msg, const char* title) {
|
|
reportMessage(msg, title, request);
|
|
});
|
|
|
|
if (this->Protocol) {
|
|
this->Protocol->CMakeInstance()->SetProgressCallback(
|
|
[&request](const std::string& msg, float prog) {
|
|
reportProgress(msg, prog, request);
|
|
});
|
|
this->WriteResponse(connection, this->Protocol->Process(request),
|
|
debug.get());
|
|
} else {
|
|
this->WriteResponse(connection, this->SetProtocolVersion(request),
|
|
debug.get());
|
|
}
|
|
}
|
|
|
|
void cmServer::RegisterProtocol(std::unique_ptr<cmServerProtocol> protocol)
|
|
{
|
|
if (protocol->IsExperimental() && !this->SupportExperimental) {
|
|
protocol.reset();
|
|
return;
|
|
}
|
|
auto version = protocol->ProtocolVersion();
|
|
assert(version.first >= 0);
|
|
assert(version.second >= 0);
|
|
auto it = std::find_if(
|
|
this->SupportedProtocols.begin(), this->SupportedProtocols.end(),
|
|
[version](const std::unique_ptr<cmServerProtocol>& p) {
|
|
return p->ProtocolVersion() == version;
|
|
});
|
|
if (it == this->SupportedProtocols.end()) {
|
|
this->SupportedProtocols.push_back(std::move(protocol));
|
|
}
|
|
}
|
|
|
|
void cmServer::PrintHello(cmConnection* connection) const
|
|
{
|
|
Json::Value hello = Json::objectValue;
|
|
hello[kTYPE_KEY] = "hello";
|
|
|
|
Json::Value& protocolVersions = hello[kSUPPORTED_PROTOCOL_VERSIONS] =
|
|
Json::arrayValue;
|
|
|
|
for (auto const& proto : this->SupportedProtocols) {
|
|
auto version = proto->ProtocolVersion();
|
|
Json::Value tmp = Json::objectValue;
|
|
tmp[kMAJOR_KEY] = version.first;
|
|
tmp[kMINOR_KEY] = version.second;
|
|
if (proto->IsExperimental()) {
|
|
tmp[kIS_EXPERIMENTAL_KEY] = true;
|
|
}
|
|
protocolVersions.append(tmp);
|
|
}
|
|
|
|
this->WriteJsonObject(connection, hello, nullptr);
|
|
}
|
|
|
|
void cmServer::reportProgress(const std::string& msg, float progress,
|
|
const cmServerRequest& request)
|
|
{
|
|
if (progress < 0.0f || progress > 1.0f) {
|
|
request.ReportMessage(msg, "");
|
|
} else {
|
|
request.ReportProgress(0, static_cast<int>(progress * 1000), 1000, msg);
|
|
}
|
|
}
|
|
|
|
void cmServer::reportMessage(const std::string& msg, const char* title,
|
|
const cmServerRequest& request)
|
|
{
|
|
std::string titleString;
|
|
if (title) {
|
|
titleString = title;
|
|
}
|
|
request.ReportMessage(msg, titleString);
|
|
}
|
|
|
|
cmServerResponse cmServer::SetProtocolVersion(const cmServerRequest& request)
|
|
{
|
|
if (request.Type != kHANDSHAKE_TYPE) {
|
|
return request.ReportError("Waiting for type \"" + kHANDSHAKE_TYPE +
|
|
"\".");
|
|
}
|
|
|
|
Json::Value requestedProtocolVersion = request.Data[kPROTOCOL_VERSION_KEY];
|
|
if (requestedProtocolVersion.isNull()) {
|
|
return request.ReportError("\"" + kPROTOCOL_VERSION_KEY +
|
|
"\" is required for \"" + kHANDSHAKE_TYPE +
|
|
"\".");
|
|
}
|
|
|
|
if (!requestedProtocolVersion.isObject()) {
|
|
return request.ReportError("\"" + kPROTOCOL_VERSION_KEY +
|
|
"\" must be a JSON object.");
|
|
}
|
|
|
|
Json::Value majorValue = requestedProtocolVersion[kMAJOR_KEY];
|
|
if (!majorValue.isInt()) {
|
|
return request.ReportError("\"" + kMAJOR_KEY +
|
|
"\" must be set and an integer.");
|
|
}
|
|
|
|
Json::Value minorValue = requestedProtocolVersion[kMINOR_KEY];
|
|
if (!minorValue.isNull() && !minorValue.isInt()) {
|
|
return request.ReportError("\"" + kMINOR_KEY +
|
|
"\" 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("\"" + kMAJOR_KEY + "\" must be >= 0.");
|
|
}
|
|
if (!minorValue.isNull() && minor < 0) {
|
|
return request.ReportError("\"" + kMINOR_KEY +
|
|
"\" must be >= 0 when set.");
|
|
}
|
|
|
|
this->Protocol =
|
|
cmServer::FindMatchingProtocol(this->SupportedProtocols, major, minor);
|
|
if (!this->Protocol) {
|
|
return request.ReportError("Protocol version not supported.");
|
|
}
|
|
|
|
std::string errorMessage;
|
|
if (!this->Protocol->Activate(this, request, &errorMessage)) {
|
|
this->Protocol = nullptr;
|
|
return request.ReportError("Failed to activate protocol version: " +
|
|
errorMessage);
|
|
}
|
|
return request.Reply(Json::objectValue);
|
|
}
|
|
|
|
bool cmServer::Serve(std::string* errorMessage)
|
|
{
|
|
if (this->SupportedProtocols.empty()) {
|
|
*errorMessage =
|
|
"No protocol versions defined. Maybe you need --experimental?";
|
|
return false;
|
|
}
|
|
assert(!this->Protocol);
|
|
|
|
return cmServerBase::Serve(errorMessage);
|
|
}
|
|
|
|
cmFileMonitor* cmServer::FileMonitor() const
|
|
{
|
|
return fileMonitor.get();
|
|
}
|
|
|
|
void cmServer::WriteJsonObject(const Json::Value& jsonValue,
|
|
const DebugInfo* debug) const
|
|
{
|
|
cm::shared_lock<cm::shared_mutex> lock(ConnectionsMutex);
|
|
for (auto& connection : this->Connections) {
|
|
WriteJsonObject(connection.get(), jsonValue, debug);
|
|
}
|
|
}
|
|
|
|
void cmServer::WriteJsonObject(cmConnection* connection,
|
|
const Json::Value& jsonValue,
|
|
const DebugInfo* debug) const
|
|
{
|
|
Json::FastWriter writer;
|
|
|
|
auto beforeJson = uv_hrtime();
|
|
std::string result = writer.write(jsonValue);
|
|
|
|
if (debug) {
|
|
Json::Value copy = jsonValue;
|
|
if (debug->PrintStatistics) {
|
|
Json::Value stats = Json::objectValue;
|
|
auto endTime = uv_hrtime();
|
|
|
|
stats["jsonSerialization"] = double(endTime - beforeJson) / 1000000.0;
|
|
stats["totalTime"] = double(endTime - debug->StartTime) / 1000000.0;
|
|
stats["size"] = static_cast<int>(result.size());
|
|
if (!debug->OutputFile.empty()) {
|
|
stats["dumpFile"] = debug->OutputFile;
|
|
}
|
|
|
|
copy["zzzDebug"] = stats;
|
|
|
|
result = writer.write(copy); // Update result to include debug info
|
|
}
|
|
|
|
if (!debug->OutputFile.empty()) {
|
|
cmsys::ofstream myfile(debug->OutputFile.c_str());
|
|
myfile << result;
|
|
}
|
|
}
|
|
|
|
connection->WriteData(result);
|
|
}
|
|
|
|
cmServerProtocol* cmServer::FindMatchingProtocol(
|
|
const std::vector<std::unique_ptr<cmServerProtocol>>& protocols, int major,
|
|
int minor)
|
|
{
|
|
cmServerProtocol* bestMatch = nullptr;
|
|
for (const auto& protocol : protocols) {
|
|
auto version = protocol->ProtocolVersion();
|
|
if (major != version.first) {
|
|
continue;
|
|
}
|
|
if (minor == version.second) {
|
|
return protocol.get();
|
|
}
|
|
if (!bestMatch || bestMatch->ProtocolVersion().second < version.second) {
|
|
bestMatch = protocol.get();
|
|
}
|
|
}
|
|
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] = kPROGRESS_TYPE;
|
|
obj[kREPLY_TO_KEY] = request.Type;
|
|
obj[kCOOKIE_KEY] = request.Cookie;
|
|
obj[kPROGRESS_MESSAGE_KEY] = message;
|
|
obj[kPROGRESS_MINIMUM_KEY] = min;
|
|
obj[kPROGRESS_MAXIMUM_KEY] = max;
|
|
obj[kPROGRESS_CURRENT_KEY] = current;
|
|
|
|
this->WriteJsonObject(request.Connection, obj, nullptr);
|
|
}
|
|
|
|
void cmServer::WriteMessage(const cmServerRequest& request,
|
|
const std::string& message,
|
|
const std::string& title) const
|
|
{
|
|
if (message.empty()) {
|
|
return;
|
|
}
|
|
|
|
Json::Value obj = Json::objectValue;
|
|
obj[kTYPE_KEY] = kMESSAGE_TYPE;
|
|
obj[kREPLY_TO_KEY] = request.Type;
|
|
obj[kCOOKIE_KEY] = request.Cookie;
|
|
obj[kMESSAGE_KEY] = message;
|
|
if (!title.empty()) {
|
|
obj[kTITLE_KEY] = title;
|
|
}
|
|
|
|
WriteJsonObject(request.Connection, obj, nullptr);
|
|
}
|
|
|
|
void cmServer::WriteParseError(cmConnection* connection,
|
|
const std::string& message) const
|
|
{
|
|
Json::Value obj = Json::objectValue;
|
|
obj[kTYPE_KEY] = kERROR_TYPE;
|
|
obj[kERROR_MESSAGE_KEY] = message;
|
|
obj[kREPLY_TO_KEY] = "";
|
|
obj[kCOOKIE_KEY] = "";
|
|
|
|
this->WriteJsonObject(connection, obj, nullptr);
|
|
}
|
|
|
|
void cmServer::WriteSignal(const std::string& name,
|
|
const Json::Value& data) const
|
|
{
|
|
assert(data.isObject());
|
|
Json::Value obj = data;
|
|
obj[kTYPE_KEY] = kSIGNAL_TYPE;
|
|
obj[kREPLY_TO_KEY] = "";
|
|
obj[kCOOKIE_KEY] = "";
|
|
obj[kNAME_KEY] = name;
|
|
|
|
WriteJsonObject(obj, nullptr);
|
|
}
|
|
|
|
void cmServer::WriteResponse(cmConnection* connection,
|
|
const cmServerResponse& response,
|
|
const DebugInfo* debug) const
|
|
{
|
|
assert(response.IsComplete());
|
|
|
|
Json::Value obj = response.Data();
|
|
obj[kCOOKIE_KEY] = response.Cookie;
|
|
obj[kTYPE_KEY] = response.IsError() ? kERROR_TYPE : kREPLY_TYPE;
|
|
obj[kREPLY_TO_KEY] = response.Type;
|
|
if (response.IsError()) {
|
|
obj[kERROR_MESSAGE_KEY] = response.ErrorMessage();
|
|
}
|
|
|
|
this->WriteJsonObject(connection, obj, debug);
|
|
}
|
|
|
|
void cmServer::OnConnected(cmConnection* connection)
|
|
{
|
|
PrintHello(connection);
|
|
}
|
|
|
|
void cmServer::OnServeStart()
|
|
{
|
|
cmServerBase::OnServeStart();
|
|
fileMonitor = std::make_shared<cmFileMonitor>(GetLoop());
|
|
}
|
|
|
|
void cmServer::StartShutDown()
|
|
{
|
|
if (fileMonitor) {
|
|
fileMonitor->StopMonitoring();
|
|
fileMonitor.reset();
|
|
}
|
|
cmServerBase::StartShutDown();
|
|
}
|
|
|
|
static void __start_thread(void* arg)
|
|
{
|
|
auto server = static_cast<cmServerBase*>(arg);
|
|
std::string error;
|
|
bool success = server->Serve(&error);
|
|
if (!success || !error.empty()) {
|
|
std::cerr << "Error during serve: " << error << std::endl;
|
|
}
|
|
}
|
|
|
|
bool cmServerBase::StartServeThread()
|
|
{
|
|
ServeThreadRunning = true;
|
|
uv_thread_create(&ServeThread, __start_thread, this);
|
|
return true;
|
|
}
|
|
|
|
static void __shutdownThread(uv_async_t* arg)
|
|
{
|
|
auto server = static_cast<cmServerBase*>(arg->data);
|
|
server->StartShutDown();
|
|
}
|
|
|
|
bool cmServerBase::Serve(std::string* errorMessage)
|
|
{
|
|
#ifndef NDEBUG
|
|
uv_thread_t blank_thread_t = {};
|
|
assert(uv_thread_equal(&blank_thread_t, &ServeThreadId));
|
|
ServeThreadId = uv_thread_self();
|
|
#endif
|
|
|
|
errorMessage->clear();
|
|
|
|
ShutdownSignal.init(Loop, __shutdownThread, this);
|
|
|
|
SIGINTHandler.init(Loop, this);
|
|
SIGHUPHandler.init(Loop, this);
|
|
|
|
SIGINTHandler.start(&on_signal, SIGINT);
|
|
SIGHUPHandler.start(&on_signal, SIGHUP);
|
|
|
|
OnServeStart();
|
|
|
|
{
|
|
cm::shared_lock<cm::shared_mutex> lock(ConnectionsMutex);
|
|
for (auto& connection : Connections) {
|
|
if (!connection->OnServeStart(errorMessage)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (uv_run(&Loop, UV_RUN_DEFAULT) != 0) {
|
|
// It is important we don't ever let the event loop exit with open handles
|
|
// at best this is a memory leak, but it can also introduce race conditions
|
|
// which can hang the program.
|
|
assert(false && "Event loop stopped in unclean state.");
|
|
|
|
*errorMessage = "Internal Error: Event loop stopped in unclean state.";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void cmServerBase::OnConnected(cmConnection*)
|
|
{
|
|
}
|
|
|
|
void cmServerBase::OnServeStart()
|
|
{
|
|
}
|
|
|
|
void cmServerBase::StartShutDown()
|
|
{
|
|
ShutdownSignal.reset();
|
|
SIGINTHandler.reset();
|
|
SIGHUPHandler.reset();
|
|
|
|
{
|
|
std::unique_lock<cm::shared_mutex> lock(ConnectionsMutex);
|
|
for (auto& connection : Connections) {
|
|
connection->OnConnectionShuttingDown();
|
|
}
|
|
Connections.clear();
|
|
}
|
|
|
|
uv_walk(&Loop, on_walk_to_shutdown, nullptr);
|
|
}
|
|
|
|
bool cmServerBase::OnSignal(int signum)
|
|
{
|
|
(void)signum;
|
|
StartShutDown();
|
|
return true;
|
|
}
|
|
|
|
cmServerBase::cmServerBase(cmConnection* connection)
|
|
{
|
|
auto err = uv_loop_init(&Loop);
|
|
(void)err;
|
|
Loop.data = this;
|
|
assert(err == 0);
|
|
|
|
AddNewConnection(connection);
|
|
}
|
|
|
|
void cmServerBase::Close()
|
|
{
|
|
if (Loop.data) {
|
|
if (ServeThreadRunning) {
|
|
this->ShutdownSignal.send();
|
|
uv_thread_join(&ServeThread);
|
|
}
|
|
|
|
uv_loop_close(&Loop);
|
|
Loop.data = nullptr;
|
|
}
|
|
}
|
|
cmServerBase::~cmServerBase()
|
|
{
|
|
Close();
|
|
}
|
|
|
|
void cmServerBase::AddNewConnection(cmConnection* ownedConnection)
|
|
{
|
|
{
|
|
std::unique_lock<cm::shared_mutex> lock(ConnectionsMutex);
|
|
Connections.emplace_back(ownedConnection);
|
|
}
|
|
ownedConnection->SetServer(this);
|
|
}
|
|
|
|
uv_loop_t* cmServerBase::GetLoop()
|
|
{
|
|
return &Loop;
|
|
}
|
|
|
|
void cmServerBase::OnDisconnect(cmConnection* pConnection)
|
|
{
|
|
auto pred = [pConnection](const std::unique_ptr<cmConnection>& m) {
|
|
return m.get() == pConnection;
|
|
};
|
|
{
|
|
std::unique_lock<cm::shared_mutex> lock(ConnectionsMutex);
|
|
Connections.erase(
|
|
std::remove_if(Connections.begin(), Connections.end(), pred),
|
|
Connections.end());
|
|
}
|
|
|
|
if (Connections.empty()) {
|
|
this->ShutdownSignal.send();
|
|
}
|
|
}
|