From 77a140c58114f1abdf37b3c74309d3ad32257766 Mon Sep 17 00:00:00 2001 From: Jarek Kobus Date: Fri, 7 Oct 2022 12:05:37 +0200 Subject: [PATCH] QtcProcess: Introduce TextChannelMode and textOnChannel signals This is alternative to setStd[Out/Err](Line)Callback() methods. In this way there may be many clients connected to textOnStandard[Output/Error]() signals. This should also simplify handling the lifetime of user callback. Change-Id: If82baa1f3f9c432ed431926619b9bbf11d770a84 Reviewed-by: Reviewed-by: hjk --- src/libs/utils/processenums.h | 16 +++++ src/libs/utils/qtcprocess.cpp | 51 +++++++++++++-- src/libs/utils/qtcprocess.h | 13 ++-- .../auto/utils/qtcprocess/tst_qtcprocess.cpp | 63 ++++++++++++++++++- 4 files changed, 134 insertions(+), 9 deletions(-) diff --git a/src/libs/utils/processenums.h b/src/libs/utils/processenums.h index 661f2c61ff7..8091dabeefa 100644 --- a/src/libs/utils/processenums.h +++ b/src/libs/utils/processenums.h @@ -37,6 +37,21 @@ enum class EventLoopMode { On // Avoid }; +enum class Channel { + Output, + Error +}; + +enum class TextChannelMode { + // Keep | Emit | Emit + // raw | text | content + // data | sig | + // -----+------+-------- + Off, // yes | no | - + SingleLine, // no | yes | Single lines + MultiLine // yes | yes | All the available data +}; + enum class ProcessResult { // Finished successfully. Unless an ExitCodeInterpreter is set // this corresponds to a return code 0. @@ -53,6 +68,7 @@ enum class ProcessResult { }; using ExitCodeInterpreter = std::function; +using TextChannelCallback = std::function; } // namespace Utils diff --git a/src/libs/utils/qtcprocess.cpp b/src/libs/utils/qtcprocess.cpp index 18abb0de20f..7ddce617637 100644 --- a/src/libs/utils/qtcprocess.cpp +++ b/src/libs/utils/qtcprocess.cpp @@ -187,6 +187,7 @@ public: QTextCodec *codec = nullptr; // Not owner std::unique_ptr codecState; std::function outputCallback; + TextChannelMode m_textChannelMode = TextChannelMode::Off; bool emitSingleLines = true; bool keepRawData = true; @@ -1831,32 +1832,74 @@ void QtcProcess::runBlocking(EventLoopMode eventLoopMode) } } -void QtcProcess::setStdOutCallback(const std::function &callback) +void QtcProcess::setStdOutCallback(const TextChannelCallback &callback) { d->m_stdOut.outputCallback = callback; d->m_stdOut.emitSingleLines = false; } -void QtcProcess::setStdOutLineCallback(const std::function &callback) +void QtcProcess::setStdOutLineCallback(const TextChannelCallback &callback) { d->m_stdOut.outputCallback = callback; d->m_stdOut.emitSingleLines = true; d->m_stdOut.keepRawData = false; } -void QtcProcess::setStdErrCallback(const std::function &callback) +void QtcProcess::setStdErrCallback(const TextChannelCallback &callback) { d->m_stdErr.outputCallback = callback; d->m_stdErr.emitSingleLines = false; } -void QtcProcess::setStdErrLineCallback(const std::function &callback) +void QtcProcess::setStdErrLineCallback(const TextChannelCallback &callback) { d->m_stdErr.outputCallback = callback; d->m_stdErr.emitSingleLines = true; d->m_stdErr.keepRawData = false; } +void QtcProcess::setTextChannelMode(Channel channel, TextChannelMode mode) +{ + const TextChannelCallback outputCb = [this](const QString &text) { + GuardLocker locker(d->m_guard); + emit textOnStandardOutput(text); + }; + const TextChannelCallback errorCb = [this](const QString &text) { + GuardLocker locker(d->m_guard); + emit textOnStandardError(text); + }; + const TextChannelCallback callback = (channel == Channel::Output) ? outputCb : errorCb; + ChannelBuffer *buffer = channel == Channel::Output ? &d->m_stdOut : &d->m_stdErr; + QTC_ASSERT(buffer->m_textChannelMode == TextChannelMode::Off, qWarning() + << "QtcProcess::setTextChannelMode(): Changing text channel mode for" + << (channel == Channel::Output ? "Output": "Error") + << "channel while it was previously set for this channel."); + buffer->m_textChannelMode = mode; + switch (mode) { + case TextChannelMode::Off: + buffer->outputCallback = {}; + buffer->emitSingleLines = true; + buffer->keepRawData = true; + break; + case TextChannelMode::SingleLine: + buffer->outputCallback = callback; + buffer->emitSingleLines = true; + buffer->keepRawData = false; + break; + case TextChannelMode::MultiLine: + buffer->outputCallback = callback; + buffer->emitSingleLines = false; + buffer->keepRawData = true; + break; + } +} + +TextChannelMode QtcProcess::textChannelMode(Channel channel) const +{ + ChannelBuffer *buffer = channel == Channel::Output ? &d->m_stdOut : &d->m_stdErr; + return buffer->m_textChannelMode; +} + void QtcProcessPrivate::slotTimeout() { if (!m_waitingForUser && (++m_hangTimerCount > m_maxHangTimerCount)) { diff --git a/src/libs/utils/qtcprocess.h b/src/libs/utils/qtcprocess.h index 37a639dba6b..e553b397ee6 100644 --- a/src/libs/utils/qtcprocess.h +++ b/src/libs/utils/qtcprocess.h @@ -148,10 +148,13 @@ public: void setTimeOutMessageBoxEnabled(bool); void setExitCodeInterpreter(const ExitCodeInterpreter &interpreter); - void setStdOutCallback(const std::function &callback); - void setStdOutLineCallback(const std::function &callback); - void setStdErrCallback(const std::function &callback); - void setStdErrLineCallback(const std::function &callback); + void setStdOutCallback(const TextChannelCallback &callback); + void setStdOutLineCallback(const TextChannelCallback &callback); + void setStdErrCallback(const TextChannelCallback &callback); + void setStdErrLineCallback(const TextChannelCallback &callback); + + void setTextChannelMode(Channel channel, TextChannelMode mode); + TextChannelMode textChannelMode(Channel channel) const; bool readDataFromProcess(QByteArray *stdOut, QByteArray *stdErr, int timeoutS = 30); @@ -182,6 +185,8 @@ signals: void done(); // On Starting | Running -> NotRunning state transition void readyReadStandardOutput(); void readyReadStandardError(); + void textOnStandardOutput(const QString &text); + void textOnStandardError(const QString &text); private: friend QTCREATOR_UTILS_EXPORT QDebug operator<<(QDebug str, const QtcProcess &r); diff --git a/tests/auto/utils/qtcprocess/tst_qtcprocess.cpp b/tests/auto/utils/qtcprocess/tst_qtcprocess.cpp index 037729a389b..7a3ba45ec46 100644 --- a/tests/auto/utils/qtcprocess/tst_qtcprocess.cpp +++ b/tests/auto/utils/qtcprocess/tst_qtcprocess.cpp @@ -112,7 +112,10 @@ private slots: void exitCode(); void runBlockingStdOut_data(); void runBlockingStdOut(); + void runBlockingSignal_data(); + void runBlockingSignal(); void lineCallback(); + void lineSignal(); void waitForStartedAfterStarted(); void waitForStartedAfterStarted2(); void waitForStartedAndFinished(); @@ -937,13 +940,46 @@ void tst_QtcProcess::runBlockingStdOut() QVERIFY2(readLastLine, "Last line was read."); } +void tst_QtcProcess::runBlockingSignal_data() +{ + runBlockingStdOut_data(); +} + +void tst_QtcProcess::runBlockingSignal() +{ + QFETCH(bool, withEndl); + QFETCH(int, timeOutS); + QFETCH(ProcessResult, expectedResult); + + SubProcessConfig subConfig(ProcessTestApp::RunBlockingStdOut::envVar(), withEndl ? "true" : "false"); + QtcProcess process; + subConfig.setupSubProcess(&process); + + process.setTimeoutS(timeOutS); + bool readLastLine = false; + process.setTextChannelMode(Channel::Output, TextChannelMode::MultiLine); + connect(&process, &QtcProcess::textOnStandardOutput, + this, [&readLastLine, &process](const QString &out) { + if (out.startsWith(s_runBlockingStdOutSubProcessMagicWord)) { + readLastLine = true; + process.kill(); + } + }); + process.runBlocking(); + + // See also QTCREATORBUG-25667 for why it is a bad idea to use QtcProcess::runBlocking + // with interactive cli tools. + QCOMPARE(process.result(), expectedResult); + QVERIFY2(readLastLine, "Last line was read."); +} + void tst_QtcProcess::lineCallback() { SubProcessConfig subConfig(ProcessTestApp::LineCallback::envVar(), {}); QtcProcess process; subConfig.setupSubProcess(&process); - QStringList lines = QString(s_lineCallbackData).split('|'); + const QStringList lines = QString(s_lineCallbackData).split('|'); int lineNumber = 0; process.setStdErrLineCallback([lines, &lineNumber](const QString &actual) { QString expected = lines.at(lineNumber); @@ -960,6 +996,31 @@ void tst_QtcProcess::lineCallback() QCOMPARE(lineNumber, lines.size()); } +void tst_QtcProcess::lineSignal() +{ + SubProcessConfig subConfig(ProcessTestApp::LineCallback::envVar(), {}); + QtcProcess process; + subConfig.setupSubProcess(&process); + + const QStringList lines = QString(s_lineCallbackData).split('|'); + int lineNumber = 0; + process.setTextChannelMode(Channel::Error, TextChannelMode::SingleLine); + connect(&process, &QtcProcess::textOnStandardError, + this, [lines, &lineNumber](const QString &actual) { + QString expected = lines.at(lineNumber); + expected.replace("\r\n", "\n"); + // Omit some initial lines generated by Qt, e.g. + // Warning: Ignoring WAYLAND_DISPLAY on Gnome. Use QT_QPA_PLATFORM=wayland to run on Wayland anyway. + if (lineNumber == 0 && actual != expected) + return; + ++lineNumber; + QCOMPARE(actual, expected); + }); + process.start(); + process.waitForFinished(); + QCOMPARE(lineNumber, lines.size()); +} + void tst_QtcProcess::waitForStartedAfterStarted() { SubProcessConfig subConfig(ProcessTestApp::SimpleTest::envVar(), {});