From 4c1a7515c9ac544fed85e0dfa78d354004217ece Mon Sep 17 00:00:00 2001 From: Tobias Hunger Date: Mon, 17 Oct 2016 14:35:07 +0200 Subject: [PATCH] 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 --- .../cmakeprojectmanager.pro | 2 + .../cmakeprojectmanager.qbs | 2 + .../cmakeprojectmanager/servermode.cpp | 447 ++++++++++++++++++ src/plugins/cmakeprojectmanager/servermode.h | 117 +++++ 4 files changed, 568 insertions(+) create mode 100644 src/plugins/cmakeprojectmanager/servermode.cpp create mode 100644 src/plugins/cmakeprojectmanager/servermode.h diff --git a/src/plugins/cmakeprojectmanager/cmakeprojectmanager.pro b/src/plugins/cmakeprojectmanager/cmakeprojectmanager.pro index 263efb955d7..678cdbf1bc0 100644 --- a/src/plugins/cmakeprojectmanager/cmakeprojectmanager.pro +++ b/src/plugins/cmakeprojectmanager/cmakeprojectmanager.pro @@ -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 diff --git a/src/plugins/cmakeprojectmanager/cmakeprojectmanager.qbs b/src/plugins/cmakeprojectmanager/cmakeprojectmanager.qbs index 92a55350c15..3bef712ddc2 100644 --- a/src/plugins/cmakeprojectmanager/cmakeprojectmanager.qbs +++ b/src/plugins/cmakeprojectmanager/cmakeprojectmanager.qbs @@ -74,6 +74,8 @@ QtcPlugin { "configmodel.h", "configmodelitemdelegate.cpp", "configmodelitemdelegate.h", + "servermode.cpp", + "servermode.h", "tealeafreader.cpp", "tealeafreader.h" ] diff --git a/src/plugins/cmakeprojectmanager/servermode.cpp b/src/plugins/cmakeprojectmanager/servermode.cpp new file mode 100644 index 00000000000..ee51aa7adfc --- /dev/null +++ b/src/plugins/cmakeprojectmanager/servermode.cpp @@ -0,0 +1,447 @@ +/**************************************************************************** +** +** 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 + +#include +#include +#include + +#include +#include +#include +#include + +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(&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(&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 diff --git a/src/plugins/cmakeprojectmanager/servermode.h b/src/plugins/cmakeprojectmanager/servermode.h new file mode 100644 index 00000000000..bc06a568435 --- /dev/null +++ b/src/plugins/cmakeprojectmanager/servermode.h @@ -0,0 +1,117 @@ +/**************************************************************************** +** +** 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 + +#include +#include +#include + +#include + +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 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 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