1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include <QtQmlLS/private/qqmllanguageserver_p.h>
5#include <QtCore/qdebug.h>
6#include <QtCore/qfile.h>
7#include <QtCore/qdir.h>
8#include <QtCore/qfileinfo.h>
9#include <QtCore/qcoreapplication.h>
10#include <QtQmlToolingSettings/private/qqmltoolingsettings_p.h>
11#include <QtCore/qdiriterator.h>
12#include <QtCore/qjsonobject.h>
13#include <QtCore/qjsonarray.h>
14#include <QtCore/qjsondocument.h>
15#include <QtCore/qmutex.h>
16#include <QtCore/QMutexLocker>
17#include <QtCore/qscopedpointer.h>
18#include <QtCore/qrunnable.h>
19#include <QtCore/qthreadpool.h>
20#include <QtCore/qtimer.h>
21
22#include <QtJsonRpc/private/qhttpmessagestreamparser_p.h>
23
24#include <QtQmlCompiler/private/qqmljsresourcefilemapper_p.h>
25#include <QtQmlCompiler/private/qqmljscompiler_p.h>
26#include <QtQmlCompiler/private/qqmljslogger_p.h>
27#include <QtQmlCompiler/private/qqmljsscope_p.h>
28#include <QtQmlCompiler/private/qqmljsimporter_p.h>
29#if QT_CONFIG(commandlineparser)
30# include <QtCore/qcommandlineparser.h>
31#endif
32
33#ifndef QT_BOOTSTRAPPED
34# include <QtCore/qlibraryinfo.h>
35#endif
36
37#include <iostream>
38#ifdef Q_OS_WIN32
39# include <fcntl.h>
40# include <io.h>
41#endif
42
43using namespace QmlLsp;
44
45QFile *logFile = nullptr;
46QBasicMutex *logFileLock = nullptr;
47
48class StdinReader : public QObject
49{
50 Q_OBJECT
51public:
52 void run()
53 {
54 auto guard = qScopeGuard(f: [this]() { emit eof(); });
55 const constexpr qsizetype bufSize = 1024;
56 qsizetype bytesInBuf = 0;
57 char bufferData[2 * bufSize];
58 char *buffer = static_cast<char *>(bufferData);
59
60 auto trySend = [this, &bytesInBuf, buffer]() {
61 if (bytesInBuf == 0)
62 return;
63 qsizetype toSend = bytesInBuf;
64 bytesInBuf = 0;
65 QByteArray dataToSend(buffer, toSend);
66 emit receivedData(data: dataToSend);
67 };
68 QHttpMessageStreamParser streamParser(
69 [](const QByteArray &, const QByteArray &) { /* just a header, do nothing */ },
70 [&trySend](const QByteArray &) {
71 // message body
72 trySend();
73 },
74 [&trySend](QtMsgType, QString) {
75 // there was an error
76 trySend();
77 },
78 QHttpMessageStreamParser::UNBUFFERED);
79
80 while (std::cin.get(c&: buffer[bytesInBuf])) { // should poll/select and process events
81 qsizetype readNow = std::cin.readsome(s: buffer + bytesInBuf + 1, n: bufSize) + 1;
82 QByteArray toAdd(buffer + bytesInBuf, readNow);
83 bytesInBuf += readNow;
84 if (bytesInBuf >= bufSize)
85 trySend();
86 streamParser.receiveData(data: toAdd);
87 }
88 trySend();
89 }
90signals:
91 void receivedData(const QByteArray &data);
92 void eof();
93};
94
95// To debug:
96//
97// * simple logging can be redirected to a file
98// passing -l <file> to the qmlls command
99//
100// * more complex debugging can use named pipes:
101//
102// mkfifo qmllsIn
103// mkfifo qmllsOut
104//
105// this together with a qmllsEcho script that can be defined as
106//
107// #!/bin/sh
108// cat -u < ~/qmllsOut &
109// cat -u > ~/qmllsIn
110//
111// allows to use qmllsEcho as lsp server, and still easily start
112// it in a terminal
113//
114// qmlls < ~/qmllsIn > ~/qmllsOut
115//
116// * statup can be slowed down to have the time to attach via the
117// -w <nSeconds> flag.
118
119int main(int argv, char *argc[])
120{
121#ifdef Q_OS_WIN32
122 // windows does not open stdin/stdout in binary mode by default
123 int err = _setmode(_fileno(stdout), _O_BINARY);
124 if (err == -1)
125 perror("Cannot set mode for stdout");
126 err = _setmode(_fileno(stdin), _O_BINARY);
127 if (err == -1)
128 perror("Cannot set mode for stdin");
129#endif
130
131 QHashSeed::setDeterministicGlobalSeed();
132 QCoreApplication app(argv, argc);
133 QCoreApplication::setApplicationName("qmlls");
134 QCoreApplication::setApplicationVersion(QT_VERSION_STR);
135
136 QCommandLineParser parser;
137 QQmlToolingSettings settings(QLatin1String("qmlls"));
138 parser.setApplicationDescription(QLatin1String(R"(QML languageserver)"));
139
140 parser.addHelpOption();
141 QCommandLineOption waitOption(QStringList() << "w"
142 << "wait",
143 QLatin1String("Waits the given number of seconds before startup"),
144 QLatin1String("waitSeconds"));
145 parser.addOption(commandLineOption: waitOption);
146
147 QCommandLineOption verboseOption(
148 QStringList() << "v"
149 << "verbose",
150 QLatin1String("Outputs extra information on the operations being performed"));
151 parser.addOption(commandLineOption: verboseOption);
152
153 QCommandLineOption logFileOption(QStringList() << "l"
154 << "log-file",
155 QLatin1String("Writes logging to the given file"),
156 QLatin1String("logFile"));
157 parser.addOption(commandLineOption: logFileOption);
158
159 QString buildDir = QStringLiteral(u"buildDir");
160 QCommandLineOption buildDirOption(
161 QStringList() << "b"
162 << "build-dir",
163 QLatin1String("Adds a build dir to look up for qml information"), buildDir);
164 parser.addOption(commandLineOption: buildDirOption);
165 settings.addOption(name: buildDir);
166
167 QCommandLineOption writeDefaultsOption(
168 QStringList() << "write-defaults",
169 QLatin1String("Writes defaults settings to .qmlls.ini and exits (Warning: This "
170 "will overwrite any existing settings and comments!)"));
171 parser.addOption(commandLineOption: writeDefaultsOption);
172
173 QCommandLineOption ignoreSettings(QStringList() << "ignore-settings",
174 QLatin1String("Ignores all settings files and only takes "
175 "command line options into consideration"));
176 parser.addOption(commandLineOption: ignoreSettings);
177
178 parser.process(app);
179
180 if (parser.isSet(option: writeDefaultsOption)) {
181 return settings.writeDefaults() ? 0 : 1;
182 }
183 if (parser.isSet(option: logFileOption)) {
184 QString fileName = parser.value(option: logFileOption);
185 qInfo() << "will log to" << fileName;
186 logFile = new QFile(fileName);
187 logFileLock = new QMutex;
188 logFile->open(flags: QFile::WriteOnly | QFile::Truncate | QFile::Text);
189 qInstallMessageHandler([](QtMsgType t, const QMessageLogContext &, const QString &msg) {
190 QMutexLocker l(logFileLock);
191 logFile->write(data: QString::number(int(t)).toUtf8());
192 logFile->write(data: " ");
193 logFile->write(data: msg.toUtf8());
194 logFile->write(data: "\n");
195 logFile->flush();
196 });
197 }
198 if (parser.isSet(option: verboseOption))
199 QLoggingCategory::setFilterRules("qt.languageserver*.debug=true\n");
200 if (parser.isSet(option: waitOption)) {
201 int waitSeconds = parser.value(option: waitOption).toInt();
202 if (waitSeconds > 0)
203 qDebug() << "waiting";
204 QThread::sleep(waitSeconds);
205 qDebug() << "starting";
206 }
207 QMutex writeMutex;
208 QQmlLanguageServer qmlServer(
209 [&writeMutex](const QByteArray &data) {
210 QMutexLocker l(&writeMutex);
211 std::cout.write(s: data.constData(), n: data.size());
212 std::cout.flush();
213 },
214 (parser.isSet(option: ignoreSettings) ? nullptr : &settings));
215
216 const QStringList envPaths =
217 qEnvironmentVariable(varName: "QMLLS_BUILD_DIRS").split(sep: u',', behavior: Qt::SkipEmptyParts);
218 for (const QString &envPath : envPaths) {
219 QFileInfo info(envPath);
220 if (!info.exists()) {
221 qWarning() << "Argument" << buildDir << "passed via QMLLS_BUILD_DIRS does not exist.";
222 } else if (!info.isDir()) {
223 qWarning() << "Argument" << buildDir
224 << "passed via QMLLS_BUILD_DIRS is not a directory.";
225 }
226 }
227
228 QStringList buildDirs;
229 if (parser.isSet(option: buildDirOption)) {
230 buildDirs = parser.values(option: buildDirOption);
231 for (const QString &buildDir : buildDirs) {
232 QFileInfo info(buildDir);
233 if (!info.exists()) {
234 qWarning() << "Argument" << buildDir << "passed to --build-dir does not exist.";
235 } else if (!info.isDir()) {
236 qWarning() << "Argument" << buildDir << "passed to --build-dir is not a directory.";
237 }
238 }
239 qmlServer.codeModel()->setBuildPathsForRootUrl(url: QByteArray(), paths: buildDirs);
240 }
241
242 if (!buildDirs.isEmpty()) {
243 qInfo() << "Using the build directories passed via the --build-dir option:"
244 << buildDirs.join(sep: ", ");
245 } else if (!envPaths.isEmpty()) {
246 qInfo() << "Using the build directories passed via the QMLLS_BUILD_DIRS environment "
247 "variable"
248 << buildDirs.join(sep: ", ");
249 } else {
250 qInfo() << "Using the build directories found in the .qmlls.ini file. Your build folder "
251 "might not be found if no .qmlls.ini files are present in the root source "
252 "folder.";
253 }
254
255 if (buildDirs.isEmpty() && envPaths.isEmpty()) {
256 qInfo() << "Build directory path omitted: Your source folders will be searched for "
257 ".qmlls.ini files.";
258 }
259 StdinReader r;
260 QObject::connect(sender: &r, signal: &StdinReader::receivedData,
261 context: qmlServer.server(), slot: &QLanguageServer::receiveData);
262 QObject::connect(sender: &r, signal: &StdinReader::eof, context: &app, slot: [&app]() {
263 QTimer::singleShot(interval: 100, receiver: &app, slot: []() {
264 QCoreApplication::processEvents();
265 QCoreApplication::exit();
266 });
267 });
268 QThreadPool::globalInstance()->start(functionToRun: [&r]() { r.run(); });
269 app.exec();
270 return qmlServer.returnValue();
271}
272
273#include "qmllanguageservertool.moc"
274

source code of qtdeclarative/tools/qmlls/qmllanguageservertool.cpp