File indexing completed on 2024-05-05 04:39:26

0001 /*
0002     SPDX-FileCopyrightText: 2017 Aleix Pol <aleixpol@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "cmakeserver.h"
0008 #include "cmakeprojectdata.h"
0009 #include "cmakeutils.h"
0010 
0011 #include <interfaces/iruntime.h>
0012 #include <interfaces/iruntimecontroller.h>
0013 #include <interfaces/icore.h>
0014 #include <interfaces/iproject.h>
0015 
0016 #include <QDir>
0017 #include <QJsonDocument>
0018 #include <QJsonObject>
0019 #include <QJsonArray>
0020 #include <QTimer>
0021 #include <QTemporaryFile>
0022 #include "debug.h"
0023 
0024 CMakeServer::CMakeServer(KDevelop::IProject* project)
0025     : QObject()
0026     , m_localSocket(new QLocalSocket(this))
0027 {
0028     QString path;
0029     {
0030         const auto cacheLocation = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
0031         QDir::temp().mkpath(cacheLocation);
0032 
0033         QTemporaryFile file(cacheLocation + QLatin1String("/kdevelopcmake"));
0034         file.open();
0035         file.close();
0036         path = file.fileName();
0037         Q_ASSERT(!path.isEmpty());
0038     }
0039 
0040     m_process.setProcessChannelMode(QProcess::ForwardedChannels);
0041 
0042     connect(&m_process, &QProcess::errorOccurred,
0043             this, [this, path](QProcess::ProcessError error) {
0044         qCWarning(CMAKE) << "cmake server error:" << error << path << m_process.readAllStandardError() << m_process.readAllStandardOutput();
0045     });
0046     connect(&m_process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [](int code){
0047         qCDebug(CMAKE) << "cmake server finished with code" << code;
0048     });
0049     connect(&m_process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &CMakeServer::finished);
0050 
0051     connect(m_localSocket, &QIODevice::readyRead, this, &CMakeServer::processOutput);
0052     connect(m_localSocket, &QLocalSocket::errorOccurred, this,
0053             [this, path](QLocalSocket::LocalSocketError socketError) {
0054                 qCWarning(CMAKE) << "cmake server socket error:" << socketError << path;
0055                 setConnected(false);
0056             });
0057     connect(m_localSocket, &QLocalSocket::connected, this, [this]() { setConnected(true); });
0058 
0059     connect(&m_process, &QProcess::started, this, [this, path](){
0060         //Once the process has started, wait for the file to be created, then connect to it
0061         QTimer::singleShot(1000, this, [this, path]() {
0062             m_localSocket->connectToServer(path, QIODevice::ReadWrite);
0063         });
0064     });
0065     // we're called with the importing project as our parent, so we can fetch configured
0066     // cmake executable (project-specific or kdevelop-wide) rather than the system version.
0067     m_process.setProgram(CMake::currentCMakeExecutable(project).toLocalFile());
0068     m_process.setArguments({QStringLiteral("-E"), QStringLiteral("server"), QStringLiteral("--experimental"), QLatin1String("--pipe=") + path});
0069     KDevelop::ICore::self()->runtimeController()->currentRuntime()->startProcess(&m_process);
0070 }
0071 
0072 CMakeServer::~CMakeServer()
0073 {
0074     m_process.disconnect();
0075     m_process.kill();
0076     m_process.waitForFinished();
0077 }
0078 
0079 void CMakeServer::setConnected(bool conn)
0080 {
0081     if (conn == m_connected)
0082         return;
0083 
0084     m_connected = conn;
0085     if (m_connected)
0086         Q_EMIT connected();
0087     else
0088         Q_EMIT disconnected();
0089 }
0090 
0091 bool CMakeServer::isServerAvailable()
0092 {
0093     return m_localSocket->isOpen();
0094 }
0095 
0096 static QByteArray openTag() { return QByteArrayLiteral("\n[== \"CMake Server\" ==[\n"); }
0097 static QByteArray closeTag() { return QByteArrayLiteral("\n]== \"CMake Server\" ==]\n"); }
0098 
0099 void CMakeServer::sendCommand(const QJsonObject& object)
0100 {
0101     Q_ASSERT(isServerAvailable());
0102 
0103     const QByteArray data = openTag() + QJsonDocument(object).toJson(QJsonDocument::Compact) + closeTag();
0104     auto len = m_localSocket->write(data);
0105 //     qCDebug(CMAKE) << "writing...\n" << QJsonDocument(object).toJson();
0106     Q_ASSERT(len > 0);
0107 }
0108 
0109 void CMakeServer::processOutput()
0110 {
0111     Q_ASSERT(m_localSocket);
0112 
0113     const auto openTag = ::openTag();
0114     const auto closeTag = ::closeTag();
0115 
0116     m_buffer += m_localSocket->readAll();
0117     for(; m_buffer.size() > openTag.size(); ) {
0118 
0119         Q_ASSERT(m_buffer.startsWith(openTag));
0120         const int idx = m_buffer.indexOf(closeTag, openTag.size());
0121         if (idx >= 0) {
0122             emitResponse(m_buffer.mid(openTag.size(), idx - openTag.size()));
0123             m_buffer.remove(0, idx + closeTag.size());
0124         } else {
0125             break;
0126         }
0127     }
0128 }
0129 
0130 void CMakeServer::emitResponse(const QByteArray& data)
0131 {
0132     QJsonParseError error;
0133     auto doc = QJsonDocument::fromJson(data, &error);
0134     if (error.error) {
0135         qCWarning(CMAKE) << "error processing" << error.errorString() << data;
0136     }
0137     Q_ASSERT(doc.isObject());
0138     Q_EMIT response(doc.object());
0139 }
0140 
0141 void CMakeServer::handshake(const KDevelop::Path& source, const KDevelop::Path& build)
0142 {
0143     Q_ASSERT(!source.isEmpty());
0144 
0145     const QString generatorVariable = QStringLiteral("CMAKE_GENERATOR");
0146     const QString homeDirectoryVariable = QStringLiteral("CMAKE_HOME_DIRECTORY");
0147     const QString cacheFileDirectoryVariable = QStringLiteral("CMAKE_CACHEFILE_DIR");
0148     const auto cacheValues = CMake::readCacheValues(KDevelop::Path(build, QStringLiteral("CMakeCache.txt")),
0149                                                     {generatorVariable, homeDirectoryVariable, cacheFileDirectoryVariable});
0150 
0151     QString generator = cacheValues.value(generatorVariable);
0152     if (generator.isEmpty()) {
0153         generator = CMake::defaultGenerator();
0154     }
0155 
0156     // prefer pre-existing source directory, see also: https://gitlab.kitware.com/cmake/cmake/issues/16736
0157     QString sourceDirectory = cacheValues.value(homeDirectoryVariable);
0158     if (sourceDirectory.isEmpty()) {
0159         sourceDirectory = source.toLocalFile();
0160     } else if (QFileInfo(sourceDirectory).canonicalFilePath() != QFileInfo(source.toLocalFile()).canonicalFilePath()) {
0161         qCWarning(CMAKE) << "Build directory is configured for another source directory:"
0162                    << homeDirectoryVariable << sourceDirectory
0163                    << "wanted to open" << source << "in" << build;
0164     }
0165 
0166     // prefer to reuse the exact same build dir path to prevent useless recompilation
0167     // when we open a symlinked project path
0168     QString buildDirectory = cacheValues.value(cacheFileDirectoryVariable);
0169     if (buildDirectory.isEmpty()) {
0170         buildDirectory = build.toLocalFile();
0171     } else if (QFileInfo(buildDirectory).canonicalFilePath() != QFileInfo(build.toLocalFile()).canonicalFilePath()) {
0172         qCWarning(CMAKE) << "Build directory mismatch:"
0173                    << cacheFileDirectoryVariable << buildDirectory
0174                    << "wanted to open" << build;
0175         buildDirectory = build.toLocalFile();
0176     }
0177 
0178     qCDebug(CMAKE) << "Using generator" << generator << "for project"
0179                    << sourceDirectory << "aka" << source
0180                    << "in" << buildDirectory << "aka" << build;
0181 
0182     sendCommand({
0183         {QStringLiteral("cookie"), {}},
0184         {QStringLiteral("type"), QStringLiteral("handshake")},
0185         {QStringLiteral("major"), 1},
0186         {QStringLiteral("protocolVersion"), QJsonObject{{QStringLiteral("major"), 1}} },
0187         {QStringLiteral("sourceDirectory"), sourceDirectory},
0188         {QStringLiteral("buildDirectory"), buildDirectory},
0189         {QStringLiteral("generator"), generator}
0190     });
0191 }
0192 
0193 void CMakeServer::configure(const QStringList& args)
0194 {
0195     sendCommand({
0196         {QStringLiteral("type"), QStringLiteral("configure")},
0197         {QStringLiteral("cacheArguments"), QJsonArray::fromStringList(args)}
0198     });
0199 }
0200 
0201 void CMakeServer::compute()
0202 {
0203     sendCommand({ {QStringLiteral("type"), QStringLiteral("compute")} });
0204 }
0205 
0206 void CMakeServer::codemodel()
0207 {
0208     sendCommand({ {QStringLiteral("type"), QStringLiteral("codemodel")} });
0209 }
0210 
0211 #include "moc_cmakeserver.cpp"