From 8d2c9aa8d4e6e9644d8257173592102f4b89553d Mon Sep 17 00:00:00 2001 From: Jarek Kobus Date: Mon, 14 Feb 2022 18:37:56 +0100 Subject: [PATCH] LinuxDevice: Implement shared ssh connection Change-Id: I1897f0a468e477e0be61767d4bad0086d59fb075 Reviewed-by: Christian Kandeler Reviewed-by: hjk Reviewed-by: --- src/plugins/qnx/qnxdevice.cpp | 63 +- src/plugins/qnx/qnxdevice.h | 1 + src/plugins/remotelinux/CMakeLists.txt | 1 + src/plugins/remotelinux/linuxdevice.cpp | 812 ++++++++++++++++-- src/plugins/remotelinux/linuxdevice.h | 3 +- src/plugins/remotelinux/remotelinux.qbs | 1 + src/plugins/remotelinux/sshprocessinterface.h | 70 ++ 7 files changed, 876 insertions(+), 75 deletions(-) create mode 100644 src/plugins/remotelinux/sshprocessinterface.h diff --git a/src/plugins/qnx/qnxdevice.cpp b/src/plugins/qnx/qnxdevice.cpp index eb0f907f8a1..afd0170d1cf 100644 --- a/src/plugins/qnx/qnxdevice.cpp +++ b/src/plugins/qnx/qnxdevice.cpp @@ -36,6 +36,8 @@ #include #include +#include + #include #include #include @@ -47,11 +49,63 @@ #include using namespace ProjectExplorer; +using namespace RemoteLinux; using namespace Utils; namespace Qnx { namespace Internal { +class QnxProcessImpl final : public SshProcessInterface +{ +public: + QnxProcessImpl(const LinuxDevice *linuxDevice); + ~QnxProcessImpl() { killIfRunning(); } + +private: + QString fullCommandLine(const CommandLine &commandLine) const final; + QString pidArgumentForKill() const final; + + const QString m_pidFile; +}; + +static std::atomic_int s_pidFileCounter = 1; + +QnxProcessImpl::QnxProcessImpl(const LinuxDevice *linuxDevice) + : SshProcessInterface(linuxDevice) + , m_pidFile(QString::fromLatin1("/var/run/qtc.%1.pid").arg(s_pidFileCounter.fetch_add(1))) +{ +} + +QString QnxProcessImpl::fullCommandLine(const CommandLine &commandLine) const +{ + QStringList args = ProcessArgs::splitArgs(commandLine.arguments()); + args.prepend(commandLine.executable().toString()); + const QString cmd = ProcessArgs::createUnixArgs(args).toString(); + + QString fullCommandLine = + "test -f /etc/profile && . /etc/profile ; " + "test -f $HOME/profile && . $HOME/profile ; "; + + if (!m_setup.m_workingDirectory.isEmpty()) + fullCommandLine += QString::fromLatin1("cd %1 ; ").arg( + ProcessArgs::quoteArg(m_setup.m_workingDirectory.toString())); + + const Environment env = m_setup.m_remoteEnvironment; + for (auto it = env.constBegin(); it != env.constEnd(); ++it) { + fullCommandLine += QString::fromLatin1("%1='%2' ") + .arg(env.key(it)).arg(env.expandedValueForKey(env.key(it))); + } + + fullCommandLine += QString::fromLatin1("%1 & echo $! > %2").arg(cmd).arg(m_pidFile); + + return fullCommandLine; +} + +QString QnxProcessImpl::pidArgumentForKill() const +{ + return QString::fromLatin1("`cat %1`").arg(m_pidFile); +} + const char QnxVersionKey[] = "QnxVersion"; class QnxPortsGatheringMethod : public PortsGatheringMethod @@ -130,12 +184,12 @@ void QnxDevice::updateVersionNumber() const void QnxDevice::fromMap(const QVariantMap &map) { m_versionNumber = map.value(QLatin1String(QnxVersionKey), 0).toInt(); - RemoteLinux::LinuxDevice::fromMap(map); + LinuxDevice::fromMap(map); } QVariantMap QnxDevice::toMap() const { - QVariantMap map(RemoteLinux::LinuxDevice::toMap()); + QVariantMap map(LinuxDevice::toMap()); map.insert(QLatin1String(QnxVersionKey), m_versionNumber); return map; } @@ -160,6 +214,11 @@ QtcProcess *QnxDevice::createProcess(QObject *parent) const return new QnxDeviceProcess(sharedFromThis(), parent); } +Utils::ProcessInterface *QnxDevice::createProcessInterface() const +{ + return new QnxProcessImpl(this); +} + DeviceProcessSignalOperation::Ptr QnxDevice::signalOperation() const { return DeviceProcessSignalOperation::Ptr( diff --git a/src/plugins/qnx/qnxdevice.h b/src/plugins/qnx/qnxdevice.h index cd9f91fca6c..f537ffdec97 100644 --- a/src/plugins/qnx/qnxdevice.h +++ b/src/plugins/qnx/qnxdevice.h @@ -48,6 +48,7 @@ public: ProjectExplorer::DeviceTester *createDeviceTester() const override; Utils::QtcProcess *createProcess(QObject *parent) const override; + Utils::ProcessInterface *createProcessInterface() const override; int qnxVersion() const; diff --git a/src/plugins/remotelinux/CMakeLists.txt b/src/plugins/remotelinux/CMakeLists.txt index fb9299c3564..9b41edbc305 100644 --- a/src/plugins/remotelinux/CMakeLists.txt +++ b/src/plugins/remotelinux/CMakeLists.txt @@ -42,6 +42,7 @@ add_qtc_plugin(RemoteLinux remotelinuxx11forwardingaspect.cpp remotelinuxx11forwardingaspect.h rsyncdeploystep.cpp rsyncdeploystep.h sshkeydeployer.cpp sshkeydeployer.h + sshprocessinterface.h tarpackagecreationstep.cpp tarpackagecreationstep.h uploadandinstalltarpackagestep.cpp uploadandinstalltarpackagestep.h ) diff --git a/src/plugins/remotelinux/linuxdevice.cpp b/src/plugins/remotelinux/linuxdevice.cpp index 64746dc771c..bcc6642cd94 100644 --- a/src/plugins/remotelinux/linuxdevice.cpp +++ b/src/plugins/remotelinux/linuxdevice.cpp @@ -33,6 +33,7 @@ #include "remotelinux_constants.h" #include "remotelinuxsignaloperation.h" #include "remotelinuxenvironmentreader.h" +#include "sshprocessinterface.h" #include #include @@ -40,7 +41,6 @@ #include #include -#include #include #include @@ -58,7 +58,9 @@ #include #include #include +#include #include +#include using namespace ProjectExplorer; using namespace QSsh; @@ -66,6 +68,8 @@ using namespace Utils; namespace RemoteLinux { +const QByteArray s_pidMarker = "__qtc"; + const char Delimiter0[] = "x--"; const char Delimiter1[] = "---"; @@ -75,6 +79,245 @@ static Q_LOGGING_CATEGORY(linuxDeviceLog, "qtc.remotelinux.device", QtWarningMsg //#define DEBUG(x) LOG(x) #define DEBUG(x) +class SshSharedConnection : public QObject +{ + Q_OBJECT + +public: + explicit SshSharedConnection(const SshConnectionParameters &sshParameters, QObject *parent = nullptr); + ~SshSharedConnection() override; + + SshConnectionParameters sshParameters() const { return m_sshParameters; } + void ref(); + void deref(); + void makeStale(); + + void connectToHost(); + void disconnectFromHost(); + + QProcess::ProcessState state() const; + SshConnectionInfo connectionInfo() const; + QString socketFilePath() const + { + QTC_ASSERT(m_masterSocketDir, return QString()); + return m_masterSocketDir->path() + "/cs"; + } + QStringList connectionOptions(const Utils::FilePath &binary) const + { + return m_sshParameters.connectionOptions(binary) << "-o" << ("ControlPath=" + socketFilePath()); + } + +signals: + void connected(const QString &socketFilePath); + void disconnected(const ProcessResultData &result); + + void autoDestructRequested(); + +private: + void emitError(QProcess::ProcessError processError, const QString &errorString); + void emitConnected(); + QString fullProcessError(const QString &sshErrorPrefix); + QStringList connectionArgs(const FilePath &binary) const + { return connectionOptions(binary) << m_sshParameters.host(); } + + const SshConnectionParameters m_sshParameters; + mutable SshConnectionInfo m_connInfo; + std::unique_ptr m_masterProcess; + std::unique_ptr m_masterSocketDir; + QTimer m_timer; + int m_ref = 0; + bool m_stale = false; +}; + +SshSharedConnection::SshSharedConnection(const SshConnectionParameters &sshParameters, QObject *parent) + : QObject(parent), m_sshParameters(sshParameters) +{ +} + +SshSharedConnection::~SshSharedConnection() +{ + QTC_CHECK(m_ref == 0); + disconnect(); + disconnectFromHost(); +} + +void SshSharedConnection::ref() +{ + ++m_ref; + m_timer.stop(); +} + +void SshSharedConnection::deref() +{ + QTC_ASSERT(m_ref, return); + if (--m_ref) + return; + if (m_stale) // no one uses it + deleteLater(); + // not stale, so someone may reuse it + m_timer.start(SshSettings::connectionSharingTimeout() * 1000 * 60); +} + +void SshSharedConnection::makeStale() +{ + m_stale = true; + if (!m_ref) // no one uses it + deleteLater(); +} + +void SshSharedConnection::connectToHost() +{ + if (state() != QProcess::NotRunning) + return; + + const FilePath sshBinary = SshSettings::sshFilePath(); + if (!sshBinary.exists()) { + emitError(QProcess::FailedToStart, tr("Cannot establish SSH connection: ssh binary " + "\"%1\" does not exist.").arg(sshBinary.toUserOutput())); + return; + } + + m_masterSocketDir.reset(new QTemporaryDir); + if (!m_masterSocketDir->isValid()) { + emitError(QProcess::FailedToStart, tr("Cannot establish SSH connection: Failed to create temporary " + "directory for control socket: %1") + .arg(m_masterSocketDir->errorString())); + m_masterSocketDir.reset(); + return; + } + + m_masterProcess.reset(new QtcProcess); + SshRemoteProcess::setupSshEnvironment(m_masterProcess.get()); + m_timer.setSingleShot(true); + connect(&m_timer, &QTimer::timeout, this, &SshSharedConnection::autoDestructRequested); + connect(m_masterProcess.get(), &QtcProcess::readyReadStandardOutput, [this] { + const QByteArray reply = m_masterProcess->readAllStandardOutput(); + if (reply == "\n") + emitConnected(); + }); + connect(m_masterProcess.get(), &QtcProcess::done, [this] { + const QProcess::ProcessError error = m_masterProcess->error(); + if (error == QProcess::FailedToStart) { + emitError(error, fullProcessError(tr("Cannot establish SSH connection. " + "Control process failed to start:"))); + return; + } else if (error != QProcess::UnknownError) { + emitError(error, fullProcessError(tr("SSH connection failure:"))); + return; + } + emit disconnected(m_masterProcess->resultData()); + }); + + QStringList args = QStringList{"-M", "-N", "-o", "ControlPersist=no", + "-o", "PermitLocalCommand=yes", // Enable local command + "-o", "LocalCommand=echo"} // Local command is executed after successfully + // connecting to the server. "echo" will print "\n" + // on the process output if everything went fine. + << connectionArgs(sshBinary); + if (!m_sshParameters.x11DisplayName.isEmpty()) { + args.prepend("-X"); + Environment env = m_masterProcess->environment(); + env.set("DISPLAY", m_sshParameters.x11DisplayName); + m_masterProcess->setEnvironment(env); + } + m_masterProcess->setCommand(CommandLine(sshBinary, args)); + m_masterProcess->start(); +} + +void SshSharedConnection::disconnectFromHost() +{ + m_masterProcess.reset(); + m_masterSocketDir.reset(); +} + +QProcess::ProcessState SshSharedConnection::state() const +{ + return m_masterProcess ? m_masterProcess->state() : QProcess::NotRunning; +} + +SshConnectionInfo SshSharedConnection::connectionInfo() const +{ + QTC_ASSERT(state() == QProcess::Running, return SshConnectionInfo()); + if (m_connInfo.isValid()) + return m_connInfo; + QtcProcess p; + const FilePath sshFilePath = SshSettings::sshFilePath(); + p.setCommand({sshFilePath, connectionArgs(sshFilePath) << "echo" << "-n" << "$SSH_CLIENT"}); + p.start(); + if (!p.waitForStarted() || !p.waitForFinished()) { +// qCWarning(Internal::sshLog) << "failed to retrieve connection info:" << p.errorString(); + return SshConnectionInfo(); + } + const QByteArrayList data = p.readAllStandardOutput().split(' '); + if (data.size() != 3) { +// qCWarning(Internal::sshLog) << "failed to retrieve connection info: unexpected output"; + return SshConnectionInfo(); + } + m_connInfo.localPort = data.at(1).toInt(); + if (m_connInfo.localPort == 0) { +// qCWarning(Internal::sshLog) << "failed to retrieve connection info: unexpected output"; + return SshConnectionInfo(); + } + if (!m_connInfo.localAddress.setAddress(QString::fromLatin1(data.first()))) { +// qCWarning(Internal::sshLog) << "failed to retrieve connection info: unexpected output"; + return SshConnectionInfo(); + } + m_connInfo.peerPort = m_sshParameters.port(); + m_connInfo.peerAddress.setAddress(m_sshParameters.host()); + return m_connInfo; +} + +void SshSharedConnection::emitError(QProcess::ProcessError error, const QString &errorString) +{ + emit disconnected({ 0, QProcess::NormalExit, error, errorString }); +} + +void SshSharedConnection::emitConnected() +{ + emit connected(socketFilePath()); +} + +QString SshSharedConnection::fullProcessError(const QString &sshErrorPrefix) +{ + QString error; + if (m_masterProcess->exitStatus() != QProcess::NormalExit) + error = m_masterProcess->errorString(); + const QByteArray stdErr = m_masterProcess->readAllStandardError(); + if (!stdErr.isEmpty()) { + if (!error.isEmpty()) + error.append('\n'); + error.append(QString::fromLocal8Bit(stdErr)); + } + + QString fullError = sshErrorPrefix; + if (!error.isEmpty()) + fullError.append('\n').append(error); + + return fullError; +} + +// SshConnectionHandle + +class SshConnectionHandle : public QObject +{ + Q_OBJECT +public: + SshConnectionHandle(const IDevice::ConstPtr &device) : m_device(device) {} + ~SshConnectionHandle() override { emit detachFromSharedConnection(); } + +signals: + // direction: connection -> caller + void connected(const QString &socketFilePath); + void disconnected(const ProcessResultData &result); + // direction: caller -> connection + void detachFromSharedConnection(); + +private: + // Store the IDevice::ConstPtr in order to extend the lifetime of device for as long + // as this object is alive. + IDevice::ConstPtr m_device; +}; + static QString visualizeNull(QString s) { return s.replace(QLatin1Char('\0'), QLatin1String("")); @@ -186,37 +429,425 @@ class LinuxPortsGatheringMethod : public PortsGatheringMethod } }; +// LinuxDevicePrivate + +class ShellThreadHandler; + +class LinuxDevicePrivate +{ +public: + explicit LinuxDevicePrivate(LinuxDevice *parent); + ~LinuxDevicePrivate(); + + bool setupShell(); + bool runInShell(const CommandLine &cmd, const QByteArray &data = {}); + QByteArray outputForRunInShell(const QString &cmd); + QByteArray outputForRunInShell(const CommandLine &cmd); + void attachToSharedConnection(SshConnectionHandle *connectionHandle, + const SshConnectionParameters &sshParameters); + + LinuxDevice *q = nullptr; + QThread m_shellThread; + ShellThreadHandler *m_handler = nullptr; + mutable QMutex m_shellMutex; + mutable QMutex m_sharedConnectionMutex; +}; + +// SshProcessImpl + +class SshProcessInterfacePrivate : public QObject +{ + Q_OBJECT + +public: + SshProcessInterfacePrivate(SshProcessInterface *sshInterface, LinuxDevicePrivate *devicePrivate); + + void start(); + void sendControlSignal(ControlSignal controlSignal); + + void handleConnected(const QString &socketFilePath); + void handleDisconnected(const ProcessResultData &result); + + void handleStarted(); + void handleDone(); + void handleReadyReadStandardOutput(); + void handleReadyReadStandardError(); + + void clearForStart(); + void doStart(); + CommandLine fullLocalCommandLine() const; + + SshProcessInterface *q = nullptr; + + qint64 m_processId = 0; + QtcProcess m_process; + LinuxDevicePrivate *m_devicePrivate = nullptr; + // Store the IDevice::ConstPtr in order to extend the lifetime of device for as long + // as this object is alive. + IDevice::ConstPtr m_device; + std::unique_ptr m_connectionHandle; + + QString m_socketFilePath; + SshConnectionParameters m_sshParameters; + bool m_connecting = false; + + ProcessResultData m_result; +}; + +SshProcessInterface::SshProcessInterface(const LinuxDevice *linuxDevice) + : d(new SshProcessInterfacePrivate(this, linuxDevice->d)) +{ +} + +SshProcessInterface::~SshProcessInterface() +{ + delete d; +} + +void SshProcessInterface::handleStarted(qint64 processId) +{ + emitStarted(processId); +} + +void SshProcessInterface::handleReadyReadStandardOutput(const QByteArray &outputData) +{ + emit readyRead(outputData, {}); +} + +void SshProcessInterface::emitStarted(qint64 processId) +{ + d->m_processId = processId; + emit started(processId); +} + +void SshProcessInterface::killIfRunning() +{ + if (d->m_process.state() == QProcess::Running) + sendControlSignal(ControlSignal::Kill); +} + +void SshProcessInterface::start() +{ + d->start(); +} + +qint64 SshProcessInterface::write(const QByteArray &data) +{ + Q_UNUSED(data) + QTC_CHECK(false); + return -1; +} + +void SshProcessInterface::sendControlSignal(ControlSignal controlSignal) +{ + d->sendControlSignal(controlSignal); +} + +bool SshProcessInterface::waitForStarted(int msecs) +{ + Q_UNUSED(msecs) + QTC_CHECK(false); + return false; +} + +bool SshProcessInterface::waitForReadyRead(int msecs) +{ + Q_UNUSED(msecs) + QTC_CHECK(false); + return false; +} + +bool SshProcessInterface::waitForFinished(int msecs) +{ + Q_UNUSED(msecs) + QTC_CHECK(false); + return false; +} + +class LinuxProcessImpl final : public SshProcessInterface +{ + Q_OBJECT + +public: + LinuxProcessImpl(const LinuxDevice *linuxDevice); + ~LinuxProcessImpl() { killIfRunning(); } + +private: + void handleStarted(qint64 processId) final; + void handleReadyReadStandardOutput(const QByteArray &outputData) final; + + QString fullCommandLine(const Utils::CommandLine &commandLine) const final; + + QByteArray m_output; + bool m_pidParsed = false; +}; + +LinuxProcessImpl::LinuxProcessImpl(const LinuxDevice *linuxDevice) + : SshProcessInterface(linuxDevice) +{ +} + +QString LinuxProcessImpl::fullCommandLine(const CommandLine &commandLine) const +{ + CommandLine cmd; + + const QStringList rcFilesToSource = {"/etc/profile", "$HOME/.profile"}; + for (const QString &filePath : rcFilesToSource) { + cmd.addArgs({"test", "-f", filePath}); + cmd.addArgs("&&", CommandLine::Raw); + cmd.addArgs({".", filePath}); + cmd.addArgs(";", CommandLine::Raw); + } + + if (!m_setup.m_workingDirectory.isEmpty()) { + cmd.addArgs({"cd", m_setup.m_workingDirectory.path()}); + cmd.addArgs("&&", CommandLine::Raw); + } + + if (m_setup.m_terminalMode == TerminalMode::Off) + cmd.addArgs(QString("echo ") + s_pidMarker + "$$" + s_pidMarker + " && ", CommandLine::Raw); + + const Environment &env = m_setup.m_remoteEnvironment; + for (auto it = env.constBegin(); it != env.constEnd(); ++it) + cmd.addArgs(env.key(it) + "='" + env.expandedValueForKey(env.key(it)) + '\'', CommandLine::Raw); + + if (m_setup.m_terminalMode == TerminalMode::Off) + cmd.addArg("exec"); + + cmd.addCommandLineAsArgs(commandLine, CommandLine::Raw); + return cmd.arguments(); +} + +void LinuxProcessImpl::handleStarted(qint64 processId) +{ + // Don't emit started() when terminal is off, + // it's being done later inside handleReadyReadStandardOutput(). + if (m_setup.m_terminalMode == TerminalMode::Off) + return; + + emitStarted(processId); +} + +void LinuxProcessImpl::handleReadyReadStandardOutput(const QByteArray &outputData) +{ + if (m_pidParsed || m_setup.m_terminalMode != TerminalMode::Off) { + emit readyRead(outputData, {}); + return; + } + + m_output.append(outputData); + + static const QByteArray endMarker = s_pidMarker + '\n'; + const int endMarkerOffset = m_output.indexOf(endMarker); + if (endMarkerOffset == -1) + return; + const int startMarkerOffset = m_output.indexOf(s_pidMarker); + if (startMarkerOffset == endMarkerOffset) // Only theoretically possible. + return; + const int pidStart = startMarkerOffset + s_pidMarker.length(); + const QByteArray pidString = m_output.mid(pidStart, endMarkerOffset - pidStart); + m_pidParsed = true; + 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()); + + emitStarted(processId); + + if (!m_output.isEmpty()) + emit readyRead(m_output, {}); + + m_output.clear(); +} + +SshProcessInterfacePrivate::SshProcessInterfacePrivate(SshProcessInterface *sshInterface, + LinuxDevicePrivate *devicePrivate) + : QObject(sshInterface) + , q(sshInterface) + , m_devicePrivate(devicePrivate) + , m_device(m_devicePrivate->q->sharedFromThis()) +{ + connect(&m_process, &QtcProcess::started, this, &SshProcessInterfacePrivate::handleStarted); + connect(&m_process, &QtcProcess::done, this, &SshProcessInterfacePrivate::handleDone); + connect(&m_process, &QtcProcess::readyReadStandardOutput, + this, &SshProcessInterfacePrivate::handleReadyReadStandardOutput); + connect(&m_process, &QtcProcess::readyReadStandardError, + this, &SshProcessInterfacePrivate::handleReadyReadStandardError); +} + +void SshProcessInterfacePrivate::start() +{ + clearForStart(); + + m_sshParameters = m_devicePrivate->q->sshParameters(); + // TODO: Do we really need it for master process? + m_sshParameters.x11DisplayName + = q->m_setup.m_extraData.value("Ssh.X11ForwardToDisplay").toString(); + if (SshSettings::connectionSharingEnabled()) { + m_connecting = true; + m_connectionHandle.reset(new SshConnectionHandle(m_devicePrivate->q->sharedFromThis())); + connect(m_connectionHandle.get(), &SshConnectionHandle::connected, + this, &SshProcessInterfacePrivate::handleConnected); + connect(m_connectionHandle.get(), &SshConnectionHandle::disconnected, + this, &SshProcessInterfacePrivate::handleDisconnected); + m_devicePrivate->attachToSharedConnection(m_connectionHandle.get(), m_sshParameters); + } else { + doStart(); + } +} + +static int controlSignalToInt(ControlSignal controlSignal) +{ + switch (controlSignal) { + case ControlSignal::Terminate: return 15; + case ControlSignal::Kill: return 9; + case ControlSignal::Interrupt: return 2; + case ControlSignal::KickOff: QTC_CHECK(false); return 0; + } + return 0; +} + +QString SshProcessInterface::pidArgumentForKill() const +{ + return QString::fromLatin1("-%1 %1").arg(d->m_processId); +} + +void SshProcessInterfacePrivate::sendControlSignal(ControlSignal controlSignal) +{ + QTC_ASSERT(controlSignal != ControlSignal::KickOff, return); + // TODO: In case if m_processId == 0 try sending a signal based on process name. + const QString args = QString::fromLatin1("-%1 %2") + .arg(controlSignalToInt(controlSignal)).arg(q->pidArgumentForKill()); + CommandLine command = { "kill", args, CommandLine::Raw }; + // Note: This blocking call takes up to 2 ms for local remote. + m_devicePrivate->runInShell(command); +} + +void SshProcessInterfacePrivate::handleConnected(const QString &socketFilePath) +{ + m_connecting = false; + m_socketFilePath = socketFilePath; + doStart(); +} + +void SshProcessInterfacePrivate::handleDisconnected(const ProcessResultData &result) +{ + ProcessResultData resultData = result; + if (m_connecting) + resultData.m_error = QProcess::FailedToStart; + + m_connecting = false; + if (m_connectionHandle) // TODO: should it disconnect from signals first? + m_connectionHandle.release()->deleteLater(); + + if (resultData.m_error != QProcess::UnknownError && m_process.state() != QProcess::NotRunning) + emit q->done(resultData); // TODO: don't emit done() on process finished afterwards +} + +void SshProcessInterfacePrivate::handleStarted() +{ + const qint64 processId = m_process.usesTerminal() ? m_process.processId() : 0; + // By default emits started signal, Linux impl doesn't emit it when terminal is off. + q->handleStarted(processId); +} + +void SshProcessInterfacePrivate::handleDone() +{ + m_connectionHandle.reset(); + emit q->done(m_process.resultData()); +} + +void SshProcessInterfacePrivate::handleReadyReadStandardOutput() +{ + q->handleReadyReadStandardOutput(m_process.readAllStandardOutput()); // by default emits signal. linux impl does custom parsing for processId and emits delayed start() - only when terminal is off +} + +void SshProcessInterfacePrivate::handleReadyReadStandardError() +{ + emit q->readyRead({}, m_process.readAllStandardError()); +} + +void SshProcessInterfacePrivate::clearForStart() +{ + m_result = {}; +} + +void SshProcessInterfacePrivate::doStart() +{ + m_process.setProcessImpl(q->m_setup.m_processImpl); + m_process.setProcessMode(q->m_setup.m_processMode); + m_process.setTerminalMode(q->m_setup.m_terminalMode); + // TODO: what about other fields from m_setup? + SshRemoteProcess::setupSshEnvironment(&m_process); + if (!m_sshParameters.x11DisplayName.isEmpty()) { + Environment env = m_process.environment(); + // Note: it seems this is no-op when shared connection is used. + // In this case the display is taken from master process. + env.set("DISPLAY", m_sshParameters.x11DisplayName); + m_process.setEnvironment(env); + } + m_process.setCommand(fullLocalCommandLine()); + m_process.start(); +} + +CommandLine SshProcessInterfacePrivate::fullLocalCommandLine() const +{ + Utils::CommandLine cmd{SshSettings::sshFilePath()}; + + if (!m_sshParameters.x11DisplayName.isEmpty()) + cmd.addArg("-X"); + if (q->m_setup.m_terminalMode != TerminalMode::Off) + cmd.addArg("-tt"); + + cmd.addArg("-q"); + + QStringList options = m_sshParameters.connectionOptions(SshSettings::sshFilePath()); + if (!m_socketFilePath.isEmpty()) + options << "-o" << ("ControlPath=" + m_socketFilePath); + options << m_sshParameters.host(); + cmd.addArgs(options); + + CommandLine remoteWithLocalPath = q->m_setup.m_commandLine; + FilePath executable; + executable.setPath(remoteWithLocalPath.executable().path()); + remoteWithLocalPath.setExecutable(executable); + + cmd.addArg(q->fullCommandLine(remoteWithLocalPath)); + return cmd; +} + // ShellThreadHandler +static SshConnectionParameters displayless(const SshConnectionParameters &sshParameters) +{ + SshConnectionParameters parameters = sshParameters; + parameters.x11DisplayName.clear(); + return parameters; +} + class ShellThreadHandler : public QObject { public: ~ShellThreadHandler() { - if (!m_shell) - return; - if (m_shell->isRunning()) { + if (m_shell && m_shell->isRunning()) { m_shell->write("exit\n"); m_shell->waitForFinished(); } - delete m_shell; + qDeleteAll(m_connections); } bool startFailed(const SshConnectionParameters ¶meters) { - delete m_shell; - m_shell = nullptr; + m_shell.reset(); qCDebug(linuxDeviceLog) << "Failed to connect to" << parameters.host(); return false; } bool start(const SshConnectionParameters ¶meters) { - // TODO: start here shared ssh connection if needed (take it from settings) - // connect to it - // wait for connected - m_shell = new SshRemoteProcess("/bin/sh", - parameters.connectionOptions(SshSettings::sshFilePath()) << parameters.host()); + m_shell.reset(new SshRemoteProcess("/bin/sh", + parameters.connectionOptions(SshSettings::sshFilePath()) << parameters.host())); m_shell->setProcessMode(ProcessMode::Writer); m_shell->start(); const bool startOK = m_shell->waitForStarted(); @@ -288,31 +919,76 @@ public: return output; } - bool isRunning() const { return m_shell; } + void setSshParameters(const SshConnectionParameters &sshParameters) + { + const SshConnectionParameters displaylessSshParameters = displayless(sshParameters); + + if (m_displaylessSshParameters == displaylessSshParameters) + return; + + // If displayless sshParameters don't match the old connections' sshParameters, then stale + // old connections (don't delete, as the last deref() to each one will delete them). + for (SshSharedConnection *connection : qAsConst(m_connections)) + connection->makeStale(); + m_connections.clear(); + m_displaylessSshParameters = displaylessSshParameters; + } + + QString attachToSharedConnection(SshConnectionHandle *connectionHandle, + const SshConnectionParameters &sshParameters) + { + setSshParameters(sshParameters); + + SshSharedConnection *matchingConnection = nullptr; + + // Find the matching connection + for (SshSharedConnection *connection : qAsConst(m_connections)) { + if (connection->sshParameters() == sshParameters) { + matchingConnection = connection; + break; + } + } + + // If no matching connection has been found, create a new one + if (!matchingConnection) { + matchingConnection = new SshSharedConnection(sshParameters); + connect(matchingConnection, &SshSharedConnection::autoDestructRequested, + this, [this, matchingConnection] { + // This slot is just for removing the matchingConnection from the connection list. + // The SshSharedConnection could have deleted itself otherwise. + m_connections.removeOne(matchingConnection); + matchingConnection->deleteLater(); + }); + m_connections.append(matchingConnection); + } + + matchingConnection->ref(); + + connect(matchingConnection, &SshSharedConnection::connected, + connectionHandle, &SshConnectionHandle::connected); + connect(matchingConnection, &SshSharedConnection::disconnected, + connectionHandle, &SshConnectionHandle::disconnected); + + connect(connectionHandle, &SshConnectionHandle::detachFromSharedConnection, + matchingConnection, &SshSharedConnection::deref, + Qt::BlockingQueuedConnection); // Ensure the signal is delivered before sender's + // destruction, otherwise we may get out of sync + // with ref count. + + if (matchingConnection->state() == QProcess::Running) + return matchingConnection->socketFilePath(); + + if (matchingConnection->state() == QProcess::NotRunning) + matchingConnection->connectToHost(); + + return {}; + } + + bool isRunning() const { return m_shell.get(); } private: - SshRemoteProcess *m_shell = nullptr; -}; - -// LinuxDevicePrivate - -class LinuxDevicePrivate -{ -public: - explicit LinuxDevicePrivate(LinuxDevice *parent); - ~LinuxDevicePrivate(); - - CommandLine fullLocalCommandLine(const CommandLine &remoteCommand, - TerminalMode terminalMode, - bool hasDisplay) const; - bool setupShell(); - bool runInShell(const CommandLine &cmd, const QByteArray &data = {}); - QByteArray outputForRunInShell(const QString &cmd); - QByteArray outputForRunInShell(const CommandLine &cmd); - - LinuxDevice *q = nullptr; - QThread m_shellThread; - ShellThreadHandler *m_handler = nullptr; - mutable QMutex m_shellMutex; + SshConnectionParameters m_displaylessSshParameters; + QList m_connections; + std::unique_ptr m_shell; }; // LinuxDevice @@ -442,8 +1118,8 @@ FilePath LinuxDevice::mapToGlobalPath(const FilePath &pathOnDevice) const return pathOnDevice; } FilePath result; - result.setScheme("ssh"); - result.setHost(userAtHost()); + result.setScheme("device"); + result.setHost(id().toString()); result.setPath(pathOnDevice.path()); return result; } @@ -457,39 +1133,9 @@ bool LinuxDevice::handlesFile(const FilePath &filePath) const return false; } -CommandLine LinuxDevicePrivate::fullLocalCommandLine(const CommandLine &remoteCommand, - TerminalMode terminalMode, - bool hasDisplay) const +ProcessInterface *LinuxDevice::createProcessInterface() const { - Utils::CommandLine cmd{SshSettings::sshFilePath()}; - const SshConnectionParameters parameters = q->sshParameters(); - - if (hasDisplay) - cmd.addArg("-X"); - if (terminalMode != TerminalMode::Off) - cmd.addArg("-tt"); - - cmd.addArg("-q"); - // TODO: currently this drops shared connection (-o ControlPath=socketFilePath) - cmd.addArgs(parameters.connectionOptions(SshSettings::sshFilePath()) << parameters.host()); - - CommandLine remoteWithLocalPath = remoteCommand; - FilePath executable = remoteWithLocalPath.executable(); - executable.setScheme({}); - executable.setHost({}); - remoteWithLocalPath.setExecutable(executable); - cmd.addArg(remoteWithLocalPath.toUserOutput()); - return cmd; -} - -void LinuxDevice::runProcess(QtcProcess &process) const -{ - QTC_ASSERT(!process.isRunning(), return); - - const bool hasDisplay = SshRemoteProcess::setupSshEnvironment(&process); - process.setCommand(d->fullLocalCommandLine(process.commandLine(), process.terminalMode(), - hasDisplay)); - process.start(); + return new LinuxProcessImpl(this); } LinuxDevicePrivate::LinuxDevicePrivate(LinuxDevice *parent) @@ -503,8 +1149,14 @@ LinuxDevicePrivate::LinuxDevicePrivate(LinuxDevice *parent) LinuxDevicePrivate::~LinuxDevicePrivate() { - m_shellThread.quit(); - m_shellThread.wait(); + auto closeShell = [this] { + m_shellThread.quit(); + m_shellThread.wait(); + }; + if (QThread::currentThread() == m_shellThread.thread()) + closeShell(); + else // We might be in a non-main thread now due to extended lifetime of IDevice::Ptr + QMetaObject::invokeMethod(&m_shellThread, closeShell, Qt::BlockingQueuedConnection); } bool LinuxDevicePrivate::setupShell() @@ -553,6 +1205,20 @@ QByteArray LinuxDevicePrivate::outputForRunInShell(const CommandLine &cmd) return outputForRunInShell(cmd.toUserOutput()); } +void LinuxDevicePrivate::attachToSharedConnection(SshConnectionHandle *connectionHandle, + const SshConnectionParameters &sshParameters) +{ + QString socketFilePath; + { + QMutexLocker locker(&m_sharedConnectionMutex); + QMetaObject::invokeMethod(m_handler, [this, connectionHandle, sshParameters] { + return m_handler->attachToSharedConnection(connectionHandle, sshParameters); + }, Qt::BlockingQueuedConnection, &socketFilePath); + } + if (!socketFilePath.isEmpty()) + emit connectionHandle->connected(socketFilePath); +} + bool LinuxDevice::isExecutableFile(const FilePath &filePath) const { QTC_ASSERT(handlesFile(filePath), return false); @@ -776,3 +1442,5 @@ LinuxDeviceFactory::LinuxDeviceFactory() } // namespace Internal } // namespace RemoteLinux + +#include "linuxdevice.moc" diff --git a/src/plugins/remotelinux/linuxdevice.h b/src/plugins/remotelinux/linuxdevice.h index 2e55cd5bb85..af3baeb22bd 100644 --- a/src/plugins/remotelinux/linuxdevice.h +++ b/src/plugins/remotelinux/linuxdevice.h @@ -82,7 +82,7 @@ public: QByteArray fileContents(const Utils::FilePath &filePath, qint64 limit, qint64 offset) const override; bool writeFileContents(const Utils::FilePath &filePath, const QByteArray &data) const override; QDateTime lastModified(const Utils::FilePath &filePath) const override; - void runProcess(Utils::QtcProcess &process) const override; + Utils::ProcessInterface *createProcessInterface() const override; qint64 fileSize(const Utils::FilePath &filePath) const override; qint64 bytesAvailable(const Utils::FilePath &filePath) const override; QFileDevice::Permissions permissions(const Utils::FilePath &filePath) const override; @@ -92,6 +92,7 @@ protected: LinuxDevice(); class LinuxDevicePrivate *d; + friend class SshProcessInterface; }; namespace Internal { diff --git a/src/plugins/remotelinux/remotelinux.qbs b/src/plugins/remotelinux/remotelinux.qbs index 8c8d8c63eed..332ee8de60c 100644 --- a/src/plugins/remotelinux/remotelinux.qbs +++ b/src/plugins/remotelinux/remotelinux.qbs @@ -91,6 +91,7 @@ Project { "rsyncdeploystep.h", "sshkeydeployer.cpp", "sshkeydeployer.h", + "sshprocessinterface.h", "tarpackagecreationstep.cpp", "tarpackagecreationstep.h", "uploadandinstalltarpackagestep.cpp", diff --git a/src/plugins/remotelinux/sshprocessinterface.h b/src/plugins/remotelinux/sshprocessinterface.h new file mode 100644 index 00000000000..3d1881090cb --- /dev/null +++ b/src/plugins/remotelinux/sshprocessinterface.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "remotelinux_export.h" + +#include + +namespace RemoteLinux { + +class LinuxDevice; +class SshProcessInterfacePrivate; + +class REMOTELINUX_EXPORT SshProcessInterface : public Utils::ProcessInterface +{ + Q_OBJECT + +public: + SshProcessInterface(const LinuxDevice *linuxDevice); + ~SshProcessInterface(); + +protected: + void emitStarted(qint64 processId); + // To be called from leaf destructor. + // Can't call it from SshProcessInterface destructor as it calls virtual method. + void killIfRunning(); + +private: + virtual void handleStarted(qint64 processId); + virtual void handleReadyReadStandardOutput(const QByteArray &outputData); + + virtual QString fullCommandLine(const Utils::CommandLine &commandLine) const = 0; + virtual QString pidArgumentForKill() const; + + void start() final; + qint64 write(const QByteArray &data) final; + void sendControlSignal(Utils::ControlSignal controlSignal) final; + + bool waitForStarted(int msecs) final; + bool waitForReadyRead(int msecs) final; + bool waitForFinished(int msecs) final; + + friend class SshProcessInterfacePrivate; + SshProcessInterfacePrivate *d = nullptr; +}; + +} // namespace RemoteLinux