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

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