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