2022-05-13 13:12:05 +00:00
|
|
|
// Copyright (C) 2018 The Qt Company Ltd.
|
|
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
2018-07-10 07:39:16 +00:00
|
|
|
|
|
|
|
#include "qmlpreviewapplication.h"
|
|
|
|
|
|
|
|
#include <QtCore/QStringList>
|
|
|
|
#include <QtCore/QTextStream>
|
|
|
|
#include <QtCore/QProcess>
|
|
|
|
#include <QtCore/QTimer>
|
|
|
|
#include <QtCore/QDateTime>
|
|
|
|
#include <QtCore/QFileInfo>
|
|
|
|
#include <QtCore/QDebug>
|
|
|
|
#include <QtCore/QDir>
|
|
|
|
#include <QtCore/QCommandLineParser>
|
|
|
|
#include <QtCore/QTemporaryFile>
|
|
|
|
#include <QtCore/QUrl>
|
2022-08-30 09:15:44 +00:00
|
|
|
#include <QtCore/QLibraryInfo>
|
2018-07-10 07:39:16 +00:00
|
|
|
|
|
|
|
QmlPreviewApplication::QmlPreviewApplication(int &argc, char **argv) :
|
|
|
|
QCoreApplication(argc, argv),
|
|
|
|
m_verbose(false),
|
|
|
|
m_connectionAttempts(0)
|
|
|
|
{
|
|
|
|
m_connection.reset(new QQmlDebugConnection);
|
|
|
|
m_qmlPreviewClient.reset(new QQmlPreviewClient(m_connection.data()));
|
|
|
|
m_connectTimer.setInterval(1000);
|
|
|
|
|
|
|
|
m_loadTimer.setInterval(100);
|
|
|
|
m_loadTimer.setSingleShot(true);
|
|
|
|
connect(&m_loadTimer, &QTimer::timeout, this, [this]() {
|
|
|
|
m_qmlPreviewClient->triggerLoad(QUrl());
|
|
|
|
});
|
|
|
|
|
|
|
|
connect(&m_connectTimer, &QTimer::timeout, this, &QmlPreviewApplication::tryToConnect);
|
|
|
|
connect(m_connection.data(), &QQmlDebugConnection::connected, &m_connectTimer, &QTimer::stop);
|
|
|
|
|
|
|
|
connect(m_qmlPreviewClient.data(), &QQmlPreviewClient::error,
|
|
|
|
this, &QmlPreviewApplication::logError);
|
|
|
|
connect(m_qmlPreviewClient.data(), &QQmlPreviewClient::request,
|
|
|
|
this, &QmlPreviewApplication::serveRequest);
|
|
|
|
|
2018-11-13 13:22:59 +00:00
|
|
|
connect(&m_watcher, &QmlPreviewFileSystemWatcher::fileChanged,
|
2018-07-10 07:39:16 +00:00
|
|
|
this, &QmlPreviewApplication::sendFile);
|
2018-11-13 13:22:59 +00:00
|
|
|
connect(&m_watcher, &QmlPreviewFileSystemWatcher::directoryChanged,
|
2018-07-10 07:39:16 +00:00
|
|
|
this, &QmlPreviewApplication::sendDirectory);
|
|
|
|
}
|
|
|
|
|
|
|
|
QmlPreviewApplication::~QmlPreviewApplication()
|
|
|
|
{
|
|
|
|
if (m_process && m_process->state() != QProcess::NotRunning) {
|
|
|
|
logStatus("Terminating process ...");
|
|
|
|
m_process->disconnect();
|
|
|
|
m_process->terminate();
|
|
|
|
if (!m_process->waitForFinished(1000)) {
|
|
|
|
logStatus("Killing process ...");
|
|
|
|
m_process->kill();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void QmlPreviewApplication::parseArguments()
|
|
|
|
{
|
|
|
|
setApplicationName(QLatin1String("qmlpreview"));
|
|
|
|
setApplicationVersion(QLatin1String(qVersion()));
|
|
|
|
|
|
|
|
QCommandLineParser parser;
|
|
|
|
parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
|
|
|
|
parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsPositionalArguments);
|
|
|
|
|
|
|
|
parser.setApplicationDescription(QChar::LineFeed + tr(
|
|
|
|
"The QML Preview tool watches QML and JavaScript files on disk and updates\n"
|
|
|
|
"the application live with any changes. The application to be previewed\n"
|
|
|
|
"has to enable QML debugging. See the Qt Creator documentation on how to do\n"
|
|
|
|
"this for different Qt versions."));
|
|
|
|
|
|
|
|
QCommandLineOption verbose(QStringList() << QLatin1String("verbose"),
|
|
|
|
tr("Print debugging output."));
|
|
|
|
parser.addOption(verbose);
|
|
|
|
|
|
|
|
parser.addHelpOption();
|
|
|
|
parser.addVersionOption();
|
|
|
|
|
2018-11-14 08:53:21 +00:00
|
|
|
parser.addPositionalArgument(QLatin1String("executable"),
|
|
|
|
tr("The executable to be started and previewed."),
|
|
|
|
QLatin1String("[executable]"));
|
2018-07-10 07:39:16 +00:00
|
|
|
parser.addPositionalArgument(QLatin1String("parameters"),
|
2018-11-14 08:53:21 +00:00
|
|
|
tr("Parameters for the executable to be started."),
|
2018-07-10 07:39:16 +00:00
|
|
|
QLatin1String("[parameters...]"));
|
|
|
|
|
|
|
|
parser.process(*this);
|
|
|
|
|
|
|
|
QTemporaryFile file;
|
|
|
|
if (file.open())
|
|
|
|
m_socketFile = file.fileName();
|
|
|
|
|
|
|
|
if (parser.isSet(verbose))
|
|
|
|
m_verbose = true;
|
|
|
|
|
2018-11-14 08:53:21 +00:00
|
|
|
m_arguments = parser.positionalArguments();
|
|
|
|
if (!m_arguments.isEmpty())
|
|
|
|
m_executablePath = m_arguments.takeFirst();
|
2018-07-10 07:39:16 +00:00
|
|
|
|
2018-11-14 08:53:21 +00:00
|
|
|
if (m_executablePath.isEmpty()) {
|
|
|
|
logError(tr("You have to specify an executable to start."));
|
2018-07-10 07:39:16 +00:00
|
|
|
parser.showHelp(2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int QmlPreviewApplication::exec()
|
|
|
|
{
|
|
|
|
QTimer::singleShot(0, this, &QmlPreviewApplication::run);
|
|
|
|
return QCoreApplication::exec();
|
|
|
|
}
|
|
|
|
|
|
|
|
void QmlPreviewApplication::run()
|
|
|
|
{
|
|
|
|
logStatus(QString("Listening on %1 ...").arg(m_socketFile));
|
|
|
|
m_connection->startLocalServer(m_socketFile);
|
|
|
|
m_process.reset(new QProcess(this));
|
|
|
|
QStringList arguments;
|
|
|
|
arguments << QString("-qmljsdebugger=file:%1,block,services:QmlPreview").arg(m_socketFile);
|
2018-11-14 08:53:21 +00:00
|
|
|
arguments << m_arguments;
|
2018-07-10 07:39:16 +00:00
|
|
|
|
|
|
|
m_process->setProcessChannelMode(QProcess::MergedChannels);
|
|
|
|
connect(m_process.data(), &QIODevice::readyRead,
|
|
|
|
this, &QmlPreviewApplication::processHasOutput);
|
2019-02-05 11:30:10 +00:00
|
|
|
connect(m_process.data(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
2018-07-10 07:39:16 +00:00
|
|
|
this, [this](int){ processFinished(); });
|
2018-11-14 08:53:21 +00:00
|
|
|
logStatus(QString("Starting '%1 %2' ...").arg(m_executablePath, arguments.join(QLatin1Char(' '))));
|
|
|
|
m_process->start(m_executablePath, arguments);
|
2018-07-10 07:39:16 +00:00
|
|
|
if (!m_process->waitForStarted()) {
|
2018-11-14 08:53:21 +00:00
|
|
|
logError(QString("Could not run '%1': %2").arg(m_executablePath, m_process->errorString()));
|
2018-07-10 07:39:16 +00:00
|
|
|
exit(1);
|
|
|
|
}
|
|
|
|
m_connectTimer.start();
|
|
|
|
}
|
|
|
|
|
|
|
|
void QmlPreviewApplication::tryToConnect()
|
|
|
|
{
|
|
|
|
Q_ASSERT(!m_connection->isConnected());
|
|
|
|
++m_connectionAttempts;
|
|
|
|
|
|
|
|
if (m_verbose && !(m_connectionAttempts % 5)) {// print every 5 seconds
|
|
|
|
logError(QString("No connection received on %1 for %2 seconds ...")
|
|
|
|
.arg(m_socketFile).arg(m_connectionAttempts));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void QmlPreviewApplication::processHasOutput()
|
|
|
|
{
|
|
|
|
Q_ASSERT(m_process);
|
|
|
|
while (m_process->bytesAvailable()) {
|
|
|
|
QTextStream out(stderr);
|
|
|
|
out << m_process->readAll();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void QmlPreviewApplication::processFinished()
|
|
|
|
{
|
|
|
|
Q_ASSERT(m_process);
|
|
|
|
int exitCode = 0;
|
|
|
|
if (m_process->exitStatus() == QProcess::NormalExit) {
|
|
|
|
logStatus(QString("Process exited (%1).").arg(m_process->exitCode()));
|
|
|
|
} else {
|
|
|
|
logError("Process crashed!");
|
|
|
|
exitCode = 3;
|
|
|
|
}
|
|
|
|
exit(exitCode);
|
|
|
|
}
|
|
|
|
|
|
|
|
void QmlPreviewApplication::logError(const QString &error)
|
|
|
|
{
|
|
|
|
QTextStream err(stderr);
|
2019-06-26 14:46:23 +00:00
|
|
|
err << "Error: " << error << Qt::endl;
|
2018-07-10 07:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void QmlPreviewApplication::logStatus(const QString &status)
|
|
|
|
{
|
|
|
|
if (!m_verbose)
|
|
|
|
return;
|
|
|
|
QTextStream err(stderr);
|
2019-06-26 14:46:23 +00:00
|
|
|
err << status << Qt::endl;
|
2018-07-10 07:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void QmlPreviewApplication::serveRequest(const QString &path)
|
|
|
|
{
|
|
|
|
QFileInfo info(path);
|
|
|
|
|
|
|
|
if (info.isDir()) {
|
|
|
|
m_qmlPreviewClient->sendDirectory(path, QDir(path).entryList());
|
2018-11-13 13:22:59 +00:00
|
|
|
m_watcher.addDirectory(path);
|
2018-07-10 07:39:16 +00:00
|
|
|
} else {
|
|
|
|
QFile file(path);
|
|
|
|
if (file.open(QIODevice::ReadOnly)) {
|
|
|
|
m_qmlPreviewClient->sendFile(path, file.readAll());
|
2018-11-13 13:22:59 +00:00
|
|
|
m_watcher.addFile(path);
|
2018-07-10 07:39:16 +00:00
|
|
|
} else {
|
|
|
|
logStatus(QString("Could not open file %1 for reading: %2").arg(path)
|
|
|
|
.arg(file.errorString()));
|
|
|
|
m_qmlPreviewClient->sendError(path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool QmlPreviewApplication::sendFile(const QString &path)
|
|
|
|
{
|
|
|
|
QFile file(path);
|
|
|
|
if (file.open(QIODevice::ReadOnly)) {
|
|
|
|
m_qmlPreviewClient->sendFile(path, file.readAll());
|
|
|
|
// Defer the Load, because files tend to change multiple times in a row.
|
|
|
|
m_loadTimer.start();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
logStatus(QString("Could not open file %1 for reading: %2").arg(path).arg(file.errorString()));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void QmlPreviewApplication::sendDirectory(const QString &path)
|
|
|
|
{
|
|
|
|
m_qmlPreviewClient->sendDirectory(path, QDir(path).entryList());
|
|
|
|
m_loadTimer.start();
|
|
|
|
}
|