1// Copyright (C) 2018 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 "qmlpreviewapplication.h"
5
6#include <QtCore/QStringList>
7#include <QtCore/QTextStream>
8#include <QtCore/QProcess>
9#include <QtCore/QTimer>
10#include <QtCore/QDateTime>
11#include <QtCore/QFileInfo>
12#include <QtCore/QDebug>
13#include <QtCore/QDir>
14#include <QtCore/QCommandLineParser>
15#include <QtCore/QTemporaryFile>
16#include <QtCore/QUrl>
17#include <QtCore/QLibraryInfo>
18
19QmlPreviewApplication::QmlPreviewApplication(int &argc, char **argv) :
20 QCoreApplication(argc, argv),
21 m_verbose(false),
22 m_connectionAttempts(0)
23{
24 m_connection.reset(other: new QQmlDebugConnection);
25 m_qmlPreviewClient.reset(other: new QQmlPreviewClient(m_connection.data()));
26 m_connectTimer.setInterval(1000);
27
28 m_loadTimer.setInterval(100);
29 m_loadTimer.setSingleShot(true);
30 connect(sender: &m_loadTimer, signal: &QTimer::timeout, context: this, slot: [this]() {
31 m_qmlPreviewClient->triggerLoad(url: QUrl());
32 });
33
34 connect(sender: &m_connectTimer, signal: &QTimer::timeout, context: this, slot: &QmlPreviewApplication::tryToConnect);
35 connect(sender: m_connection.data(), signal: &QQmlDebugConnection::connected, context: &m_connectTimer, slot: &QTimer::stop);
36
37 connect(sender: m_qmlPreviewClient.data(), signal: &QQmlPreviewClient::error,
38 context: this, slot: &QmlPreviewApplication::logError);
39 connect(sender: m_qmlPreviewClient.data(), signal: &QQmlPreviewClient::request,
40 context: this, slot: &QmlPreviewApplication::serveRequest);
41
42 connect(sender: &m_watcher, signal: &QmlPreviewFileSystemWatcher::fileChanged,
43 context: this, slot: &QmlPreviewApplication::sendFile);
44 connect(sender: &m_watcher, signal: &QmlPreviewFileSystemWatcher::directoryChanged,
45 context: this, slot: &QmlPreviewApplication::sendDirectory);
46}
47
48QmlPreviewApplication::~QmlPreviewApplication()
49{
50 if (m_process && m_process->state() != QProcess::NotRunning) {
51 logStatus(status: "Terminating process ...");
52 m_process->disconnect();
53 m_process->terminate();
54 if (!m_process->waitForFinished(msecs: 1000)) {
55 logStatus(status: "Killing process ...");
56 m_process->kill();
57 }
58 }
59}
60
61void QmlPreviewApplication::parseArguments()
62{
63 setApplicationName(QLatin1String("qmlpreview"));
64 setApplicationVersion(QLatin1String(qVersion()));
65
66 QCommandLineParser parser;
67 parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
68 parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsPositionalArguments);
69
70 parser.setApplicationDescription(QChar::LineFeed + tr(
71 s: "The QML Preview tool watches QML and JavaScript files on disk and updates\n"
72 "the application live with any changes. The application to be previewed\n"
73 "has to enable QML debugging. See the Qt Creator documentation on how to do\n"
74 "this for different Qt versions."));
75
76 QCommandLineOption verbose(QStringList() << QLatin1String("verbose"),
77 tr(s: "Print debugging output."));
78 parser.addOption(commandLineOption: verbose);
79
80 parser.addHelpOption();
81 parser.addVersionOption();
82
83 parser.addPositionalArgument(name: QLatin1String("executable"),
84 description: tr(s: "The executable to be started and previewed."),
85 syntax: QLatin1String("[executable]"));
86 parser.addPositionalArgument(name: QLatin1String("parameters"),
87 description: tr(s: "Parameters for the executable to be started."),
88 syntax: QLatin1String("[parameters...]"));
89
90 parser.process(app: *this);
91
92 QTemporaryFile file;
93 if (file.open())
94 m_socketFile = file.fileName();
95
96 if (parser.isSet(option: verbose))
97 m_verbose = true;
98
99 m_arguments = parser.positionalArguments();
100 if (!m_arguments.isEmpty())
101 m_executablePath = m_arguments.takeFirst();
102
103 if (m_executablePath.isEmpty()) {
104 logError(error: tr(s: "You have to specify an executable to start."));
105 parser.showHelp(exitCode: 2);
106 }
107}
108
109int QmlPreviewApplication::exec()
110{
111 QTimer::singleShot(interval: 0, receiver: this, slot: &QmlPreviewApplication::run);
112 return QCoreApplication::exec();
113}
114
115void QmlPreviewApplication::run()
116{
117 logStatus(status: QString("Listening on %1 ...").arg(a: m_socketFile));
118 m_connection->startLocalServer(fileName: m_socketFile);
119 m_process.reset(other: new QProcess(this));
120 QStringList arguments;
121 arguments << QString("-qmljsdebugger=file:%1,block,services:QmlPreview").arg(a: m_socketFile);
122 arguments << m_arguments;
123
124 m_process->setProcessChannelMode(QProcess::MergedChannels);
125 connect(sender: m_process.data(), signal: &QIODevice::readyRead,
126 context: this, slot: &QmlPreviewApplication::processHasOutput);
127 connect(sender: m_process.data(), signal: QOverload<int, QProcess::ExitStatus>::of(ptr: &QProcess::finished),
128 context: this, slot: [this](int){ processFinished(); });
129 logStatus(status: QString("Starting '%1 %2' ...").arg(args&: m_executablePath, args: arguments.join(sep: QLatin1Char(' '))));
130 m_process->start(program: m_executablePath, arguments);
131 if (!m_process->waitForStarted()) {
132 logError(error: QString("Could not run '%1': %2").arg(args&: m_executablePath, args: m_process->errorString()));
133 exit(retcode: 1);
134 }
135 m_connectTimer.start();
136}
137
138void QmlPreviewApplication::tryToConnect()
139{
140 Q_ASSERT(!m_connection->isConnected());
141 ++m_connectionAttempts;
142
143 if (m_verbose && !(m_connectionAttempts % 5)) {// print every 5 seconds
144 logError(error: QString("No connection received on %1 for %2 seconds ...")
145 .arg(a: m_socketFile).arg(a: m_connectionAttempts));
146 }
147}
148
149void QmlPreviewApplication::processHasOutput()
150{
151 Q_ASSERT(m_process);
152 while (m_process->bytesAvailable()) {
153 QTextStream out(stderr);
154 out << m_process->readAll();
155 }
156}
157
158void QmlPreviewApplication::processFinished()
159{
160 Q_ASSERT(m_process);
161 int exitCode = 0;
162 if (m_process->exitStatus() == QProcess::NormalExit) {
163 logStatus(status: QString("Process exited (%1).").arg(a: m_process->exitCode()));
164 } else {
165 logError(error: "Process crashed!");
166 exitCode = 3;
167 }
168 exit(retcode: exitCode);
169}
170
171void QmlPreviewApplication::logError(const QString &error)
172{
173 QTextStream err(stderr);
174 err << "Error: " << error << Qt::endl;
175}
176
177void QmlPreviewApplication::logStatus(const QString &status)
178{
179 if (!m_verbose)
180 return;
181 QTextStream err(stderr);
182 err << status << Qt::endl;
183}
184
185void QmlPreviewApplication::serveRequest(const QString &path)
186{
187 QFileInfo info(path);
188
189 if (info.isDir()) {
190 m_qmlPreviewClient->sendDirectory(path, entries: QDir(path).entryList());
191 m_watcher.addDirectory(file: path);
192 } else {
193 QFile file(path);
194 if (file.open(flags: QIODevice::ReadOnly)) {
195 m_qmlPreviewClient->sendFile(path, contents: file.readAll());
196 m_watcher.addFile(file: path);
197 } else {
198 logStatus(status: QString("Could not open file %1 for reading: %2").arg(a: path)
199 .arg(a: file.errorString()));
200 m_qmlPreviewClient->sendError(path);
201 }
202 }
203}
204
205bool QmlPreviewApplication::sendFile(const QString &path)
206{
207 QFile file(path);
208 if (file.open(flags: QIODevice::ReadOnly)) {
209 m_qmlPreviewClient->sendFile(path, contents: file.readAll());
210 // Defer the Load, because files tend to change multiple times in a row.
211 m_loadTimer.start();
212 return true;
213 }
214 logStatus(status: QString("Could not open file %1 for reading: %2").arg(a: path).arg(a: file.errorString()));
215 return false;
216}
217
218void QmlPreviewApplication::sendDirectory(const QString &path)
219{
220 m_qmlPreviewClient->sendDirectory(path, entries: QDir(path).entryList());
221 m_loadTimer.start();
222}
223

source code of qtdeclarative/tools/qmlpreview/qmlpreviewapplication.cpp