Commit 4c1a7515 authored by Tobias Hunger's avatar Tobias Hunger
Browse files

CMake: Implement helper to talk to CMake server-mode



Implement a helper class that can be used to talk to CMake's
server-mode.

Change-Id: I1df4af665991a5e0a3acb301ffd28008dd4fe86f
Reviewed-by: Tim Jenssen's avatarTim Jenssen <tim.jenssen@qt.io>
parent 9a980adf
......@@ -30,6 +30,7 @@ HEADERS = builddirmanager.h \
cmakeautocompleter.h \
configmodel.h \
configmodelitemdelegate.h \
servermode.h \
tealeafreader.h
SOURCES = builddirmanager.cpp \
......@@ -58,6 +59,7 @@ SOURCES = builddirmanager.cpp \
cmakeautocompleter.cpp \
configmodel.cpp \
configmodelitemdelegate.cpp \
servermode.cpp \
tealeafreader.cpp
RESOURCES += cmakeproject.qrc
......@@ -74,6 +74,8 @@ QtcPlugin {
"configmodel.h",
"configmodelitemdelegate.cpp",
"configmodelitemdelegate.h",
"servermode.cpp",
"servermode.h",
"tealeafreader.cpp",
"tealeafreader.h"
]
......
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "servermode.h"
#include <coreplugin/reaper.h>
#include <utils/algorithm.h>
#include <utils/qtcassert.h>
#include <utils/qtcprocess.h>
#include <QByteArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLocalSocket>
using namespace Utils;
namespace CMakeProjectManager {
namespace Internal {
const char COOKIE_KEY[] = "cookie";
const char IN_REPLY_TO_KEY[] = "inReplyTo";
const char NAME_KEY[] = "name";
const char TYPE_KEY[] = "type";
const char ERROR_TYPE[] = "error";
const char HANDSHAKE_TYPE[] = "handshake";
const char START_MAGIC[] = "\n[== \"CMake Server\" ==[\n";
const char END_MAGIC[] = "\n]== \"CMake Server\" ==]\n";
// ----------------------------------------------------------------------
// Helpers:
// ----------------------------------------------------------------------
QString socketName(const Utils::FileName &buildDirectory)
{
return buildDirectory.toString() + "/socket";
}
// --------------------------------------------------------------------
// ServerMode:
// --------------------------------------------------------------------
ServerMode::ServerMode(const Environment &env,
const FileName &sourceDirectory, const FileName &buildDirectory,
const FileName &cmakeExecutable,
const QString &generator, const QString &extraGenerator,
const QString &platform, const QString &toolset,
bool experimental, int major, int minor,
QObject *parent) :
QObject(parent),
m_sourceDirectory(sourceDirectory), m_buildDirectory(buildDirectory),
m_cmakeExecutable(cmakeExecutable),
m_generator(generator), m_extraGenerator(extraGenerator),
m_platform(platform), m_toolset(toolset),
m_useExperimental(experimental), m_majorProtocol(major), m_minorProtocol(minor)
{
QTC_ASSERT(!m_sourceDirectory.isEmpty() && m_sourceDirectory.exists(), return);
QTC_ASSERT(!m_buildDirectory.isEmpty() && m_buildDirectory.exists(), return);
m_connectionTimer.setInterval(100);
connect(&m_connectionTimer, &QTimer::timeout, this, &ServerMode::connectToServer);
m_cmakeProcess.reset(new QtcProcess);
m_cmakeProcess->setEnvironment(env);
m_cmakeProcess->setWorkingDirectory(buildDirectory.toString());
const QStringList args = QStringList({ "-E", "server", "--pipe=" + socketName(buildDirectory) });
connect(m_cmakeProcess.get(), &QtcProcess::started, this, [this]() { m_connectionTimer.start(); });
connect(m_cmakeProcess.get(),
static_cast<void(QtcProcess::*)(int, QProcess::ExitStatus)>(&QtcProcess::finished),
this, &ServerMode::handleCMakeFinished);
QString argumentString;
QtcProcess::addArgs(&argumentString, args);
if (m_useExperimental)
QtcProcess::addArg(&argumentString, "--experimental");
m_cmakeProcess->setCommand(cmakeExecutable.toString(), argumentString);
// Delay start:
QTimer::singleShot(0, [argumentString, this] {
emit message(tr("Running \"%1 %2\" in %3.")
.arg(m_cmakeExecutable.toUserOutput())
.arg(argumentString)
.arg(m_buildDirectory.toUserOutput()));
m_cmakeProcess->start();
});
}
ServerMode::~ServerMode()
{
if (m_cmakeProcess)
m_cmakeProcess->disconnect();
if (m_cmakeSocket) {
m_cmakeSocket->disconnect();
m_cmakeSocket->disconnectFromServer();
delete(m_cmakeSocket);
}
m_cmakeSocket = nullptr;
Core::Reaper::reap(m_cmakeProcess.release());
}
void ServerMode::sendRequest(const QString &type, const QVariantMap &extra, const QVariant &cookie)
{
QTC_ASSERT(m_cmakeSocket, return);
++m_requestCounter;
QVariantMap data = extra;
data.insert(TYPE_KEY, type);
const QVariant realCookie = cookie.isNull() ? QVariant(m_requestCounter) : cookie;
data.insert(COOKIE_KEY, realCookie);
m_expectedReplies.push_back({ type, realCookie });
QJsonObject object = QJsonObject::fromVariantMap(data);
QJsonDocument document;
document.setObject(object);
const QByteArray rawData = START_MAGIC + document.toJson() + END_MAGIC;
m_cmakeSocket->write(rawData);
m_cmakeSocket->flush();
}
bool ServerMode::isConnected()
{
return m_cmakeSocket && m_isConnected;
}
void ServerMode::connectToServer()
{
QTC_ASSERT(m_cmakeProcess, return);
if (m_cmakeSocket)
return; // We connected in the meantime...
const QString socketString = socketName(m_buildDirectory);
static int counter = 0;
++counter;
if (counter > 50) {
counter = 0;
m_cmakeProcess->disconnect();
reportError(tr("Running \"%1\" failed: Timeout waiting for pipe \"%2\".")
.arg(m_cmakeExecutable.toUserOutput())
.arg(socketString));
Core::Reaper::reap(m_cmakeProcess.release());
emit disconnected();
return;
}
QTC_ASSERT(!m_cmakeSocket, return);
auto socket = new QLocalSocket(m_cmakeProcess.get());
connect(socket, &QLocalSocket::readyRead,
this, &ServerMode::handleRawCMakeServerData);
connect(socket, static_cast<void(QLocalSocket::*)(QLocalSocket::LocalSocketError)>(&QLocalSocket::error),
[this, socket]() {
reportError(socket->errorString());
socket->disconnect();
socket->deleteLater();
});
connect(socket, &QLocalSocket::connected, [this, socket]() { m_cmakeSocket = socket; });
connect(socket, &QLocalSocket::disconnected, [this, socket]() {
m_cmakeSocket = nullptr;
socket->disconnect();
socket->deleteLater();
});
socket->connectToServer(socketString);
m_connectionTimer.start();
}
void ServerMode::handleCMakeFinished(int code, QProcess::ExitStatus status)
{
QString msg;
if (status != QProcess::NormalExit)
msg = tr("CMake process \"%1\" crashed.").arg(m_cmakeExecutable.toUserOutput());
else if (code != 0)
msg = tr("CMake process \"%1\" quit with exit code %2.").arg(m_cmakeExecutable.toUserOutput()).arg(code);
if (!msg.isEmpty()) {
reportError(msg);
} else {
emit message(tr("CMake process \"%1\" quit normally.").arg(m_cmakeExecutable.toUserOutput()));
}
if (m_cmakeSocket) {
m_cmakeSocket->disconnect();
delete m_cmakeSocket;
m_cmakeSocket = nullptr;
}
QFile::remove(socketName(m_buildDirectory));
emit disconnected();
}
void ServerMode::handleRawCMakeServerData()
{
const static QByteArray startNeedle(START_MAGIC);
const static QByteArray endNeedle(END_MAGIC);
if (!m_cmakeSocket) // might happen during shutdown
return;
m_buffer.append(m_cmakeSocket->readAll());
while (true) {
const int startPos = m_buffer.indexOf(startNeedle);
if (startPos >= 0) {
const int afterStartNeedle = startPos + startNeedle.count();
const int endPos = m_buffer.indexOf(endNeedle, afterStartNeedle);
if (endPos > afterStartNeedle) {
// Process JSON, remove junk and JSON-part, continue to loop with shorter buffer
parseBuffer(m_buffer.mid(afterStartNeedle, endPos - afterStartNeedle));
m_buffer.remove(0, endPos + endNeedle.count());
} else {
// Remove junk up to the start needle and break out of the loop
if (startPos > 0)
m_buffer.remove(0, startPos);
break;
}
} else {
// Keep at last startNeedle.count() characters (as that might be a
// partial startNeedle), break out of the loop
if (m_buffer.count() > startNeedle.count())
m_buffer.remove(0, m_buffer.count() - startNeedle.count());
break;
}
}
}
void ServerMode::parseBuffer(const QByteArray &buffer)
{
QJsonDocument document = QJsonDocument::fromJson(buffer);
if (document.isNull()) {
reportError(tr("Failed to parse JSON from CMake server."));
return;
}
QJsonObject rootObject = document.object();
if (rootObject.isEmpty()) {
reportError(tr("JSON data from CMake server was not a JSON object."));
return;
}
parseJson(rootObject.toVariantMap());
}
void ServerMode::parseJson(const QVariantMap &data)
{
QString type = data.value(TYPE_KEY).toString();
if (type == "hello") {
if (m_gotHello) {
reportError(tr("Unexpected hello received from CMake server."));
return;
} else {
handleHello(data);
m_gotHello = true;
return;
}
}
if (!m_gotHello && type != ERROR_TYPE) {
reportError(tr("Unexpected type \"%1\" received while waiting for \"hello\".").arg(type));
return;
}
if (type == "reply") {
if (m_expectedReplies.empty()) {
reportError(tr("Received a reply even though no request is open."));
return;
}
const QString replyTo = data.value(IN_REPLY_TO_KEY).toString();
const QVariant cookie = data.value(COOKIE_KEY);
const auto expected = m_expectedReplies.begin();
if (expected->type != replyTo) {
reportError(tr("Received a reply to a request of type \"%1\", when a request of type \"%2\" was sent.")
.arg(replyTo).arg(expected->type));
return;
}
if (expected->cookie != cookie) {
reportError(tr("Received a reply with cookie \"%1\", when \"%2\" was expected.")
.arg(cookie.toString()).arg(expected->cookie.toString()));
return;
}
m_expectedReplies.erase(expected);
if (replyTo != HANDSHAKE_TYPE)
emit cmakeReply(data, replyTo, cookie);
else {
m_isConnected = true;
emit connected();
}
return;
}
if (type == "error") {
if (m_expectedReplies.empty()) {
reportError(tr("An error was reported even though no request is open."));
return;
}
const QString replyTo = data.value(IN_REPLY_TO_KEY).toString();
const QVariant cookie = data.value(COOKIE_KEY);
const auto expected = m_expectedReplies.begin();
if (expected->type != replyTo) {
reportError(tr("Received an error in response to a request of type \"%1\", when a request of type \"%2\" was sent.")
.arg(replyTo).arg(expected->type));
return;
}
if (expected->cookie != cookie) {
reportError(tr("Received an error with cookie \"%1\", when \"%2\" was expected.")
.arg(cookie.toString()).arg(expected->cookie.toString()));
return;
}
m_expectedReplies.erase(expected);
emit cmakeError(data.value("errorMessage").toString(), replyTo, cookie);
if (replyTo == HANDSHAKE_TYPE) {
Core::Reaper::reap(m_cmakeProcess.release());
m_cmakeSocket->disconnectFromServer();
m_cmakeSocket = nullptr;
emit disconnected();
}
return;
}
if (type == "message") {
const QString replyTo = data.value(IN_REPLY_TO_KEY).toString();
const QVariant cookie = data.value(COOKIE_KEY);
const auto expected = m_expectedReplies.begin();
if (expected->type != replyTo) {
reportError(tr("Received a message in response to a request of type \"%1\", when a request of type \"%2\" was sent.")
.arg(replyTo).arg(expected->type));
return;
}
if (expected->cookie != cookie) {
reportError(tr("Received a message with cookie \"%1\", when \"%2\" was expected.")
.arg(cookie.toString()).arg(expected->cookie.toString()));
return;
}
emit cmakeMessage(data.value("message").toString(), replyTo, cookie);
return;
}
if (type == "progress") {
const QString replyTo = data.value(IN_REPLY_TO_KEY).toString();
const QVariant cookie = data.value(COOKIE_KEY);
const auto expected = m_expectedReplies.begin();
if (expected->type != replyTo) {
reportError(tr("Received a progress report in response to a request of type \"%1\", when a request of type \"%2\" was sent.")
.arg(replyTo).arg(expected->type));
return;
}
if (expected->cookie != cookie) {
reportError(tr("Received a progress report with cookie \"%1\", when \"%2\" was expected.")
.arg(cookie.toString()).arg(expected->cookie.toString()));
return;
}
emit cmakeProgress(data.value("progressMinimum").toInt(),
data.value("progressCurrent").toInt(),
data.value("progressMaximum").toInt(), replyTo, cookie);
return;
}
if (type == "signal") {
const QString replyTo = data.value(IN_REPLY_TO_KEY).toString();
const QVariant cookie = data.value(COOKIE_KEY);
const QString name = data.value(NAME_KEY).toString();
if (name.isEmpty()) {
reportError(tr("Received a signal without a name."));
return;
}
if (!replyTo.isEmpty() || cookie.isValid()) {
reportError(tr("Received a signal in reply to a request."));
return;
}
emit cmakeSignal(name, data);
return;
}
}
void ServerMode::handleHello(const QVariantMap &data)
{
Q_UNUSED(data);
QVariantMap extra;
QVariantMap version;
version.insert("major", m_majorProtocol);
if (m_minorProtocol >= 0)
version.insert("minor", m_minorProtocol);
extra.insert("protocolVersion", version);
extra.insert("sourceDirectory", m_sourceDirectory.toString());
extra.insert("buildDirectory", m_buildDirectory.toString());
extra.insert("generator", m_generator);
if (!m_platform.isEmpty())
extra.insert("platform", m_platform);
if (!m_toolset.isEmpty())
extra.insert("toolset", m_toolset);
if (!m_extraGenerator.isEmpty())
extra.insert("extraGenerator", m_extraGenerator);
if (!m_platform.isEmpty())
extra.insert("platform", m_platform);
if (!m_toolset.isEmpty())
extra.insert("toolset", m_toolset);
sendRequest(HANDSHAKE_TYPE, extra);
}
void ServerMode::reportError(const QString &msg)
{
emit message(msg);
emit errorOccured(msg);
}
} // namespace Internal
} // namespace CMakeProjectManager
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#pragma once
#include <utils/qtcprocess.h>
#include <QTemporaryDir>
#include <QTimer>
#include <QVariantMap>
#include <memory>
QT_FORWARD_DECLARE_CLASS(QLocalSocket);
namespace Utils { class QtcProcess; }
namespace CMakeProjectManager {
namespace Internal {
class ServerMode : public QObject
{
Q_OBJECT
public:
ServerMode(const Utils::Environment &env,
const Utils::FileName &sourceDirectory, const Utils::FileName &buildDirectory,
const Utils::FileName &cmakeExecutable,
const QString &generator, const QString &extraGenerator,
const QString &platform, const QString &toolset,
bool experimental, int major, int minor = -1,
QObject *parent = nullptr);
~ServerMode() final;
void sendRequest(const QString &type, const QVariantMap &extra = QVariantMap(),
const QVariant &cookie = QVariant());
bool isConnected();
signals:
void connected();
void disconnected();
void message(const QString &msg);
void errorOccured(const QString &msg);
// Forward stuff from the server
void cmakeReply(const QVariantMap &data, const QString &inResponseTo, const QVariant &cookie);
void cmakeError(const QString &errorMessage, const QString &inResponseTo, const QVariant &cookie);
void cmakeMessage(const QString &message, const QString &inResponseTo, const QVariant &cookie);
void cmakeProgress(int min, int cur, int max, const QString &inResponseTo, const QVariant &cookie);
void cmakeSignal(const QString &name, const QVariantMap &data);
private:
void connectToServer();
void handleCMakeFinished(int code, QProcess::ExitStatus status);
void handleRawCMakeServerData();
void parseBuffer(const QByteArray &buffer);
void parseJson(const QVariantMap &data);
void handleHello(const QVariantMap &data);
void reportError(const QString &msg);
std::unique_ptr<Utils::QtcProcess> m_cmakeProcess;
QLocalSocket *m_cmakeSocket = nullptr;
QTimer m_connectionTimer;
Utils::FileName m_sourceDirectory;
Utils::FileName m_buildDirectory;
Utils::FileName m_cmakeExecutable;
QByteArray m_buffer;
struct ExpectedReply {
QString type;
QVariant cookie;
};
std::vector<ExpectedReply> m_expectedReplies;
const QString m_generator;
const QString m_extraGenerator;
const QString m_platform;
const QString m_toolset;
const bool m_useExperimental;
bool m_gotHello = false;
bool m_isConnected = false;
const int m_majorProtocol = -1;
const int m_minorProtocol = -1;
int m_requestCounter = 0;
};
} // namespace Internal
} // namespace CMakeProjectManager
Markdown is supported
0% or .