From 1da18a4b62c9fcb5d499b098b0e13049f0c34193 Mon Sep 17 00:00:00 2001 From: Marcus Tillmanns Date: Wed, 1 Mar 2023 08:15:58 +0100 Subject: [PATCH] Utils: Integrate ptyqt into qtcprocess Integrating PtyQt directly into QtcProcess allows us to start Pseudo terminal processes using the existing QtcProcess functionality such as starting remote process on e.g. docker or remote linux devices. This is needed for the new Terminal plugin. Change-Id: Iaeed5ff9b341ba4646d955b2ed9577a18cd7100f Reviewed-by: Jarek Kobus Reviewed-by: Cristian Adam --- src/libs/3rdparty/libptyqt/conptyprocess.cpp | 16 +-- src/libs/3rdparty/libptyqt/unixptyprocess.cpp | 4 +- src/libs/3rdparty/libptyqt/winptyprocess.cpp | 1 + src/libs/3rdparty/winpty/src/CMakeLists.txt | 1 - src/libs/utils/CMakeLists.txt | 2 +- src/libs/utils/processenums.h | 1 + src/libs/utils/qtcprocess.cpp | 98 +++++++++++++++++++ src/plugins/docker/dockerdevice.cpp | 6 +- src/plugins/remotelinux/linuxdevice.cpp | 14 ++- src/plugins/terminal/CMakeLists.txt | 2 +- src/plugins/terminal/terminalwidget.cpp | 78 +++++++-------- src/plugins/terminal/terminalwidget.h | 4 +- 12 files changed, 168 insertions(+), 59 deletions(-) diff --git a/src/libs/3rdparty/libptyqt/conptyprocess.cpp b/src/libs/3rdparty/libptyqt/conptyprocess.cpp index d112f5e1533..c788e74c92a 100644 --- a/src/libs/3rdparty/libptyqt/conptyprocess.cpp +++ b/src/libs/3rdparty/libptyqt/conptyprocess.cpp @@ -123,6 +123,7 @@ bool ConPtyProcess::startProcess(const QString &executable, } m_shellPath = executable; + m_shellPath.replace('/', '\\'); m_size = QPair(cols, rows); //env @@ -134,6 +135,7 @@ bool ConPtyProcess::startProcess(const QString &executable, envBlock << L'\0'; std::wstring env = envBlock.str(); LPWSTR envArg = env.empty() ? nullptr : env.data(); + LPCWSTR workingDirPointer = workingDir.isEmpty() ? nullptr : workingDir.toStdWString().c_str(); QStringList exeAndArgs = arguments; exeAndArgs.prepend(m_shellPath); @@ -165,7 +167,7 @@ bool ConPtyProcess::startProcess(const QString &executable, FALSE, // Inherit handles EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, // Creation flags envArg, // Environment block - workingDir.toStdWString().c_str(), // Use parent's starting directory + workingDirPointer, // Use parent's starting directory &m_shellStartupInfo.StartupInfo, // Pointer to STARTUPINFO &m_shellProcessInformation) // Pointer to PROCESS_INFORMATION ? S_OK @@ -264,11 +266,13 @@ bool ConPtyProcess::kill() if (INVALID_HANDLE_VALUE != m_hPipeIn) CloseHandle(m_hPipeIn); - m_readThread->requestInterruption(); - if (!m_readThread->wait(1000)) - m_readThread->terminate(); - m_readThread->deleteLater(); - m_readThread = nullptr; + if (m_readThread) { + m_readThread->requestInterruption(); + if (!m_readThread->wait(1000)) + m_readThread->terminate(); + m_readThread->deleteLater(); + m_readThread = nullptr; + } delete m_shellCloseWaitNotifier; m_shellCloseWaitNotifier = nullptr; diff --git a/src/libs/3rdparty/libptyqt/unixptyprocess.cpp b/src/libs/3rdparty/libptyqt/unixptyprocess.cpp index 7cb8237a605..e049a2abb92 100644 --- a/src/libs/3rdparty/libptyqt/unixptyprocess.cpp +++ b/src/libs/3rdparty/libptyqt/unixptyprocess.cpp @@ -182,6 +182,7 @@ bool UnixPtyProcess::startProcess(const QString &shellPath, QObject::connect(&m_shellProcess, &QProcess::finished, &m_shellProcess, [this](int exitCode) { m_exitCode = exitCode; emit m_shellProcess.aboutToClose(); + m_readMasterNotify->disconnect(); }); QStringList defaultVars; @@ -216,7 +217,8 @@ bool UnixPtyProcess::startProcess(const QString &shellPath, m_shellProcess.setProcessEnvironment(envFormat); m_shellProcess.setReadChannel(QProcess::StandardOutput); m_shellProcess.start(m_shellPath, arguments); - m_shellProcess.waitForStarted(); + if (!m_shellProcess.waitForStarted()) + return false; m_pid = m_shellProcess.processId(); diff --git a/src/libs/3rdparty/libptyqt/winptyprocess.cpp b/src/libs/3rdparty/libptyqt/winptyprocess.cpp index be3a0de609e..0509bb77c37 100644 --- a/src/libs/3rdparty/libptyqt/winptyprocess.cpp +++ b/src/libs/3rdparty/libptyqt/winptyprocess.cpp @@ -60,6 +60,7 @@ bool WinPtyProcess::startProcess(const QString &executable, } m_shellPath = executable; + m_shellPath.replace('/', '\\'); m_size = QPair(cols, rows); #ifdef PTYQT_DEBUG diff --git a/src/libs/3rdparty/winpty/src/CMakeLists.txt b/src/libs/3rdparty/winpty/src/CMakeLists.txt index 5763955e8d0..22b15111d4f 100644 --- a/src/libs/3rdparty/winpty/src/CMakeLists.txt +++ b/src/libs/3rdparty/winpty/src/CMakeLists.txt @@ -46,7 +46,6 @@ set(shared_sources # add_qtc_executable(winpty-agent - DESTINATION ${IDE_PLUGIN_PATH} INCLUDES include ${CMAKE_BINARY_DIR} DEFINES WINPTY_AGENT_ASSERT diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index 04f475afb7c..d6b6efb4f2d 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -257,7 +257,7 @@ extend_qtc_library(Utils CONDITION UNIX AND NOT APPLE extend_qtc_library(Utils CONDITION TARGET Qt::CorePrivate - DEPENDS Qt::CorePrivate + DEPENDS Qt::CorePrivate ptyqt DEFINES QTC_UTILS_WITH_FSENGINE SOURCES fsengine/fsengine_impl.cpp fsengine/fsengine_impl.h diff --git a/src/libs/utils/processenums.h b/src/libs/utils/processenums.h index 6ea37a2d37a..1c16f22dcb4 100644 --- a/src/libs/utils/processenums.h +++ b/src/libs/utils/processenums.h @@ -24,6 +24,7 @@ enum class ProcessImpl { enum class TerminalMode { Off, + Pty, Run, Debug, Suspend, diff --git a/src/libs/utils/qtcprocess.cpp b/src/libs/utils/qtcprocess.cpp index 42a29366bd9..4808764ddde 100644 --- a/src/libs/utils/qtcprocess.cpp +++ b/src/libs/utils/qtcprocess.cpp @@ -16,6 +16,9 @@ #include "threadutils.h" #include "utilstr.h" +#include +#include + #include #include #include @@ -304,6 +307,99 @@ private: QProcess *m_process = nullptr; }; +class PtyProcessImpl final : public DefaultImpl +{ +public: + ~PtyProcessImpl() { m_setup.m_ptyData.setResizeHandler({}); } + + qint64 write(const QByteArray &data) final + { + if (m_ptyProcess) + return m_ptyProcess->write(data); + return -1; + } + + void sendControlSignal(ControlSignal controlSignal) final + { + if (!m_ptyProcess) + return; + + switch (controlSignal) { + case ControlSignal::Terminate: + m_ptyProcess.reset(); + break; + case ControlSignal::Kill: + m_ptyProcess->kill(); + break; + default: + QTC_CHECK(false); + } + } + + void doDefaultStart(const QString &program, const QStringList &arguments) final + { + m_setup.m_ptyData.setResizeHandler([this](const QSize &size) { + if (m_ptyProcess) + m_ptyProcess->resize(size.width(), size.height()); + }); + m_ptyProcess.reset(PtyQt::createPtyProcess(IPtyProcess::AutoPty)); + if (!m_ptyProcess) { + const ProcessResultData result = {-1, + QProcess::CrashExit, + QProcess::FailedToStart, + "Failed to create pty process"}; + emit done(result); + return; + } + + bool startResult + = m_ptyProcess->startProcess(program, + arguments, + m_setup.m_workingDirectory.path(), + m_setup.m_environment.toProcessEnvironment().toStringList(), + m_setup.m_ptyData.size().width(), + m_setup.m_ptyData.size().height()); + + if (!startResult) { + const ProcessResultData result = {-1, + QProcess::CrashExit, + QProcess::FailedToStart, + "Failed to start pty process: " + + m_ptyProcess->lastError()}; + emit done(result); + return; + } + + if (!m_ptyProcess->lastError().isEmpty()) { + const ProcessResultData result + = {-1, QProcess::CrashExit, QProcess::FailedToStart, m_ptyProcess->lastError()}; + emit done(result); + return; + } + + connect(m_ptyProcess->notifier(), &QIODevice::readyRead, this, [this] { + emit readyRead(m_ptyProcess->readAll(), {}); + }); + + connect(m_ptyProcess->notifier(), &QIODevice::aboutToClose, this, [this] { + if (m_ptyProcess) { + const ProcessResultData result + = {m_ptyProcess->exitCode(), QProcess::NormalExit, QProcess::UnknownError, {}}; + emit done(result); + return; + } + + const ProcessResultData result = {0, QProcess::NormalExit, QProcess::UnknownError, {}}; + emit done(result); + }); + + emit started(m_ptyProcess->pid()); + } + +private: + std::unique_ptr m_ptyProcess; +}; + class QProcessImpl final : public DefaultImpl { public: @@ -629,6 +725,8 @@ public: ProcessInterface *createProcessInterface() { + if (m_setup.m_terminalMode == TerminalMode::Pty) + return new PtyProcessImpl(); if (m_setup.m_terminalMode != TerminalMode::Off) return Terminal::Hooks::instance().createTerminalProcessInterfaceHook()(); diff --git a/src/plugins/docker/dockerdevice.cpp b/src/plugins/docker/dockerdevice.cpp index 9c6bf8c9bd8..26569166a7d 100644 --- a/src/plugins/docker/dockerdevice.cpp +++ b/src/plugins/docker/dockerdevice.cpp @@ -241,7 +241,7 @@ DockerProcessImpl::DockerProcessImpl(IDevice::ConstPtr device, DockerDevicePriva if (!m_hasReceivedFirstOutput) { QByteArray output = m_process.readAllRawStandardOutput(); qsizetype idx = output.indexOf('\n'); - QByteArray firstLine = output.left(idx); + QByteArray firstLine = output.left(idx).trimmed(); QByteArray rest = output.mid(idx + 1); qCDebug(dockerDeviceLog) << "Process first line received:" << m_process.commandLine() << firstLine; @@ -301,7 +301,9 @@ void DockerProcessImpl::start() = m_devicePrivate->withDockerExecCmd(m_setup.m_commandLine, m_setup.m_environment, m_setup.m_workingDirectory, - interactive); + interactive, + true, + m_setup.m_terminalMode == TerminalMode::Pty); m_process.setCommand(fullCommandLine); m_process.start(); diff --git a/src/plugins/remotelinux/linuxdevice.cpp b/src/plugins/remotelinux/linuxdevice.cpp index 6d611bf7b83..754a7f0281e 100644 --- a/src/plugins/remotelinux/linuxdevice.cpp +++ b/src/plugins/remotelinux/linuxdevice.cpp @@ -656,9 +656,15 @@ void LinuxProcessInterface::handleReadyReadStandardOutput(const QByteArray &outp m_output.append(outputData); static const QByteArray endMarker = s_pidMarker + '\n'; - const int endMarkerOffset = m_output.indexOf(endMarker); - if (endMarkerOffset == -1) - return; + int endMarkerLength = endMarker.length(); + int endMarkerOffset = m_output.indexOf(endMarker); + if (endMarkerOffset == -1) { + static const QByteArray endMarker = s_pidMarker + "\r\n"; + endMarkerOffset = m_output.indexOf(endMarker); + endMarkerLength = endMarker.length(); + if (endMarkerOffset == -1) + return; + } const int startMarkerOffset = m_output.indexOf(s_pidMarker); if (startMarkerOffset == endMarkerOffset) // Only theoretically possible. return; @@ -668,7 +674,7 @@ void LinuxProcessInterface::handleReadyReadStandardOutput(const QByteArray &outp const qint64 processId = pidString.toLongLong(); // We don't want to show output from e.g. /etc/profile. - m_output = m_output.mid(endMarkerOffset + endMarker.length()); + m_output = m_output.mid(endMarkerOffset + endMarkerLength); emitStarted(processId); diff --git a/src/plugins/terminal/CMakeLists.txt b/src/plugins/terminal/CMakeLists.txt index a970b467e19..fafe3e7346e 100644 --- a/src/plugins/terminal/CMakeLists.txt +++ b/src/plugins/terminal/CMakeLists.txt @@ -1,7 +1,7 @@ add_qtc_plugin(Terminal PLUGIN_DEPENDS Core - DEPENDS libvterm ptyqt + DEPENDS libvterm SOURCES celllayout.cpp celllayout.h terminal.qrc diff --git a/src/plugins/terminal/terminalwidget.cpp b/src/plugins/terminal/terminalwidget.cpp index 94cff232d16..572bc74f9e0 100644 --- a/src/plugins/terminal/terminalwidget.cpp +++ b/src/plugins/terminal/terminalwidget.cpp @@ -15,11 +15,11 @@ #include #include -#include #include #include #include +#include #include #include #include @@ -69,7 +69,7 @@ TerminalWidget::TerminalWidget(QWidget *parent, const OpenTerminalParameters &op m_readDelayTimer.setSingleShot(true); m_readDelayTimer.setInterval(10); - connect(&m_readDelayTimer, &QTimer::timeout, this, [this]() { + connect(&m_readDelayTimer, &QTimer::timeout, this, [this] { m_readDelayRestarts = 0; onReadyRead(); }); @@ -80,7 +80,7 @@ TerminalWidget::TerminalWidget(QWidget *parent, const OpenTerminalParameters &op connect(&m_zoomInAction, &QAction::triggered, this, &TerminalWidget::zoomIn); connect(&m_zoomOutAction, &QAction::triggered, this, &TerminalWidget::zoomOut); - connect(&TerminalSettings::instance(), &AspectContainer::applied, this, [this]() { + connect(&TerminalSettings::instance(), &AspectContainer::applied, this, [this] { m_layoutVersion++; // Setup colors first, as setupFont will redraw the screen. setupColors(); @@ -90,7 +90,7 @@ TerminalWidget::TerminalWidget(QWidget *parent, const OpenTerminalParameters &op void TerminalWidget::setupPty() { - m_ptyProcess.reset(PtyQt::createPtyProcess(IPtyProcess::PtyType::AutoPty)); + m_process = std::make_unique(); Environment env = m_openParameters.environment.value_or(Environment::systemEnvironment()); @@ -99,29 +99,17 @@ void TerminalWidget::setupPty() // For git bash on Windows env.prependOrSetPath(shellCommand.executable().parentDir()); + if (env.hasKey("CLINK_NOAUTORUN")) + env.unset("CLINK_NOAUTORUN"); - QStringList envList = filtered(env.toStringList(), [](const QString &envPair) { - return envPair != "CLINK_NOAUTORUN=1"; - }); + m_process->setProcessMode(ProcessMode::Writer); + m_process->setTerminalMode(TerminalMode::Pty); + m_process->setCommand(shellCommand); + m_process->setWorkingDirectory( + m_openParameters.workingDirectory.value_or(FilePath::fromString(QDir::homePath()))); + m_process->setEnvironment(env); - m_ptyProcess->startProcess(shellCommand.executable().nativePath(), - shellCommand.splitArguments(), - m_openParameters.workingDirectory - .value_or(FilePath::fromString(QDir::homePath())) - .nativePath(), - envList, - m_vtermSize.width(), - m_vtermSize.height()); - - emit started(m_ptyProcess->pid()); - - if (!m_ptyProcess->lastError().isEmpty()) { - qCWarning(terminalLog) << m_ptyProcess->lastError(); - m_ptyProcess.reset(); - return; - } - - connect(m_ptyProcess->notifier(), &QIODevice::readyRead, this, [this]() { + connect(m_process.get(), &QtcProcess::readyReadStandardOutput, this, [this] { if (m_readDelayTimer.isActive()) m_readDelayRestarts++; @@ -131,16 +119,19 @@ void TerminalWidget::setupPty() m_readDelayTimer.start(); }); - connect(m_ptyProcess->notifier(), &QIODevice::aboutToClose, this, [this]() { + connect(m_process.get(), &QtcProcess::done, this, [this] { m_cursor.visible = false; - if (m_ptyProcess) { + if (m_process) { onReadyRead(); - if (m_ptyProcess->exitCode() != 0) { + if (m_process->exitCode() != 0) { QByteArray msg = QString("\r\n\033[31mProcess exited with code: %1") - .arg(m_ptyProcess->exitCode()) + .arg(m_process->exitCode()) .toUtf8(); + if (!m_process->errorString().isEmpty()) + msg += QString(" (%1)").arg(m_process->errorString()).toUtf8(); + vterm_input_write(m_vterm.get(), msg.constData(), msg.size()); vterm_screen_flush_damage(m_vtermScreen); @@ -149,10 +140,8 @@ void TerminalWidget::setupPty() } if (m_openParameters.m_exitBehavior == ExitBehavior::Restart) { - QMetaObject::invokeMethod( - this, - [this]() { - m_ptyProcess.reset(); + QMetaObject::invokeMethod(this, [this] { + m_process.reset(); setupPty(); }, Qt::QueuedConnection); @@ -163,13 +152,20 @@ void TerminalWidget::setupPty() if (m_openParameters.m_exitBehavior == ExitBehavior::Keep) { QByteArray msg = QString("\r\nProcess exited with code: %1") - .arg(m_ptyProcess ? m_ptyProcess->exitCode() : -1) + .arg(m_process ? m_process->exitCode() : -1) .toUtf8(); vterm_input_write(m_vterm.get(), msg.constData(), msg.size()); vterm_screen_flush_damage(m_vtermScreen); } }); + + connect(m_process.get(), &QtcProcess::started, this, [this] { + applySizeChange(); + emit started(m_process->processId()); + }); + + m_process->start(); } void TerminalWidget::setupFont() @@ -229,8 +225,8 @@ void TerminalWidget::setupColors() void TerminalWidget::writeToPty(const QByteArray &data) { - if (m_ptyProcess) - m_ptyProcess->write(data); + if (m_process) + m_process->writeRaw(data); } void TerminalWidget::setupVTerm() @@ -305,7 +301,7 @@ void TerminalWidget::setFont(const QFont &font) QAbstractScrollArea::setFont(m_font); - if (m_ptyProcess) { + if (m_process) { applySizeChange(); } } @@ -425,7 +421,7 @@ void TerminalWidget::clearContents() void TerminalWidget::onReadyRead() { - QByteArray data = m_ptyProcess->readAll(); + QByteArray data = m_process->readAllRawStandardOutput(); vterm_input_write(m_vterm.get(), data.constData(), data.size()); vterm_screen_flush_damage(m_vtermScreen); } @@ -709,8 +705,8 @@ void TerminalWidget::applySizeChange() if (m_vtermSize.width() <= 0) m_vtermSize.setWidth(1); - if (m_ptyProcess) - m_ptyProcess->resize(m_vtermSize.width(), m_vtermSize.height()); + if (m_process) + m_process->ptyData().resize(m_vtermSize); vterm_set_size(m_vterm.get(), m_vtermSize.height(), m_vtermSize.width()); vterm_screen_flush_damage(m_vtermScreen); @@ -869,7 +865,7 @@ void TerminalWidget::showEvent(QShowEvent *event) { Q_UNUSED(event); - if (!m_ptyProcess) + if (!m_process) setupPty(); QAbstractScrollArea::showEvent(event); diff --git a/src/plugins/terminal/terminalwidget.h b/src/plugins/terminal/terminalwidget.h index 3f1590dfaaf..96e24f41172 100644 --- a/src/plugins/terminal/terminalwidget.h +++ b/src/plugins/terminal/terminalwidget.h @@ -5,6 +5,7 @@ #include "scrollback.h" +#include #include #include @@ -12,7 +13,6 @@ #include #include -#include #include #include @@ -111,7 +111,7 @@ protected: void updateScrollBars(); private: - std::unique_ptr m_ptyProcess; + std::unique_ptr m_process; std::unique_ptr m_vterm; VTermScreen *m_vtermScreen;