Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qmllsmain.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5#include "qmllsmain_p.h"
6
7#include <private/qhttpmessagestreamparser_p.h>
8#include <private/qqmlglobal_p.h>
9#include <private/qqmljscompiler_p.h>
10#include <private/qqmljsimporter_p.h>
11#include <private/qqmljslogger_p.h>
12#include <private/qqmljsresourcefilemapper_p.h>
13#include <private/qqmljsscope_p.h>
14#include <private/qqmllanguageserver_p.h>
15#include <private/qqmltoolingsettings_p.h>
16#include <private/qqmltoolingutils_p.h>
17
18#include <QtCore/qdebug.h>
19#include <QtCore/qfile.h>
20#include <QtCore/qdir.h>
21#include <QtCore/qfileinfo.h>
22#include <QtCore/qcoreapplication.h>
23#include <QtCore/qdiriterator.h>
24#include <QtCore/qjsonobject.h>
25#include <QtCore/qjsonarray.h>
26#include <QtCore/qjsondocument.h>
27#include <QtCore/qmutex.h>
28#include <QtCore/QMutexLocker>
29#include <QtCore/qscopedpointer.h>
30#include <QtCore/qrunnable.h>
31#include <QtCore/qthreadpool.h>
32#include <QtCore/qtimer.h>
33
34#if QT_CONFIG(commandlineparser)
35# include <QtCore/qcommandlineparser.h>
36#endif
37
38#ifndef QT_BOOTSTRAPPED
39# include <QtCore/qlibraryinfo.h>
40#endif
41
42#include <iostream>
43#ifdef Q_OS_WIN32
44# include <fcntl.h>
45# include <io.h>
46#endif
47
48QT_BEGIN_NAMESPACE
49
50namespace QmlLsp {
51
52QFile *logFile = nullptr;
54
55class AbstractReader : public QObject
56{
58public:
59 virtual ~AbstractReader() { }
60
61protected:
62 virtual void readNextMessageImpl() = 0;
65 void eof();
66public slots:
68};
69
70class StdinReader final : public AbstractReader
71{
72 Q_OBJECT
73public:
74 StdinReader()
75 : m_streamReader(
76 [](const QByteArray &, const QByteArray &) { /* just a header, do nothing */ },
77 [this](const QByteArray &) {
78 // stop reading until we are sure that the server is not shutting down
79 m_isReading = false;
80
81 // message body
82 m_shouldSendData = true;
83 },
84 [this](QtMsgType, QString) {
85 // there was an error
86 m_shouldSendData = true;
87 },
88 QHttpMessageStreamParser::UNBUFFERED)
89 {
90 }
91
92 void sendData()
93 {
94 const bool isEndOfMessage = !m_isReading && !m_hasEof;
95 const qsizetype toSend = m_bytesInBuf;
96 m_bytesInBuf = 0;
97 const QByteArray dataToSend(m_buffer, toSend);
98 emit receivedData(dataToSend, isEndOfMessage);
99 }
100
101private:
102 const static constexpr qsizetype s_bufSize = 1024;
103 qsizetype m_bytesInBuf = 0;
104 char m_buffer[2 * s_bufSize] = {};
105 QHttpMessageStreamParser m_streamReader;
106 /*!
107 \internal
108 Indicates if the current message is not read out entirely.
109 */
110 bool m_isReading = true;
111 /*!
112 \internal
113 Indicates if an EOF was encountered. No more data can be read after an EOF.
114 */
115 bool m_hasEof = false;
116 /*!
117 \internal
118 Indicates whether sendData() should be called or not.
119 */
120 bool m_shouldSendData = false;
121
122protected:
124 {
125 if (m_hasEof)
126 return;
127 m_isReading = true;
128 // Try to fill up the buffer as much as possible before calling the queued signal:
129 // each loop iteration might read only one character from std::in in the worstcase, this
130 // happens for example on macos.
131 while (m_isReading) {
132 // block while waiting for some data
133 if (!std::cin.get(m_buffer[m_bytesInBuf])) {
134 qInfo() << "Received EOF, stopping...";
135 m_hasEof = true;
136 emit eof();
137 return;
138 }
139 // see if more data is available and fill the buffer with it
140 qsizetype readNow = std::cin.readsome(m_buffer + m_bytesInBuf + 1, s_bufSize) + 1;
141 QByteArray toAdd(m_buffer + m_bytesInBuf, readNow);
142 m_bytesInBuf += readNow;
143 m_streamReader.receiveData(std::move(toAdd));
144
145 m_shouldSendData |= m_bytesInBuf >= s_bufSize;
146 if (std::exchange(m_shouldSendData, false))
147 sendData();
148 }
149 }
150};
151
152/*!
153 \internal
154 FileReader allows to read JsonRPC commands from a file. This is useful when the JsonRPC
155 commands can't be read from stdin, for example when running qmlls in the macos profiler.
156 */
157class FileReader final : public AbstractReader
158{
159 Q_OBJECT
160public:
161 FileReader(const QString &fileName) : m_file(fileName)
162 {
163 bool isOpen = m_file.open(QFile::ReadOnly);
164 Q_ASSERT(isOpen);
165 }
166
167private:
168 QFile m_file;
169
170protected:
172 {
173 if (!m_file.isOpen())
174 return;
175 emit receivedData(m_file.readAll(), false);
176 // emit eof will shutdown qmlls before it manages to complete the preceding requests, so
177 // don't emit eof when reading from a file.
178 // also, just read the content of the file once, so close it now
179 m_file.close();
180 }
181};
182
183static QStringList collectImportPaths(const QCommandLineParser &parser,
184 const QCommandLineOption &qmlImportPathOption,
185 const QCommandLineOption &environmentOption,
186 const QCommandLineOption &qmlImportNoDefault)
187{
188 QStringList importPaths;
189 if (parser.isSet(qmlImportPathOption)) {
190 const QStringList pathsFromOption =
191 QQmlToolingUtils::getAndWarnForInvalidDirsFromOption(parser, qmlImportPathOption);
192 qInfo().nospace().noquote() << "Using import directories passed by -I: \""
193 << pathsFromOption.join(u"\", \""_s) << "\".";
194 importPaths << pathsFromOption;
195 }
196 if (parser.isSet(environmentOption)) {
197 if (const QStringList dirsFromEnv =
198 QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv(u"QML_IMPORT_PATH"_s);
199 !dirsFromEnv.isEmpty()) {
200 qInfo().nospace().noquote()
201 << "Using import directories passed from environment variable "
202 "\"QML_IMPORT_PATH\": \""
203 << dirsFromEnv.join(u"\", \""_s) << "\".";
204 importPaths << dirsFromEnv;
205 }
206
207 if (const QStringList dirsFromEnv2 =
208 QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv(u"QML2_IMPORT_PATH"_s);
209 !dirsFromEnv2.isEmpty()) {
210 qInfo().nospace().noquote()
211 << "Using import directories passed from the deprecated environment variable "
212 "\"QML2_IMPORT_PATH\": \""
213 << dirsFromEnv2.join(u"\", \""_s) << "\".";
214 importPaths << dirsFromEnv2;
215 }
216 }
217
218 // add as default fallback at the end
219 if (!parser.isSet(qmlImportNoDefault))
220 importPaths << QLibraryInfo::path(QLibraryInfo::QmlImportsPath);
221 return importPaths;
222}
223
224static int cmakeJobs(const QCommandLineParser &parser, const QCommandLineOption &cmakeJobsOption)
225{
226 auto parseAndWarn = [](const QString &valueString, const QString &infoMessage,
227 const QString &warningMessage) {
228 bool ok = false;
229 if (valueString == QQmlCodeModel::s_maxCMakeJobs) {
230 const int value = QThread::idealThreadCount();
231 qInfo().noquote() << infoMessage.arg("max (%1)"_L1.arg(QString::number(value)));
232 return value;
233 }
234
235 const int value = valueString.toInt(&ok);
236 if (!ok || value < 1) {
237 qInfo().noquote() << warningMessage.arg(valueString);
238 return QQmlCodeModel::s_defaultCMakeJobs;
239 }
240 qInfo().noquote() << infoMessage.arg(QString::number(value));
241 return value;
242 };
243
244 if (parser.isSet(cmakeJobsOption)) {
245 return parseAndWarn(
246 parser.value(cmakeJobsOption), "Using %1 jobs for CMake, set via --cmake-jobs."_L1,
247 "Value \"%1\" passed to --cmake-jobs is not a number greater than 0 and not "
248 "\"max\", using default value of 1 instead."_L1);
249 }
250
251 if (!qEnvironmentVariableIsSet("QMLLS_CMAKE_JOBS")) {
252 qInfo() << "Using 1 job for CMake, you can increase that value with the QMLLS_CMAKE_JOBS "
253 "environment variable or the --cmake-jobs option.";
254 return QQmlCodeModel::s_defaultCMakeJobs;
255 }
256 return parseAndWarn(
257 qEnvironmentVariable("QMLLS_CMAKE_JOBS"),
258 "Using %1 jobs for CMake, set via QMLLS_CMAKE_JOBS environment variable."_L1,
259 "Value \"%1\" passed to QMLLS_CMAKE_JOBS is not a number greater than 0 and not "
260 "\"max\", using default value of 1 instead."_L1);
261}
262
263static bool prepareCMakeFeature(const QCommandLineParser &parser,
264 const QCommandLineOption &noCMakeCallsOption)
265{
266 if (qmlGetConfigOption<bool, qmlConvertBoolConfigOption>("QMLLS_NO_CMAKE_CALLS")) {
267 qWarning() << "Disabling CMake calls via QMLLS_NO_CMAKE_CALLS environment variable.";
268 return false;
269 }
270 if (parser.isSet(noCMakeCallsOption)) {
271 qWarning() << "Disabling CMake calls via command line switch.";
272 return false;
273 }
274 return true;
275}
276
277// To debug:
278//
279// * simple logging can be redirected to a file
280// passing -l <file> to the qmlls command
281//
282// * more complex debugging can use named pipes:
283//
284// mkfifo qmllsIn
285// mkfifo qmllsOut
286//
287// this together with a qmllsEcho script that can be defined as
288//
289// #!/bin/sh
290// cat -u < ~/qmllsOut &
291// cat -u > ~/qmllsIn
292//
293// allows to use qmllsEcho as lsp server, and still easily start
294// it in a terminal
295//
296// qmlls < ~/qmllsIn > ~/qmllsOut
297//
298// * statup can be slowed down to have the time to attach via the
299// -w <nSeconds> flag.
300
301int qmllsMain(int argv, char *argc[])
302{
303#ifdef Q_OS_WIN32
304 // windows does not open stdin/stdout in binary mode by default
305 int err = _setmode(_fileno(stdout), _O_BINARY);
306 if (err == -1)
307 perror("Cannot set mode for stdout");
308 err = _setmode(_fileno(stdin), _O_BINARY);
309 if (err == -1)
310 perror("Cannot set mode for stdin");
311#endif
312
313 QHashSeed::setDeterministicGlobalSeed();
314 QCoreApplication app(argv, argc);
315
316 QCommandLineParser parser;
317 QQmlToolingSharedSettings settings("qmlls"_L1);
318 parser.setApplicationDescription("QML languageserver"_L1);
319
320 parser.addHelpOption();
321 QCommandLineOption waitOption(QStringList() << "w"_L1
322 << "wait"_L1,
323 "Waits the given number of seconds before startup"_L1,
324 "waitSeconds"_L1);
325 parser.addOption(waitOption);
326
327 QCommandLineOption verboseOption(
328 QStringList() << "v"_L1
329 << "verbose"_L1,
330 "Outputs extra information on the operations being performed"_L1);
331 parser.addOption(verboseOption);
332
333 QCommandLineOption logFileOption(QStringList() << "l"_L1
334 << "log-file"_L1,
335 "Writes logging to the given file"_L1, "logFile"_L1);
336 parser.addOption(logFileOption);
337
338 QString buildDir = QStringLiteral(u"buildDir");
339 QCommandLineOption buildDirOption(QStringList() << "b"_L1
340 << "build-dir"_L1,
341 "Adds a build dir to look up for qml information"_L1,
342 buildDir);
343 parser.addOption(buildDirOption);
344 settings.addOption(buildDir);
345
346 QString qmlImportPath = QStringLiteral(u"importPaths");
347 QCommandLineOption qmlImportPathOption(QStringList() << "I"_L1,
348 "Look for QML modules in the specified directory"_L1,
349 qmlImportPath);
350 parser.addOption(qmlImportPathOption);
351 settings.addOption(qmlImportPath);
352
353 QCommandLineOption environmentOption(
354 QStringList() << "E"_L1,
355 "Use the QML_IMPORT_PATH environment variable to look for QML Modules"_L1);
356 parser.addOption(environmentOption);
357
358 QCommandLineOption writeDefaultsOption(
359 QStringList() << "write-defaults"_L1,
360 "Writes defaults settings to .qmlls.ini and exits (Warning: This "
361 "will overwrite any existing settings and comments!)"_L1);
362 parser.addOption(writeDefaultsOption);
363
364 QCommandLineOption ignoreSettings(QStringList() << "ignore-settings"_L1,
365 "Ignores all settings files and only takes "
366 "command line options into consideration"_L1);
367 parser.addOption(ignoreSettings);
368
369 QCommandLineOption noCMakeCallsOption(
370 QStringList() << "no-cmake-calls"_L1,
371 "Disables automatic CMake rebuilds and C++ file watching."_L1);
372 parser.addOption(noCMakeCallsOption);
373 settings.addOption("no-cmake-calls"_L1, "false"_L1);
374
375 QCommandLineOption cmakeJobsOption(QStringList() << "cmake-jobs"_L1 << "j"_L1,
376 "Number of CMake jobs for automatic CMake rebuilds. "_L1,
377 "jobs"_L1, "1"_L1);
378 parser.addOption(cmakeJobsOption);
379 settings.addOption("CMakeJobs"_L1, "1"_L1);
380
381 QCommandLineOption docDir({ { "d"_L1, "p"_L1, "doc-dir"_L1 },
382 "Documentation path to use for the documentation hints feature"_L1,
383 "path"_L1,
384 QString() });
385 parser.addOption(docDir);
386 settings.addOption("docDir"_L1);
387
388 QCommandLineOption qmlImportNoDefault(QStringList() << "bare"_L1,
389 "Do not include default import directories. "
390 "This may be used to run qmlls on a Boot2Qt project."_L1);
391 parser.addOption(qmlImportNoDefault);
392 const QString qmlImportNoDefaultSetting = "DisableDefaultImports"_L1;
393 settings.addOption(qmlImportNoDefaultSetting, false);
394
395 QCommandLineOption inputFile(QStringList() << "inputFile"_L1,
396 "Read from file instead of stdin"_L1, "fileName"_L1);
397 inputFile.setFlags(QCommandLineOption::HiddenFromHelp);
398 parser.addOption(inputFile);
399
400 // we can't use parser.addVersionOption() because we already have one '-v' option for verbose...
401 QCommandLineOption versionOption("version"_L1, "Displays version information."_L1);
402 parser.addOption(versionOption);
403
404 parser.process(app);
405
406 if (parser.isSet(versionOption)) {
407 parser.showVersion();
408 return EXIT_SUCCESS;
409 }
410
411 if (parser.isSet(writeDefaultsOption)) {
412 return settings.writeDefaults() ? 0 : 1;
413 }
414
415 if (parser.isSet(logFileOption)) {
416 QString fileName = parser.value(logFileOption);
417 qInfo() << "will log to" << fileName;
418 logFile = new QFile(fileName);
419 logFileLock = new QMutex;
420 if (!logFile->open(QFile::WriteOnly | QFile::Truncate | QFile::Text)) {
421 qWarning("Failed to open file %s: %s", qPrintable(logFile->fileName()),
422 qPrintable(logFile->errorString()));
423 }
424 qInstallMessageHandler([](QtMsgType t, const QMessageLogContext &, const QString &msg) {
425 QMutexLocker l(logFileLock);
426 logFile->write(QString::number(int(t)).toUtf8());
427 logFile->write(" ");
428 logFile->write(msg.toUtf8());
429 logFile->write("\n");
430 logFile->flush();
431 });
432 }
433 if (parser.isSet(waitOption)) {
434 int waitSeconds = parser.value(waitOption).toInt();
435 if (waitSeconds > 0)
436 qDebug() << "waiting";
437 QThread::sleep(waitSeconds);
438 qDebug() << "starting";
439 }
440 QMutex writeMutex;
441 QQmlLanguageServer qmlServer(
442 [&writeMutex](const QByteArray &data) {
443 QMutexLocker l(&writeMutex);
444 std::cout.write(data.constData(), data.size());
445 std::cout.flush();
446 },
447 (parser.isSet(ignoreSettings) ? nullptr : &settings));
448
449 if (parser.isSet(verboseOption)) {
450 QLoggingCategory::setFilterRules("qt.languageserver*.debug=true\n"_L1);
451 qmlServer.codeModelManager()->setVerbose(true);
452 }
453 if (parser.isSet(docDir))
454 qmlServer.codeModelManager()->setDocumentationRootPath(
455 QString::fromUtf8(parser.value(docDir).toUtf8()));
456
457 if (parser.isSet(buildDirOption)) {
458 const QStringList dirs =
459 QQmlToolingUtils::getAndWarnForInvalidDirsFromOption(parser, buildDirOption);
460
461 qInfo().nospace().noquote()
462 << "Using build directories passed by -b: \"" << dirs.join(u"\", \""_s) << "\".";
463
464 qmlServer.codeModelManager()->setBuildPathsForRootUrl(QByteArray(), dirs);
465 } else if (QStringList dirsFromEnv =
466 QQmlToolingUtils::getAndWarnForInvalidDirsFromEnv(u"QMLLS_BUILD_DIRS"_s);
467 !dirsFromEnv.isEmpty()) {
468
469 // warn now at qmlls startup that those directories will be used later in qqmlcodemodel when
470 // searching for build folders.
471 qInfo().nospace().noquote() << "Using build directories passed from environment variable "
472 "\"QMLLS_BUILD_DIRS\": \""
473 << dirsFromEnv.join(u"\", \""_s) << "\".";
474 qmlServer.codeModelManager()->setBuildPathsForRootUrl(QByteArray(), dirsFromEnv);
475 } else {
476 qInfo() << "Using the build directories found in the .qmlls.ini file. Your build folder "
477 "might not be found if no .qmlls.ini files are present in the root source "
478 "folder.";
479 }
480
481 qmlServer.codeModelManager()->setImportPaths(
482 collectImportPaths(parser, qmlImportPathOption, environmentOption, qmlImportNoDefault));
483
484 if (prepareCMakeFeature(parser, noCMakeCallsOption)) {
485 qmlServer.codeModelManager()->setCMakeJobs(cmakeJobs(parser, cmakeJobsOption));
486 qmlServer.codeModelManager()->tryEnableCMakeCalls();
487 } else {
488 qmlServer.codeModelManager()->disableCMakeCalls();
489 }
490
491 auto r = parser.isSet(inputFile) && parser.value(inputFile) != "-"_L1
492 ? std::unique_ptr<AbstractReader>(std::make_unique<FileReader>(parser.value(inputFile)))
493 : std::unique_ptr<AbstractReader>(std::make_unique<StdinReader>());
494 QThread workerThread;
495 r->moveToThread(&workerThread);
496 QObject::connect(r.get(), &AbstractReader::receivedData, qmlServer.server(),
497 &QLanguageServer::receiveData);
498 QObject::connect(qmlServer.server(), &QLanguageServer::readNextMessage, r.get(),
499 &AbstractReader::readNextMessage);
500 auto exit = [&app, &workerThread]() {
501 workerThread.quit();
502 workerThread.wait();
503 QTimer::singleShot(100, &app, []() {
504 QCoreApplication::processEvents();
505 QCoreApplication::exit();
506 });
507 };
508 QObject::connect(r.get(), &StdinReader::eof, &app, exit);
509 QObject::connect(qmlServer.server(), &QLanguageServer::exit, &workerThread, exit);
510
511 emit r->readNextMessage();
512 workerThread.start();
513 app.exec();
514 workerThread.quit();
515 workerThread.wait();
516 return qmlServer.returnValue();
517}
518
519} // namespace QmlLsp
520
521QT_END_NAMESPACE
522
523#include <qmllsmain.moc>
void readNextMessageImpl() override
void readNextMessageImpl() override
QBasicMutex * logFileLock
Definition qmllsmain.cpp:53
static int cmakeJobs(const QCommandLineParser &parser, const QCommandLineOption &cmakeJobsOption)
int qmllsMain(int argv, char *argc[])
static bool prepareCMakeFeature(const QCommandLineParser &parser, const QCommandLineOption &noCMakeCallsOption)
QFile * logFile
Definition qmllsmain.cpp:52
static QStringList collectImportPaths(const QCommandLineParser &parser, const QCommandLineOption &qmlImportPathOption, const QCommandLineOption &environmentOption, const QCommandLineOption &qmlImportNoDefault)