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 <jaroslaw.kobus@qt.io>
Reviewed-by: Cristian Adam <cristian.adam@qt.io>
This commit is contained in:
Marcus Tillmanns
2023-03-01 08:15:58 +01:00
parent 8b09ad8898
commit 1da18a4b62
12 changed files with 168 additions and 59 deletions

View File

@@ -123,6 +123,7 @@ bool ConPtyProcess::startProcess(const QString &executable,
}
m_shellPath = executable;
m_shellPath.replace('/', '\\');
m_size = QPair<qint16, qint16>(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;

View File

@@ -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();

View File

@@ -60,6 +60,7 @@ bool WinPtyProcess::startProcess(const QString &executable,
}
m_shellPath = executable;
m_shellPath.replace('/', '\\');
m_size = QPair<qint16, qint16>(cols, rows);
#ifdef PTYQT_DEBUG

View File

@@ -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

View File

@@ -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

View File

@@ -24,6 +24,7 @@ enum class ProcessImpl {
enum class TerminalMode {
Off,
Pty,
Run,
Debug,
Suspend,

View File

@@ -16,6 +16,9 @@
#include "threadutils.h"
#include "utilstr.h"
#include <iptyprocess.h>
#include <ptyqt.h>
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
@@ -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<IPtyProcess> 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()();

View File

@@ -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();

View File

@@ -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);

View File

@@ -1,7 +1,7 @@
add_qtc_plugin(Terminal
PLUGIN_DEPENDS Core
DEPENDS libvterm ptyqt
DEPENDS libvterm
SOURCES
celllayout.cpp celllayout.h
terminal.qrc

View File

@@ -15,11 +15,11 @@
#include <utils/processinterface.h>
#include <utils/stringutils.h>
#include <ptyqt.h>
#include <vterm.h>
#include <QApplication>
#include <QClipboard>
#include <QElapsedTimer>
#include <QGlyphRun>
#include <QLoggingCategory>
#include <QPaintEvent>
@@ -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<QtcProcess>();
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);

View File

@@ -5,6 +5,7 @@
#include "scrollback.h"
#include <utils/qtcprocess.h>
#include <utils/terminalhooks.h>
#include <QAbstractScrollArea>
@@ -12,7 +13,6 @@
#include <QTextLayout>
#include <QTimer>
#include <iptyprocess.h>
#include <vterm.h>
#include <memory>
@@ -111,7 +111,7 @@ protected:
void updateScrollBars();
private:
std::unique_ptr<IPtyProcess> m_ptyProcess;
std::unique_ptr<Utils::QtcProcess> m_process;
std::unique_ptr<VTerm, void (*)(VTerm *)> m_vterm;
VTermScreen *m_vtermScreen;