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 
QmlPreviewApplication(int & argc,char ** argv)43 QmlPreviewApplication::QmlPreviewApplication(int &argc, char **argv) :
44     QCoreApplication(argc, argv),
45     m_verbose(false),
46     m_connectionAttempts(0)
47 {
48     m_connection.reset(new QQmlDebugConnection);
49     m_qmlPreviewClient.reset(new QQmlPreviewClient(m_connection.data()));
50     m_connectTimer.setInterval(1000);
51 
52     m_loadTimer.setInterval(100);
53     m_loadTimer.setSingleShot(true);
54     connect(&m_loadTimer, &QTimer::timeout, this, [this]() {
55         m_qmlPreviewClient->triggerLoad(QUrl());
56     });
57 
58     connect(&m_connectTimer, &QTimer::timeout, this, &QmlPreviewApplication::tryToConnect);
59     connect(m_connection.data(), &QQmlDebugConnection::connected, &m_connectTimer, &QTimer::stop);
60 
61     connect(m_qmlPreviewClient.data(), &QQmlPreviewClient::error,
62             this, &QmlPreviewApplication::logError);
63     connect(m_qmlPreviewClient.data(), &QQmlPreviewClient::request,
64             this, &QmlPreviewApplication::serveRequest);
65 
66     connect(&m_watcher, &QmlPreviewFileSystemWatcher::fileChanged,
67             this, &QmlPreviewApplication::sendFile);
68     connect(&m_watcher, &QmlPreviewFileSystemWatcher::directoryChanged,
69             this, &QmlPreviewApplication::sendDirectory);
70 }
71 
~QmlPreviewApplication()72 QmlPreviewApplication::~QmlPreviewApplication()
73 {
74     if (m_process && m_process->state() != QProcess::NotRunning) {
75         logStatus("Terminating process ...");
76         m_process->disconnect();
77         m_process->terminate();
78         if (!m_process->waitForFinished(1000)) {
79             logStatus("Killing process ...");
80             m_process->kill();
81         }
82     }
83 }
84 
parseArguments()85 void 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         "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("Print debugging output."));
102     parser.addOption(verbose);
103 
104     parser.addHelpOption();
105     parser.addVersionOption();
106 
107     parser.addPositionalArgument(QLatin1String("executable"),
108                                  tr("The executable to be started and previewed."),
109                                  QLatin1String("[executable]"));
110     parser.addPositionalArgument(QLatin1String("parameters"),
111                                  tr("Parameters for the executable to be started."),
112                                  QLatin1String("[parameters...]"));
113 
114     parser.process(*this);
115 
116     QTemporaryFile file;
117     if (file.open())
118         m_socketFile = file.fileName();
119 
120     if (parser.isSet(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(tr("You have to specify an executable to start."));
129         parser.showHelp(2);
130     }
131 }
132 
exec()133 int QmlPreviewApplication::exec()
134 {
135     QTimer::singleShot(0, this, &QmlPreviewApplication::run);
136     return QCoreApplication::exec();
137 }
138 
run()139 void QmlPreviewApplication::run()
140 {
141     logStatus(QString("Listening on %1 ...").arg(m_socketFile));
142     m_connection->startLocalServer(m_socketFile);
143     m_process.reset(new QProcess(this));
144     QStringList arguments;
145     arguments << QString("-qmljsdebugger=file:%1,block,services:QmlPreview").arg(m_socketFile);
146     arguments << m_arguments;
147 
148     m_process->setProcessChannelMode(QProcess::MergedChannels);
149     connect(m_process.data(), &QIODevice::readyRead,
150             this, &QmlPreviewApplication::processHasOutput);
151     connect(m_process.data(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
152             this, [this](int){ processFinished(); });
153     logStatus(QString("Starting '%1 %2' ...").arg(m_executablePath, arguments.join(QLatin1Char(' '))));
154     m_process->start(m_executablePath, arguments);
155     if (!m_process->waitForStarted()) {
156         logError(QString("Could not run '%1': %2").arg(m_executablePath, m_process->errorString()));
157         exit(1);
158     }
159     m_connectTimer.start();
160 }
161 
tryToConnect()162 void 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(QString("No connection received on %1 for %2 seconds ...")
169                  .arg(m_socketFile).arg(m_connectionAttempts));
170     }
171 }
172 
processHasOutput()173 void 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 
processFinished()182 void QmlPreviewApplication::processFinished()
183 {
184     Q_ASSERT(m_process);
185     int exitCode = 0;
186     if (m_process->exitStatus() == QProcess::NormalExit) {
187         logStatus(QString("Process exited (%1).").arg(m_process->exitCode()));
188     } else {
189         logError("Process crashed!");
190         exitCode = 3;
191     }
192     exit(exitCode);
193 }
194 
logError(const QString & error)195 void QmlPreviewApplication::logError(const QString &error)
196 {
197     QTextStream err(stderr);
198     err << "Error: " << error << Qt::endl;
199 }
200 
logStatus(const QString & status)201 void QmlPreviewApplication::logStatus(const QString &status)
202 {
203     if (!m_verbose)
204         return;
205     QTextStream err(stderr);
206     err << status << Qt::endl;
207 }
208 
serveRequest(const QString & path)209 void QmlPreviewApplication::serveRequest(const QString &path)
210 {
211     QFileInfo info(path);
212 
213     if (info.isDir()) {
214         m_qmlPreviewClient->sendDirectory(path, QDir(path).entryList());
215         m_watcher.addDirectory(path);
216     } else {
217         QFile file(path);
218         if (file.open(QIODevice::ReadOnly)) {
219             m_qmlPreviewClient->sendFile(path, file.readAll());
220             m_watcher.addFile(path);
221         } else {
222             logStatus(QString("Could not open file %1 for reading: %2").arg(path)
223                       .arg(file.errorString()));
224             m_qmlPreviewClient->sendError(path);
225         }
226     }
227 }
228 
sendFile(const QString & path)229 bool QmlPreviewApplication::sendFile(const QString &path)
230 {
231     QFile file(path);
232     if (file.open(QIODevice::ReadOnly)) {
233         m_qmlPreviewClient->sendFile(path, 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(QString("Could not open file %1 for reading: %2").arg(path).arg(file.errorString()));
239     return false;
240 }
241 
sendDirectory(const QString & path)242 void QmlPreviewApplication::sendDirectory(const QString &path)
243 {
244     m_qmlPreviewClient->sendDirectory(path, QDir(path).entryList());
245     m_loadTimer.start();
246 }
247