aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorUlf Hermann <ulf.hermann@qt.io>2026-07-14 10:12:35 +0000
committerUlf Hermann <ulf.hermann@qt.io>2026-07-19 17:41:52 +0000
commitba02327bdfecd1d19653584886007c5d97e3347e (patch)
tree2bbde125eead00ed3bdd23b52462ee6351fbe8a3 /tools
parent0257d10b4822786e07e8ea3d9ac743359697ed9a (diff)
QmlDebug: Add a reusable interactive console
Factor the interactive command-line mechanism that was open-coded in qmlprofiler into QmlDebugPrivate so that other QML debugging tools can reuse it. We move qmlprofiler's console handling into QmlDebug: QQmlDebugConsole owns a QQmlDebugCommandListener on a worker thread, dispatches lines to registered command handlers, renders a generated help listing and offers a reusable yes/no confirmation flow. The listener keeps a single persistent QTextStream and reports end-of-input so consumers can quit when standard input is closed. Pick-to: 6.12 Task-number: QTBUG-146122 Change-Id: I237dce26026fd520cbab25efcf5ce08d25507c92 Reviewed-by: Semih Yavuz <semih.yavuz@qt.io>
Diffstat (limited to 'tools')
-rw-r--r--tools/qmlprofiler/CMakeLists.txt2
-rw-r--r--tools/qmlprofiler/commandlistener.cpp13
-rw-r--r--tools/qmlprofiler/commandlistener.h18
-rw-r--r--tools/qmlprofiler/constants.h32
-rw-r--r--tools/qmlprofiler/main.cpp16
-rw-r--r--tools/qmlprofiler/qmlprofilerapplication.cpp212
-rw-r--r--tools/qmlprofiler/qmlprofilerapplication.h13
7 files changed, 105 insertions, 201 deletions
diff --git a/tools/qmlprofiler/CMakeLists.txt b/tools/qmlprofiler/CMakeLists.txt
index ca4c6e1229..7fad07a908 100644
--- a/tools/qmlprofiler/CMakeLists.txt
+++ b/tools/qmlprofiler/CMakeLists.txt
@@ -12,8 +12,6 @@ qt_internal_add_tool(${target_name}
TARGET_DESCRIPTION "QML Profiler"
TOOLS_TARGET Qml # special case
SOURCES
- commandlistener.cpp commandlistener.h
- constants.h
main.cpp
qmlprofilerapplication.cpp qmlprofilerapplication.h
qmlprofilerclient.cpp qmlprofilerclient.h
diff --git a/tools/qmlprofiler/commandlistener.cpp b/tools/qmlprofiler/commandlistener.cpp
deleted file mode 100644
index 3d1fc2d311..0000000000
--- a/tools/qmlprofiler/commandlistener.cpp
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright (C) 2016 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
-
-#include "commandlistener.h"
-#include "constants.h"
-#include <QtCore/QTextStream>
-
-void CommandListener::readCommand()
-{
- emit command(QTextStream(stdin).readLine());
-}
-
-#include "moc_commandlistener.cpp"
diff --git a/tools/qmlprofiler/commandlistener.h b/tools/qmlprofiler/commandlistener.h
deleted file mode 100644
index 65ba3682bd..0000000000
--- a/tools/qmlprofiler/commandlistener.h
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (C) 2016 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
-
-#ifndef COMMANDLISTENER_H
-#define COMMANDLISTENER_H
-
-#include <QtCore/QThread>
-
-class CommandListener : public QObject {
- Q_OBJECT
-public:
- void readCommand();
-
-Q_SIGNALS:
- void command(const QString &command);
-};
-
-#endif // COMMANDLISTENER_H
diff --git a/tools/qmlprofiler/constants.h b/tools/qmlprofiler/constants.h
deleted file mode 100644
index 6fdcbd8c82..0000000000
--- a/tools/qmlprofiler/constants.h
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2016 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
-
-#ifndef CONSTANTS_H
-#define CONSTANTS_H
-
-namespace Constants {
-
-const char CMD_RECORD[] ="record";
-const char CMD_RECORD2[] ="r";
-
-const char CMD_OUTPUT[] = "output";
-const char CMD_OUTPUT2[] = "o";
-
-const char CMD_FLUSH[] = "flush";
-const char CMD_FLUSH2[] = "f";
-
-const char CMD_CLEAR[] = "clear";
-const char CMD_CLEAR2[] = "c";
-
-const char CMD_QUIT[] ="quit";
-const char CMD_QUIT2[] = "q";
-
-const char CMD_YES[] = "yes";
-const char CMD_YES2[] = "y";
-
-const char CMD_NO[] = "no";
-const char CMD_NO2[] = "n";
-
-} // Constants
-
-#endif // CONSTANTS_H
diff --git a/tools/qmlprofiler/main.cpp b/tools/qmlprofiler/main.cpp
index e89eecbef2..e5aad2babe 100644
--- a/tools/qmlprofiler/main.cpp
+++ b/tools/qmlprofiler/main.cpp
@@ -1,7 +1,6 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
-#include "commandlistener.h"
#include "qmlprofilerapplication.h"
int main(int argc, char *argv[])
@@ -11,19 +10,8 @@ int main(int argc, char *argv[])
app.parseArguments();
if (app.isInteractive()) {
- QThread listenerThread;
- CommandListener listener;
- listener.moveToThread(&listenerThread);
- QObject::connect(&listener, &CommandListener::command,
- &app, &QmlProfilerApplication::userCommand);
- QObject::connect(&app, &QmlProfilerApplication::readyForCommand,
- &listener, &CommandListener::readCommand);
- listenerThread.start();
- int exitValue = app.exec();
- listenerThread.quit();
- // wait for listener to exit
- listenerThread.wait();
- return exitValue;
+ app.startConsole();
+ return app.exec();
} else {
int exitValue = app.exec();
app.outputData();
diff --git a/tools/qmlprofiler/qmlprofilerapplication.cpp b/tools/qmlprofiler/qmlprofilerapplication.cpp
index ff02519712..97960219da 100644
--- a/tools/qmlprofiler/qmlprofilerapplication.cpp
+++ b/tools/qmlprofiler/qmlprofilerapplication.cpp
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "qmlprofilerapplication.h"
-#include "constants.h"
#include <QtCore/QStringList>
#include <QtCore/QProcess>
#include <QtCore/QTimer>
@@ -17,22 +16,7 @@
#include <iostream>
-static const char commandTextC[] =
- "The following commands are available:\n"
- "'r', 'record'\n"
- " Switch recording on or off.\n"
- "'o [file]', 'output [file]'\n"
- " Output profiling data to <file>. If no <file>\n"
- " parameter is given, output to whatever was given\n"
- " with --output, or standard output.\n"
- "'c', 'clear'\n"
- " Clear profiling data recorded so far from memory.\n"
- "'f [file]', 'flush [file]'\n"
- " Stop recording if it is running, then output the\n"
- " data, and finally clear it from memory.\n"
- "'q', 'quit'\n"
- " Terminate the target process if started from\n"
- " qmlprofiler, and qmlprofiler itself.";
+using namespace Qt::StringLiterals;
static const char *features[] = {
"javascript",
@@ -87,6 +71,10 @@ QmlProfilerApplication::QmlProfilerApplication(int &argc, char **argv) :
connect(m_profilerData.data(), &QQmlProfilerEventReceiver::dataReady,
this, &QmlProfilerApplication::traceFinished);
+ registerCommands();
+
+ // Closing standard input ends the interactive session.
+ connect(&m_console, &QQmlDebugConsole::endOfInput, this, &QCoreApplication::quit);
}
QmlProfilerApplication::~QmlProfilerApplication()
@@ -167,7 +155,8 @@ void QmlProfilerApplication::parseArguments()
QCommandLineOption interactive(QLatin1String("interactive"),
tr("Manually control the recording from the command line. The "
"profiler will not terminate itself when the application "
- "does so in this case.") + QChar::Space + tr(commandTextC));
+ "does so in this case. Type 'help' at the prompt to see the "
+ "list of available commands."));
parser.addOption(interactive);
QCommandLineOption verbose(QStringList() << QLatin1String("verbose"),
@@ -256,6 +245,11 @@ bool QmlProfilerApplication::isInteractive() const
return m_interactive;
}
+void QmlProfilerApplication::startConsole()
+{
+ m_console.start();
+}
+
quint64 QmlProfilerApplication::parseFeatures(const QStringList &featureList, const QString &values,
bool exclude)
{
@@ -312,105 +306,100 @@ void QmlProfilerApplication::output()
m_pendingRequest = REQUEST_NONE;
}
-bool QmlProfilerApplication::checkOutputFile(PendingRequest pending)
+void QmlProfilerApplication::confirmOverwriteThen(std::function<void()> action)
{
- if (m_interactiveOutputFile.isEmpty())
- return true;
- QFileInfo file(m_interactiveOutputFile);
- if (file.exists()) {
- if (!file.isFile()) {
- prompt(tr("Cannot overwrite %1.").arg(m_interactiveOutputFile));
- m_interactiveOutputFile.clear();
- } else {
- prompt(tr("%1 exists. Overwrite (y/n)?").arg(m_interactiveOutputFile));
- m_pendingRequest = pending;
- }
- return false;
- } else {
- return true;
- }
-}
-
-void QmlProfilerApplication::userCommand(const QString &command)
-{
- auto args = QStringView{command}.split(QChar::Space, Qt::SkipEmptyParts);
- if (args.isEmpty()) {
- prompt();
+ if (m_interactiveOutputFile.isEmpty()) {
+ action();
return;
}
- QByteArray cmd = args.takeFirst().trimmed().toLatin1();
-
- if (m_pendingRequest == REQUEST_QUIT) {
- if (cmd == Constants::CMD_YES || cmd == Constants::CMD_YES2) {
- quit();
- } else if (cmd == Constants::CMD_NO || cmd == Constants::CMD_NO2) {
- m_pendingRequest = REQUEST_NONE;
- prompt();
- } else {
- prompt(tr("Really quit (y/n)?"));
- }
+ const QFileInfo info(m_interactiveOutputFile);
+ if (!info.exists()) {
+ action();
return;
}
- if (m_pendingRequest == REQUEST_OUTPUT_FILE || m_pendingRequest == REQUEST_FLUSH_FILE) {
- if (cmd == Constants::CMD_YES || cmd == Constants::CMD_YES2) {
- if (m_pendingRequest == REQUEST_OUTPUT_FILE)
- output();
- else
- flush();
- } else if (cmd == Constants::CMD_NO || cmd == Constants::CMD_NO2) {
- m_pendingRequest = REQUEST_NONE;
- m_interactiveOutputFile.clear();
- prompt();
- } else {
- prompt(tr("%1 exists. Overwrite (y/n)?"));
- }
+ if (!info.isFile()) {
+ prompt(tr("Cannot overwrite %1.").arg(m_interactiveOutputFile));
+ m_interactiveOutputFile.clear();
return;
}
- if (cmd == Constants::CMD_RECORD || cmd == Constants::CMD_RECORD2) {
- m_pendingRequest = REQUEST_TOGGLE_RECORDING;
- m_qmlProfilerClient->setRecording(!m_recording);
- } else if (cmd == Constants::CMD_QUIT || cmd == Constants::CMD_QUIT2) {
- m_pendingRequest = REQUEST_QUIT;
- if (m_recording) {
- prompt(tr("The application is still generating data. Really quit (y/n)?"));
- } else if (!m_profilerData->isEmpty()) {
- prompt(tr("There is still trace data in memory. Really quit (y/n)?"));
- } else {
- quit();
- }
- } else if (cmd == Constants::CMD_OUTPUT || cmd == Constants::CMD_OUTPUT2) {
- if (m_recording) {
- prompt(tr("Cannot output while recording data."));
- } else if (m_profilerData->isEmpty()) {
- prompt(tr("No data was recorded so far."));
- } else {
- m_interactiveOutputFile = args.size() > 0 ? args.at(0).toString() : m_outputFile;
- if (checkOutputFile(REQUEST_OUTPUT_FILE))
- output();
- }
- } else if (cmd == Constants::CMD_CLEAR || cmd == Constants::CMD_CLEAR2) {
- if (m_recording) {
- prompt(tr("Cannot clear data while recording."));
- } else if (m_profilerData->isEmpty()) {
- prompt(tr("No data was recorded so far."));
- } else {
- m_profilerData->clear();
- prompt(tr("Trace data cleared."));
- }
- } else if (cmd == Constants::CMD_FLUSH || cmd == Constants::CMD_FLUSH2) {
- if (!m_recording && m_profilerData->isEmpty()) {
- prompt(tr("No data was recorded so far."));
- } else {
- m_interactiveOutputFile = args.size() > 0 ? args.at(0).toString() : m_outputFile;
- if (checkOutputFile(REQUEST_FLUSH_FILE))
- flush();
- }
- } else {
- prompt(tr(commandTextC));
- }
+ m_console.askConfirmation(tr("%1 exists. Overwrite (y/n)?").arg(m_interactiveOutputFile),
+ std::move(action), [this]() { m_interactiveOutputFile.clear(); });
+}
+
+void QmlProfilerApplication::registerCommands()
+{
+ m_console.registerCommand({ "record"_L1, "r"_L1 }, {}, tr("Switch recording on or off."),
+ [this](const QStringList &) {
+ if (!m_connection->isConnected()) {
+ prompt(tr("Not connected to a running application. "
+ "Cannot change the recording state."));
+ return;
+ }
+ m_pendingRequest = REQUEST_TOGGLE_RECORDING;
+ m_qmlProfilerClient->setRecording(!m_recording);
+ });
+
+ m_console.registerCommand(
+ { "quit"_L1, "q"_L1 }, {},
+ tr("Terminate the target process if started from qmlprofiler, and "
+ "qmlprofiler itself."),
+ [this](const QStringList &) {
+ if (m_recording) {
+ m_console.askConfirmation(
+ tr("The application is still generating data. Really quit (y/n)?"),
+ [] { quit(); });
+ } else if (!m_profilerData->isEmpty()) {
+ m_console.askConfirmation(
+ tr("There is still trace data in memory. Really quit (y/n)?"),
+ [] { quit(); });
+ } else {
+ quit();
+ }
+ });
+
+ m_console.registerCommand(
+ { "output"_L1, "o"_L1 }, "[file]"_L1,
+ tr("Output profiling data to <file>. If no <file> parameter is given, "
+ "output to whatever was given with --output, or standard output."),
+ [this](const QStringList &args) {
+ if (m_recording) {
+ prompt(tr("Cannot output while recording data."));
+ } else if (m_profilerData->isEmpty()) {
+ prompt(tr("No data was recorded so far."));
+ } else {
+ m_interactiveOutputFile = args.isEmpty() ? m_outputFile : args.first();
+ confirmOverwriteThen([this] { output(); });
+ }
+ });
+
+ m_console.registerCommand({ "clear"_L1, "c"_L1 }, {},
+ tr("Clear profiling data recorded so far from memory."),
+ [this](const QStringList &) {
+ if (m_recording) {
+ prompt(tr("Cannot clear data while recording."));
+ } else if (m_profilerData->isEmpty()) {
+ prompt(tr("No data was recorded so far."));
+ } else {
+ m_profilerData->clear();
+ prompt(tr("Trace data cleared."));
+ }
+ });
+
+ m_console.registerCommand(
+ { "flush"_L1, "f"_L1 }, "[file]"_L1,
+ tr("Stop recording if it is running, then output the data, and finally "
+ "clear it from memory."),
+ [this](const QStringList &args) {
+ if (!m_recording && m_profilerData->isEmpty()) {
+ prompt(tr("No data was recorded so far."));
+ } else {
+ m_interactiveOutputFile = args.isEmpty() ? m_outputFile : args.first();
+ confirmOverwriteThen([this] { flush(); });
+ }
+ });
}
void QmlProfilerApplication::notifyTraceStarted()
@@ -568,13 +557,8 @@ void QmlProfilerApplication::traceFinished()
void QmlProfilerApplication::prompt(const QString &line, bool ready)
{
- if (m_interactive) {
- if (!line.isEmpty())
- std::cerr << qPrintable(line) << std::endl;
- std::cerr << "> ";
- if (ready)
- emit readyForCommand();
- }
+ if (m_interactive)
+ m_console.prompt(line, ready);
}
void QmlProfilerApplication::logError(const QString &error)
diff --git a/tools/qmlprofiler/qmlprofilerapplication.h b/tools/qmlprofiler/qmlprofilerapplication.h
index 42e750e441..9a00cd8318 100644
--- a/tools/qmlprofiler/qmlprofilerapplication.h
+++ b/tools/qmlprofiler/qmlprofilerapplication.h
@@ -7,6 +7,7 @@
#include "qmlprofilerclient.h"
#include <private/qqmldebugconnection_p.h>
+#include <private/qqmldebugconsole_p.h>
#include <private/qqmlprofilereventreceiver_p.h>
#include <QtCore/qcoreapplication.h>
@@ -15,10 +16,7 @@
#include <QtNetwork/qabstractsocket.h>
enum PendingRequest {
- REQUEST_QUIT,
- REQUEST_FLUSH_FILE,
REQUEST_FLUSH,
- REQUEST_OUTPUT_FILE,
REQUEST_TOGGLE_RECORDING,
REQUEST_NONE
};
@@ -33,13 +31,10 @@ public:
void parseArguments();
int exec();
bool isInteractive() const;
- void userCommand(const QString &command);
+ void startConsole();
void notifyTraceStarted();
void outputData();
-Q_SIGNALS:
- void readyForCommand();
-
private:
void run();
void tryToConnect();
@@ -51,13 +46,14 @@ private:
void traceClientEnabledChanged(bool enabled);
void traceFinished();
+ void registerCommands();
void prompt(const QString &line = QString(), bool ready = true);
void logError(const QString &error);
void logWarning(const QString &warning);
void logStatus(const QString &status);
quint64 parseFeatures(const QStringList &featureList, const QString &values, bool exclude);
- bool checkOutputFile(PendingRequest pending);
+ void confirmOverwriteThen(std::function<void()> action);
void flush();
void output();
@@ -85,6 +81,7 @@ private:
QScopedPointer<QQmlDebugConnection> m_connection;
QScopedPointer<QmlProfilerClient> m_qmlProfilerClient;
QScopedPointer<QQmlProfilerEventReceiver> m_profilerData;
+ QQmlDebugConsole m_console;
QTimer m_connectTimer;
uint m_connectionAttempts;
};