forked from qt-creator/qt-creator
Apparently there is a limit to about 100 characters or so on some versions of Unix (e.g. Darwin), and there is also the tendency to point TMPDIR into places very far from '/' (e.g. Darwin), which can result in the local socket path getting trunkated. So make sure to put the local socket into /tmp on Unix. That works on Linux and on Darwin. Change-Id: I40bfaf932c5013cf72addb5621360e97c9583daa Reviewed-by: Alexandru Croitor <alexandru.croitor@qt.io>
493 lines
18 KiB
C++
493 lines
18 KiB
C++
/****************************************************************************
|
|
**
|
|
** 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/icore.h>
|
|
#include <coreplugin/reaper.h>
|
|
|
|
#include <utils/algorithm.h>
|
|
#include <utils/qtcassert.h>
|
|
#include <utils/qtcprocess.h>
|
|
#include <utils/temporarydirectory.h>
|
|
|
|
#include <QByteArray>
|
|
#include <QCryptographicHash>
|
|
#include <QFile>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QLocalSocket>
|
|
#include <QUuid>
|
|
|
|
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";
|
|
|
|
Q_LOGGING_CATEGORY(cmakeServerMode, "qtc.cmake.serverMode");
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Helpers:
|
|
// ----------------------------------------------------------------------
|
|
|
|
bool isValid(const QVariant &v)
|
|
{
|
|
if (v.isNull())
|
|
return false;
|
|
if (v.type() == QVariant::String) // Workaround signals sending an empty string cookie
|
|
return !v.toString().isEmpty();
|
|
return true;
|
|
}
|
|
|
|
// --------------------------------------------------------------------
|
|
// 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),
|
|
#if defined(Q_OS_UNIX)
|
|
// Some unixes (e.g. Darwin) limit the length of a local socket to about 100 char (or less).
|
|
// Since some unixes (e.g. Darwin) also point TMPDIR to /really/long/paths we need to create
|
|
// our own socket in a place closer to '/'.
|
|
m_socketDir("/tmp/cmake-"),
|
|
#endif
|
|
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());
|
|
|
|
#if defined(Q_OS_UNIX)
|
|
m_socketName = m_socketDir.path() + "/socket";
|
|
#else
|
|
m_socketName = QString::fromLatin1("\\\\.\\pipe\\") + QUuid::createUuid().toString();
|
|
#endif
|
|
|
|
const QStringList args = QStringList({ "-E", "server", "--pipe=" + m_socketName });
|
|
|
|
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");
|
|
|
|
qCInfo(cmakeServerMode)
|
|
<< "Preparing cmake:" << cmakeExecutable.toString() << argumentString
|
|
<< "in" << m_buildDirectory.toString();
|
|
m_cmakeProcess->setCommand(cmakeExecutable.toString(), argumentString);
|
|
|
|
// Delay start:
|
|
QTimer::singleShot(0, this, [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->abort();
|
|
delete(m_cmakeSocket);
|
|
}
|
|
m_cmakeSocket = nullptr;
|
|
Core::Reaper::reap(m_cmakeProcess.release());
|
|
|
|
qCDebug(cmakeServerMode) << "Server-Mode closed.";
|
|
}
|
|
|
|
void ServerMode::sendRequest(const QString &type, const QVariantMap &extra, const QVariant &cookie)
|
|
{
|
|
QTC_ASSERT(m_cmakeSocket, return);
|
|
++m_requestCounter;
|
|
|
|
qCInfo(cmakeServerMode) << "Sending Request" << type << "(" << cookie << ")";
|
|
|
|
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(QJsonDocument::Compact) + END_MAGIC;
|
|
qCDebug(cmakeServerMode) << ">>>" << rawData;
|
|
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...
|
|
|
|
static int counter = 0;
|
|
++counter;
|
|
|
|
if (counter > 50) {
|
|
counter = 0;
|
|
m_cmakeProcess->disconnect();
|
|
qCInfo(cmakeServerMode) << "Timeout waiting for pipe" << m_socketName;
|
|
reportError(tr("Running \"%1\" failed: Timeout waiting for pipe \"%2\".")
|
|
.arg(m_cmakeExecutable.toUserOutput())
|
|
.arg(m_socketName));
|
|
|
|
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, [this, socket]() {
|
|
reportError(socket->errorString());
|
|
m_cmakeSocket = nullptr;
|
|
socket->disconnect();
|
|
socket->deleteLater();
|
|
});
|
|
connect(socket, &QLocalSocket::connected, this, [this, socket]() { m_cmakeSocket = socket; });
|
|
connect(socket, &QLocalSocket::disconnected, this, [this, socket]() {
|
|
if (m_cmakeSocket)
|
|
emit disconnected();
|
|
m_cmakeSocket = nullptr;
|
|
socket->disconnect();
|
|
socket->deleteLater();
|
|
});
|
|
|
|
socket->connectToServer(m_socketName);
|
|
m_connectionTimer.start();
|
|
}
|
|
|
|
void ServerMode::handleCMakeFinished(int code, QProcess::ExitStatus status)
|
|
{
|
|
qCInfo(cmakeServerMode) << "CMake has finished" << code << 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;
|
|
}
|
|
|
|
if (!HostOsInfo::isWindowsHost())
|
|
QFile::remove(m_socketName);
|
|
|
|
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)
|
|
{
|
|
qCDebug(cmakeServerMode) << "<<<" << 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") {
|
|
qCInfo(cmakeServerMode) << "Got \"hello\" message.";
|
|
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);
|
|
qCInfo(cmakeServerMode) << "Got \"reply\" message." << replyTo << "(" << cookie << ")";
|
|
|
|
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);
|
|
qCInfo(cmakeServerMode) << "Got \"error\" message." << replyTo << "(" << cookie << ")";
|
|
|
|
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->disconnect();
|
|
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);
|
|
qCInfo(cmakeServerMode) << "Got \"message\" message." << replyTo << "(" << cookie << ")";
|
|
|
|
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);
|
|
qCInfo(cmakeServerMode) << "Got \"progress\" message." << replyTo << "(" << cookie << ")";
|
|
|
|
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 QString cookie = data.value(COOKIE_KEY).toString();
|
|
const QString name = data.value(NAME_KEY).toString();
|
|
qCInfo(cmakeServerMode) << "Got \"signal\" message." << name << replyTo << "(" << cookie << ")";
|
|
|
|
if (name.isEmpty()) {
|
|
reportError(tr("Received a signal without a name."));
|
|
return;
|
|
}
|
|
if (!replyTo.isEmpty() || isValid(cookie)) {
|
|
reportError(tr("Received a signal in reply to a request."));
|
|
return;
|
|
}
|
|
|
|
emit cmakeSignal(name, data);
|
|
return;
|
|
}
|
|
reportError("Got a message of an unknown type.");
|
|
}
|
|
|
|
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)
|
|
{
|
|
qCWarning(cmakeServerMode) << "Report Error:" << msg;
|
|
emit message(msg);
|
|
emit errorOccured(msg);
|
|
}
|
|
|
|
} // namespace Internal
|
|
} // namespace CMakeProjectManager
|