aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorSemih Yavuz <semih.yavuz@qt.io>2026-06-02 11:37:42 +0200
committerSemih Yavuz <semih.yavuz@qt.io>2026-06-24 19:35:10 +0000
commit265c9250df853df1110ca4c2545b100481b98a2e (patch)
treeb3f2ac2a7078f1c6e8b1fdcd8a39c5e445bb70b4 /tools
parent895cb0a539472302ad22df98d4e11ddad0cf69f6 (diff)
qmlpreview CLI: handle hot reload failures
Enable qmlpreview to recover from hot reload failures. - Taking inspiration from QtC qmlpreview plugin, add infrastructure for recording and replaying user input events. - Refactor QmlPreviewApplication signal connections for clarity, extract process lifecycle management into startProcess, killProcess, restartProcess Pick-to: 6.12 Task-number: QTBUG-146122 Change-Id: Ib7237535b86ece0c5647834604170f9ba6ee0f29 Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
Diffstat (limited to 'tools')
-rw-r--r--tools/qmlpreview/qmlpreviewapplication.cpp102
-rw-r--r--tools/qmlpreview/qmlpreviewapplication.h9
2 files changed, 88 insertions, 23 deletions
diff --git a/tools/qmlpreview/qmlpreviewapplication.cpp b/tools/qmlpreview/qmlpreviewapplication.cpp
index 37a29d60e8..7a46716cf4 100644
--- a/tools/qmlpreview/qmlpreviewapplication.cpp
+++ b/tools/qmlpreview/qmlpreviewapplication.cpp
@@ -17,6 +17,18 @@
#include <QtCore/QFile>
#include <QtCore/QLibraryInfo>
+static QString qmlPreviewServices()
+{
+ return QStringLiteral("QmlPreview,CanvasFrameRate,EventReplay");
+}
+
+static QString makeQmlPreviewArgument(const QString &socketFile)
+{
+ return QString("-qmljsdebugger=file:%1,block,services:%2")
+ .arg(socketFile)
+ .arg(qmlPreviewServices());
+}
+
bool QmlPreviewApplication::argumentsFromCommandLineAndFile(
QStringList &allArguments, const QStringList &arguments)
{
@@ -54,25 +66,27 @@ QmlPreviewApplication::QmlPreviewApplication(int &argc, char **argv) :
{
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_loadTimer, &QTimer::timeout, this,
+ [this]() { m_qmlPreviewClient->loadUrl(QUrl()); });
+ m_connectTimer.setInterval(1000);
connect(&m_connectTimer, &QTimer::timeout, this, &QmlPreviewApplication::tryToConnect);
- connect(m_connection.data(), &QQmlDebugConnection::connected, &m_connectTimer, &QTimer::stop);
- connect(m_connection.data(), &QQmlDebugConnection::connected, this, [this]() {
- QQmlPreviewClient::Settings settings;
- settings.enableInPlaceUpdates = true;
- m_qmlPreviewClient->sendConfiguration(settings);
- const QString status = QString::fromUtf8("Inplace updates configuration: %1")
- .arg(settings.enableInPlaceUpdates ? "enabled" : "disabled");
- logStatus(status);
- });
+ connectConnectionSignals();
+ connectQmlPreviewClientSignals();
+ connectWatcherSignals();
+}
+
+QmlPreviewApplication::~QmlPreviewApplication()
+{
+ killProcess();
+}
+
+void QmlPreviewApplication::connectQmlPreviewClientSignals()
+{
connect(m_qmlPreviewClient.data(), &QQmlPreviewClient::error, this,
&QmlPreviewApplication::logError);
connect(m_qmlPreviewClient.data(), &QQmlPreviewClient::request, this,
@@ -84,13 +98,35 @@ QmlPreviewApplication::QmlPreviewApplication(int &argc, char **argv) :
.arg(m_confirmedSettings.enableInPlaceUpdates ? "enabled" : "disabled");
logStatus(status);
});
+ connect(m_qmlPreviewClient.data(), &QQmlPreviewClient::hotReloadFailure, this,
+ [this](const QString &reason) {
+ logError(QString::fromUtf8("Hot reload failure: %1").arg(reason));
+ restartProcess();
+ });
+}
+
+void QmlPreviewApplication::connectConnectionSignals()
+{
+ connect(m_connection.data(), &QQmlDebugConnection::connected, this, [this]() {
+ QQmlPreviewClient::Settings settings;
+ settings.enableInPlaceUpdates = true;
+ m_qmlPreviewClient->sendConfiguration(settings);
+ const QString status = QString::fromUtf8("Inplace updates configuration: %1")
+ .arg(settings.enableInPlaceUpdates ? "enabled" : "disabled");
+ logStatus(status);
+ m_connectTimer.stop();
+ });
+}
+
+void QmlPreviewApplication::connectWatcherSignals()
+{
connect(&m_watcher, &QmlPreviewFileSystemWatcher::fileChanged,
this, &QmlPreviewApplication::sendFile);
connect(&m_watcher, &QmlPreviewFileSystemWatcher::directoryChanged,
this, &QmlPreviewApplication::sendDirectory);
}
-QmlPreviewApplication::~QmlPreviewApplication()
+void QmlPreviewApplication::killProcess()
{
if (m_process && m_process->state() != QProcess::NotRunning) {
logStatus("Terminating process ...");
@@ -103,6 +139,23 @@ QmlPreviewApplication::~QmlPreviewApplication()
}
}
+void QmlPreviewApplication::restartProcess()
+{
+ logStatus(QString::fromUtf8("Restarting process ..."));
+
+ if (m_process) {
+ disconnect(m_process.data(), nullptr, this, nullptr);
+ }
+
+ killProcess();
+
+ // reset the connection
+ m_connection->close();
+ m_connectionAttempts = 0;
+ m_connection->startLocalServer(m_socketFile);
+ startProcess();
+}
+
void QmlPreviewApplication::parseArguments()
{
setApplicationName(QLatin1String("qmlpreview"));
@@ -167,22 +220,18 @@ int QmlPreviewApplication::exec()
return QCoreApplication::exec();
}
-void QmlPreviewApplication::run()
+void QmlPreviewApplication::startProcess()
{
- 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);
- arguments << m_arguments;
-
m_process->setProcessChannelMode(QProcess::MergedChannels);
connect(m_process.data(), &QIODevice::readyRead,
this, &QmlPreviewApplication::processHasOutput);
connect(m_process.data(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, [this](int){ processFinished(); });
- logStatus(QString("Starting '%1 %2' ...").arg(m_executablePath, arguments.join(QLatin1Char(' '))));
- m_process->start(m_executablePath, arguments);
+ m_arguments.append(makeQmlPreviewArgument(m_socketFile));
+ logStatus(QString("Starting '%1 %2' ...")
+ .arg(m_executablePath, m_arguments.join(QLatin1Char(' '))));
+ m_process->start(m_executablePath, m_arguments);
if (!m_process->waitForStarted()) {
logError(QString("Could not run '%1': %2").arg(m_executablePath, m_process->errorString()));
exit(1);
@@ -190,6 +239,13 @@ void QmlPreviewApplication::run()
m_connectTimer.start();
}
+void QmlPreviewApplication::run()
+{
+ logStatus(QString("Listening on %1 ...").arg(m_socketFile));
+ m_connection->startLocalServer(m_socketFile);
+ startProcess();
+}
+
void QmlPreviewApplication::tryToConnect()
{
Q_ASSERT(!m_connection->isConnected());
diff --git a/tools/qmlpreview/qmlpreviewapplication.h b/tools/qmlpreview/qmlpreviewapplication.h
index 017974bfcd..09f26a3e73 100644
--- a/tools/qmlpreview/qmlpreviewapplication.h
+++ b/tools/qmlpreview/qmlpreviewapplication.h
@@ -42,6 +42,14 @@ private:
bool sendFile(const QString &path);
void sendDirectory(const QString &path);
+ void connectQmlPreviewClientSignals();
+ void connectConnectionSignals();
+ void connectWatcherSignals();
+
+ void killProcess();
+ void restartProcess();
+ void startProcess();
+
QString m_executablePath;
QStringList m_arguments;
QScopedPointer<QProcess> m_process;
@@ -53,6 +61,7 @@ private:
QHash<QString, QString> m_localToResourcePath;
QScopedPointer<QQmlDebugConnection> m_connection;
QScopedPointer<QQmlPreviewClient> m_qmlPreviewClient;
+ QScopedPointer<QQmlProfilerEventReceiver> m_eventReceiver;
QmlPreviewFileSystemWatcher m_watcher;
QTimer m_loadTimer;