diff options
| author | Ulf Hermann <ulf.hermann@qt.io> | 2026-07-14 10:12:35 +0000 |
|---|---|---|
| committer | Ulf Hermann <ulf.hermann@qt.io> | 2026-07-19 17:41:52 +0000 |
| commit | ba02327bdfecd1d19653584886007c5d97e3347e (patch) | |
| tree | 2bbde125eead00ed3bdd23b52462ee6351fbe8a3 /tools | |
| parent | 0257d10b4822786e07e8ea3d9ac743359697ed9a (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.txt | 2 | ||||
| -rw-r--r-- | tools/qmlprofiler/commandlistener.cpp | 13 | ||||
| -rw-r--r-- | tools/qmlprofiler/commandlistener.h | 18 | ||||
| -rw-r--r-- | tools/qmlprofiler/constants.h | 32 | ||||
| -rw-r--r-- | tools/qmlprofiler/main.cpp | 16 | ||||
| -rw-r--r-- | tools/qmlprofiler/qmlprofilerapplication.cpp | 212 | ||||
| -rw-r--r-- | tools/qmlprofiler/qmlprofilerapplication.h | 13 |
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; }; |
