diff options
author | Marcus Tillmanns <[email protected]> | 2025-06-23 09:10:52 +0200 |
---|---|---|
committer | Marcus Tillmanns <[email protected]> | 2025-07-01 09:17:25 +0000 |
commit | c7631c36d1cf0d912032adce4f11b9d598d25d7f (patch) | |
tree | bbf0e825d7c5b25ed5bc9e57d5d0ee4667b0870d | |
parent | 3f78b213f3ca157e2a6f2ce56561b31cbb41de96 (diff) |
Helper class to simplify implementing process interfaces
that wrap the actual call inside another process call like
docker or ssh.
Change-Id: I78b0f403ddbb928259b31683b599bdefaf13888d
Reviewed-by: hjk <[email protected]>
-rw-r--r-- | src/libs/utils/processinterface.cpp | 190 | ||||
-rw-r--r-- | src/libs/utils/processinterface.h | 25 |
2 files changed, 215 insertions, 0 deletions
diff --git a/src/libs/utils/processinterface.cpp b/src/libs/utils/processinterface.cpp index a2dc746905c..fabc4868238 100644 --- a/src/libs/utils/processinterface.cpp +++ b/src/libs/utils/processinterface.cpp @@ -4,6 +4,11 @@ #include "processinterface.h" #include "qtcassert.h" +#include "qtcprocess.h" + +#include <QLoggingCategory> + +Q_LOGGING_CATEGORY(wrappedProcessInterface, "qtc.wrappedprocessinterface", QtWarningMsg) namespace Utils { @@ -37,4 +42,189 @@ int ProcessInterface::controlSignalToInt(ControlSignal controlSignal) return 0; } +namespace Internal { +class WrappedProcessInterfacePrivate +{ +public: + Process m_process; + bool m_hasReceivedFirstOutput = false; + qint64 m_remotePID = 0; + QString m_unexpectedStartupOutput; + bool m_forwardStdout = false; + bool m_forwardStderr = false; +}; + +} // namespace Internal + +WrappedProcessInterface::WrappedProcessInterface() + : d(std::make_unique<Internal::WrappedProcessInterfacePrivate>()) +{ + d->m_process.setParent(this); + + connect(&d->m_process, &Process::started, this, [this] { + qCDebug(wrappedProcessInterface) << "Process started:" << d->m_process.commandLine(); + + if (m_setup.m_ptyData.has_value()) { + d->m_hasReceivedFirstOutput = true; + emit started(d->m_process.processId(), d->m_process.applicationMainThreadId()); + } + }); + + connect(&d->m_process, &Process::readyReadStandardOutput, this, [this] { + if (d->m_hasReceivedFirstOutput) { + emit readyRead(d->m_process.readAllRawStandardOutput(), {}); + return; + } + + QByteArray output = d->m_process.readAllRawStandardOutput(); + qsizetype idx = output.indexOf('\n'); + QByteArrayView firstLine = output.left(idx).trimmed(); + QByteArrayView rest = output.mid(idx + 1); + + qCDebug(wrappedProcessInterface) + << "Process first line received:" << d->m_process.commandLine() << firstLine; + + if (!firstLine.startsWith("__qtc")) { + d->m_unexpectedStartupOutput = QString::fromUtf8(firstLine); + d->m_process.kill(); + return; + } + + bool ok = false; + d->m_remotePID = firstLine.mid(5, firstLine.size() - 5 - 5).toLongLong(&ok); + + if (ok) + emit started(d->m_remotePID); + else { + d->m_unexpectedStartupOutput = QString::fromUtf8(firstLine); + d->m_process.kill(); + return; + } + + d->m_hasReceivedFirstOutput = true; + + if (d->m_forwardStdout && rest.size() > 0) { + fprintf(stdout, "%s", rest.constData()); + rest = QByteArrayView(); + } + + // In case we already received some error output, send it now. + QByteArray stdErr = d->m_process.readAllRawStandardError(); + if (stdErr.size() > 0 && d->m_forwardStderr) { + fprintf(stderr, "%s", stdErr.constData()); + stdErr.clear(); + } + + if (rest.size() > 0 || stdErr.size() > 0) + emit readyRead(rest.toByteArray(), stdErr); + }); + + connect(&d->m_process, &Process::readyReadStandardError, this, [this] { + if (!d->m_remotePID) + return; + + if (d->m_forwardStderr) { + fprintf(stderr, "%s", d->m_process.readAllRawStandardError().constData()); + return; + } + + emit readyRead({}, d->m_process.readAllRawStandardError()); + }); + + connect(&d->m_process, &Process::done, this, [this] { + qCDebug(wrappedProcessInterface) << "Process exited:" << d->m_process.commandLine() + << "with code:" << d->m_process.resultData().m_exitCode; + + ProcessResultData resultData = d->m_process.resultData(); + + if (d->m_remotePID == 0 && !d->m_hasReceivedFirstOutput) { + resultData.m_error = QProcess::FailedToStart; + resultData.m_errorString = d->m_unexpectedStartupOutput; + + qCWarning(wrappedProcessInterface) + << "Process failed to start:" << d->m_process.commandLine() << ":" + << d->m_unexpectedStartupOutput; + QByteArray stdOut = d->m_process.readAllRawStandardOutput(); + QByteArray stdErr = d->m_process.readAllRawStandardError(); + + if (!stdOut.isEmpty()) + qCWarning(wrappedProcessInterface) << "stdout:" << stdOut; + if (!stdErr.isEmpty()) + qCWarning(wrappedProcessInterface) << "stderr:" << stdErr; + } + + emit done(resultData); + }); +} + +WrappedProcessInterface::~WrappedProcessInterface() +{ + if (d->m_process.state() == QProcess::Running) + sendControlSignal(ControlSignal::Kill); +} + +void WrappedProcessInterface::start() +{ + d->m_process.setProcessMode(m_setup.m_processMode); + d->m_process.setTerminalMode(m_setup.m_terminalMode); + d->m_process.setPtyData(m_setup.m_ptyData); + d->m_process.setReaperTimeout(m_setup.m_reaperTimeout); + d->m_process.setWriteData(m_setup.m_writeData); + // We need separate channels so we can intercept our Process ID markers. + d->m_process.setProcessChannelMode(QProcess::ProcessChannelMode::SeparateChannels); + d->m_process.setExtraData(m_setup.m_extraData); + d->m_process.setStandardInputFile(m_setup.m_standardInputFile); + d->m_process.setAbortOnMetaChars(m_setup.m_abortOnMetaChars); + d->m_process.setCreateConsoleOnWindows(m_setup.m_createConsoleOnWindows); + if (m_setup.m_lowPriority) + d->m_process.setLowPriority(); + + d->m_forwardStdout = m_setup.m_processChannelMode == QProcess::ForwardedChannels + || m_setup.m_processChannelMode == QProcess::ForwardedOutputChannel; + d->m_forwardStderr = m_setup.m_processChannelMode == QProcess::ForwardedChannels + || m_setup.m_processChannelMode == QProcess::ForwardedErrorChannel; + + const Result<CommandLine> fullCommandLine = wrapCommmandLine(m_setup, "__qtc%1qtc__"); + + if (!fullCommandLine) { + emit done(ProcessResultData{ + -1, + QProcess::ExitStatus::CrashExit, + QProcess::ProcessError::FailedToStart, + fullCommandLine.error(), + }); + return; + } + + d->m_process.setCommand(*fullCommandLine); + d->m_process.start(); +} + +qint64 WrappedProcessInterface::write(const QByteArray &data) +{ + return d->m_process.writeRaw(data); +} + +void WrappedProcessInterface::sendControlSignal(ControlSignal controlSignal) +{ + if (!m_setup.m_ptyData.has_value()) { + QTC_ASSERT(d->m_remotePID, return); + if (controlSignal == ControlSignal::CloseWriteChannel) { + d->m_process.closeWriteChannel(); + return; + } + forwardControlSignal(controlSignal, d->m_remotePID); + } else { + // clang-format off + switch (controlSignal) { + case ControlSignal::Terminate: d->m_process.terminate(); break; + case ControlSignal::Kill: d->m_process.kill(); break; + case ControlSignal::Interrupt: d->m_process.interrupt(); break; + case ControlSignal::KickOff: d->m_process.kickoffProcess(); break; + case ControlSignal::CloseWriteChannel: break; + } + // clang-format on + } +} + } // Utils diff --git a/src/libs/utils/processinterface.h b/src/libs/utils/processinterface.h index a29e3c78dc5..ff0ed3167be 100644 --- a/src/libs/utils/processinterface.h +++ b/src/libs/utils/processinterface.h @@ -157,4 +157,29 @@ private: friend class Internal::ProcessPrivate; }; +namespace Internal { +class WrappedProcessInterfacePrivate; +} + +class QTCREATOR_UTILS_EXPORT WrappedProcessInterface : public ProcessInterface +{ +public: + WrappedProcessInterface(); + ~WrappedProcessInterface() override; + +public: + virtual Result<CommandLine> wrapCommmandLine( + const ProcessSetupData &setupData, const QString &markerTemplate) const + = 0; + virtual void forwardControlSignal(ControlSignal controlSignal, qint64 remotePid) const = 0; + +public: + void start() final; + qint64 write(const QByteArray &data) final; + void sendControlSignal(ControlSignal controlSignal) final; + +private: + std::unique_ptr<Internal::WrappedProcessInterfacePrivate> d; +}; + } // namespace Utils |