1 /*
2     SPDX-FileCopyrightText: 2017 Aleix Pol <aleixpol@kde.org>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "cmakeserver.h"
8 #include "cmakeprojectdata.h"
9 #include "cmakeutils.h"
10 
11 #include <interfaces/iruntime.h>
12 #include <interfaces/iruntimecontroller.h>
13 #include <interfaces/icore.h>
14 #include <interfaces/iproject.h>
15 
16 #include <QDir>
17 #include <QJsonDocument>
18 #include <QJsonObject>
19 #include <QJsonArray>
20 #include <QTimer>
21 #include <QTemporaryFile>
22 #include "debug.h"
23 
CMakeServer(KDevelop::IProject * project)24 CMakeServer::CMakeServer(KDevelop::IProject* project)
25     : QObject()
26     , m_localSocket(new QLocalSocket(this))
27 {
28     QString path;
29     {
30         const auto cacheLocation = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
31         QDir::temp().mkpath(cacheLocation);
32 
33         QTemporaryFile file(cacheLocation + QLatin1String("/kdevelopcmake"));
34         file.open();
35         file.close();
36         path = file.fileName();
37         Q_ASSERT(!path.isEmpty());
38     }
39 
40     m_process.setProcessChannelMode(QProcess::ForwardedChannels);
41 
42     connect(&m_process, &QProcess::errorOccurred,
43             this, [this, path](QProcess::ProcessError error) {
44         qCWarning(CMAKE) << "cmake server error:" << error << path << m_process.readAllStandardError() << m_process.readAllStandardOutput();
45     });
46     connect(&m_process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [](int code){
47         qCDebug(CMAKE) << "cmake server finished with code" << code;
48     });
49     connect(&m_process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &CMakeServer::finished);
50 
51     connect(m_localSocket, &QIODevice::readyRead, this, &CMakeServer::processOutput);
52 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
53     connect(m_localSocket, &QLocalSocket::errorOccurred,
54 #else
55     connect(m_localSocket, QOverload<QLocalSocket::LocalSocketError>::of(&QLocalSocket::error),
56 #endif
57             this, [this, path](QLocalSocket::LocalSocketError socketError) {
58         qCWarning(CMAKE) << "cmake server socket error:" << socketError << path;
59         setConnected(false);
60     });
61     connect(m_localSocket, &QLocalSocket::connected, this, [this]() { setConnected(true); });
62 
63     connect(&m_process, &QProcess::started, this, [this, path](){
64         //Once the process has started, wait for the file to be created, then connect to it
65         QTimer::singleShot(1000, this, [this, path]() {
66             m_localSocket->connectToServer(path, QIODevice::ReadWrite);
67         });
68     });
69     // we're called with the importing project as our parent, so we can fetch configured
70     // cmake executable (project-specific or kdevelop-wide) rather than the system version.
71     m_process.setProgram(CMake::currentCMakeExecutable(project).toLocalFile());
72     m_process.setArguments({QStringLiteral("-E"), QStringLiteral("server"), QStringLiteral("--experimental"), QLatin1String("--pipe=") + path});
73     KDevelop::ICore::self()->runtimeController()->currentRuntime()->startProcess(&m_process);
74 }
75 
~CMakeServer()76 CMakeServer::~CMakeServer()
77 {
78     m_process.disconnect();
79     m_process.kill();
80     m_process.waitForFinished();
81 }
82 
setConnected(bool conn)83 void CMakeServer::setConnected(bool conn)
84 {
85     if (conn == m_connected)
86         return;
87 
88     m_connected = conn;
89     if (m_connected)
90         Q_EMIT connected();
91     else
92         Q_EMIT disconnected();
93 }
94 
isServerAvailable()95 bool CMakeServer::isServerAvailable()
96 {
97     return m_localSocket->isOpen();
98 }
99 
openTag()100 static QByteArray openTag() { return QByteArrayLiteral("\n[== \"CMake Server\" ==[\n"); }
closeTag()101 static QByteArray closeTag() { return QByteArrayLiteral("\n]== \"CMake Server\" ==]\n"); }
102 
sendCommand(const QJsonObject & object)103 void CMakeServer::sendCommand(const QJsonObject& object)
104 {
105     Q_ASSERT(isServerAvailable());
106 
107     const QByteArray data = openTag() + QJsonDocument(object).toJson(QJsonDocument::Compact) + closeTag();
108     auto len = m_localSocket->write(data);
109 //     qCDebug(CMAKE) << "writing...\n" << QJsonDocument(object).toJson();
110     Q_ASSERT(len > 0);
111 }
112 
processOutput()113 void CMakeServer::processOutput()
114 {
115     Q_ASSERT(m_localSocket);
116 
117     const auto openTag = ::openTag();
118     const auto closeTag = ::closeTag();
119 
120     m_buffer += m_localSocket->readAll();
121     for(; m_buffer.size() > openTag.size(); ) {
122 
123         Q_ASSERT(m_buffer.startsWith(openTag));
124         const int idx = m_buffer.indexOf(closeTag, openTag.size());
125         if (idx >= 0) {
126             emitResponse(m_buffer.mid(openTag.size(), idx - openTag.size()));
127             m_buffer.remove(0, idx + closeTag.size());
128         } else {
129             break;
130         }
131     }
132 }
133 
emitResponse(const QByteArray & data)134 void CMakeServer::emitResponse(const QByteArray& data)
135 {
136     QJsonParseError error;
137     auto doc = QJsonDocument::fromJson(data, &error);
138     if (error.error) {
139         qCWarning(CMAKE) << "error processing" << error.errorString() << data;
140     }
141     Q_ASSERT(doc.isObject());
142     Q_EMIT response(doc.object());
143 }
144 
handshake(const KDevelop::Path & source,const KDevelop::Path & build)145 void CMakeServer::handshake(const KDevelop::Path& source, const KDevelop::Path& build)
146 {
147     Q_ASSERT(!source.isEmpty());
148 
149     const QString generatorVariable = QStringLiteral("CMAKE_GENERATOR");
150     const QString homeDirectoryVariable = QStringLiteral("CMAKE_HOME_DIRECTORY");
151     const QString cacheFileDirectoryVariable = QStringLiteral("CMAKE_CACHEFILE_DIR");
152     const auto cacheValues = CMake::readCacheValues(KDevelop::Path(build, QStringLiteral("CMakeCache.txt")),
153                                                     {generatorVariable, homeDirectoryVariable, cacheFileDirectoryVariable});
154 
155     QString generator = cacheValues.value(generatorVariable);
156     if (generator.isEmpty()) {
157         generator = CMake::defaultGenerator();
158     }
159 
160     // prefer pre-existing source directory, see also: https://gitlab.kitware.com/cmake/cmake/issues/16736
161     QString sourceDirectory = cacheValues.value(homeDirectoryVariable);
162     if (sourceDirectory.isEmpty()) {
163         sourceDirectory = source.toLocalFile();
164     } else if (QFileInfo(sourceDirectory).canonicalFilePath() != QFileInfo(source.toLocalFile()).canonicalFilePath()) {
165         qCWarning(CMAKE) << "Build directory is configured for another source directory:"
166                    << homeDirectoryVariable << sourceDirectory
167                    << "wanted to open" << source << "in" << build;
168     }
169 
170     // prefer to reuse the exact same build dir path to prevent useless recompilation
171     // when we open a symlinked project path
172     QString buildDirectory = cacheValues.value(cacheFileDirectoryVariable);
173     if (buildDirectory.isEmpty()) {
174         buildDirectory = build.toLocalFile();
175     } else if (QFileInfo(buildDirectory).canonicalFilePath() != QFileInfo(build.toLocalFile()).canonicalFilePath()) {
176         qCWarning(CMAKE) << "Build directory mismatch:"
177                    << cacheFileDirectoryVariable << buildDirectory
178                    << "wanted to open" << build;
179         buildDirectory = build.toLocalFile();
180     }
181 
182     qCDebug(CMAKE) << "Using generator" << generator << "for project"
183                    << sourceDirectory << "aka" << source
184                    << "in" << buildDirectory << "aka" << build;
185 
186     sendCommand({
187         {QStringLiteral("cookie"), {}},
188         {QStringLiteral("type"), QStringLiteral("handshake")},
189         {QStringLiteral("major"), 1},
190         {QStringLiteral("protocolVersion"), QJsonObject{{QStringLiteral("major"), 1}} },
191         {QStringLiteral("sourceDirectory"), sourceDirectory},
192         {QStringLiteral("buildDirectory"), buildDirectory},
193         {QStringLiteral("generator"), generator}
194     });
195 }
196 
configure(const QStringList & args)197 void CMakeServer::configure(const QStringList& args)
198 {
199     sendCommand({
200         {QStringLiteral("type"), QStringLiteral("configure")},
201         {QStringLiteral("cacheArguments"), QJsonArray::fromStringList(args)}
202     });
203 }
204 
compute()205 void CMakeServer::compute()
206 {
207     sendCommand({ {QStringLiteral("type"), QStringLiteral("compute")} });
208 }
209 
codemodel()210 void CMakeServer::codemodel()
211 {
212     sendCommand({ {QStringLiteral("type"), QStringLiteral("codemodel")} });
213 }
214