diff options
Diffstat (limited to 'src/plugins/remotelinux/linuxdevice.cpp')
-rw-r--r-- | src/plugins/remotelinux/linuxdevice.cpp | 660 |
1 files changed, 294 insertions, 366 deletions
diff --git a/src/plugins/remotelinux/linuxdevice.cpp b/src/plugins/remotelinux/linuxdevice.cpp index 5d0d2d6b92e..cc44dddb3c4 100644 --- a/src/plugins/remotelinux/linuxdevice.cpp +++ b/src/plugins/remotelinux/linuxdevice.cpp @@ -11,14 +11,14 @@ #include "remotelinux_constants.h" #include "remotelinuxsignaloperation.h" #include "remotelinuxtr.h" -#include "sshprocessinterface.h" #include <coreplugin/icore.h> #include <coreplugin/messagemanager.h> +#include <projectexplorer/devicesupport/devicemanager.h> #include <projectexplorer/devicesupport/filetransfer.h> #include <projectexplorer/devicesupport/filetransferinterface.h> -#include <projectexplorer/devicesupport/sshdeviceprocesslist.h> +#include <projectexplorer/devicesupport/processlist.h> #include <projectexplorer/devicesupport/sshparameters.h> #include <projectexplorer/devicesupport/sshsettings.h> @@ -28,9 +28,10 @@ #include <utils/environment.h> #include <utils/hostosinfo.h> #include <utils/port.h> +#include <utils/portlist.h> +#include <utils/process.h> #include <utils/processinfo.h> #include <utils/qtcassert.h> -#include <utils/qtcprocess.h> #include <utils/stringutils.h> #include <utils/temporaryfile.h> @@ -51,9 +52,6 @@ namespace RemoteLinux { const QByteArray s_pidMarker = "__qtc"; -const char Delimiter0[] = "x--"; -const char Delimiter1[] = "---"; - static Q_LOGGING_CATEGORY(linuxDeviceLog, "qtc.remotelinux.device", QtWarningMsg); #define DEBUG(x) qCDebug(linuxDeviceLog) << x << '\n' @@ -94,7 +92,7 @@ private: QStringList connectionArgs(const FilePath &binary) const; const SshParameters m_sshParameters; - std::unique_ptr<QtcProcess> m_masterProcess; + std::unique_ptr<Process> m_masterProcess; std::unique_ptr<QTemporaryDir> m_masterSocketDir; QTimer m_timer; int m_ref = 0; @@ -160,11 +158,11 @@ void SshSharedConnection::connectToHost() return; } - m_masterProcess.reset(new QtcProcess); + m_masterProcess.reset(new Process); SshParameters::setupSshEnvironment(m_masterProcess.get()); m_timer.setSingleShot(true); connect(&m_timer, &QTimer::timeout, this, &SshSharedConnection::autoDestructRequested); - connect(m_masterProcess.get(), &QtcProcess::readyReadStandardOutput, this, [this] { + connect(m_masterProcess.get(), &Process::readyReadStandardOutput, this, [this] { const QByteArray reply = m_masterProcess->readAllRawStandardOutput(); if (reply == "\n") emitConnected(); @@ -172,7 +170,7 @@ void SshSharedConnection::connectToHost() }); // TODO: in case of refused connection we are getting the following on stdErr: // ssh: connect to host 127.0.0.1 port 22: Connection refused\r\n - connect(m_masterProcess.get(), &QtcProcess::done, this, [this] { + connect(m_masterProcess.get(), &Process::done, this, [this] { const ProcessResult result = m_masterProcess->result(); const ProcessResultData resultData = m_masterProcess->resultData(); if (result == ProcessResult::StartFailed) { @@ -276,77 +274,6 @@ private: IDevice::ConstPtr m_device; }; -static QString visualizeNull(QString s) -{ - return s.replace(QLatin1Char('\0'), QLatin1String("<null>")); -} - -class LinuxDeviceProcessList : public SshDeviceProcessList -{ -public: - LinuxDeviceProcessList(const IDevice::ConstPtr &device, QObject *parent) - : SshDeviceProcessList(device, parent) - { - } - -private: - QString listProcessesCommandLine() const override - { - return QString::fromLatin1( - "for dir in `ls -d /proc/[0123456789]*`; do " - "test -d $dir || continue;" // Decrease the likelihood of a race condition. - "echo $dir;" - "cat $dir/cmdline;echo;" // cmdline does not end in newline - "cat $dir/stat;" - "readlink $dir/exe;" - "printf '%1''%2';" - "done").arg(QLatin1String(Delimiter0)).arg(QLatin1String(Delimiter1)); - } - - QList<ProcessInfo> buildProcessList(const QString &listProcessesReply) const override - { - QList<ProcessInfo> processes; - const QStringList lines = listProcessesReply.split(QString::fromLatin1(Delimiter0) - + QString::fromLatin1(Delimiter1), Qt::SkipEmptyParts); - for (const QString &line : lines) { - const QStringList elements = line.split(QLatin1Char('\n')); - if (elements.count() < 4) { - qDebug("%s: Expected four list elements, got %d. Line was '%s'.", Q_FUNC_INFO, - int(elements.count()), qPrintable(visualizeNull(line))); - continue; - } - bool ok; - const int pid = elements.first().mid(6).toInt(&ok); - if (!ok) { - qDebug("%s: Expected number in %s. Line was '%s'.", Q_FUNC_INFO, - qPrintable(elements.first()), qPrintable(visualizeNull(line))); - continue; - } - QString command = elements.at(1); - command.replace(QLatin1Char('\0'), QLatin1Char(' ')); - if (command.isEmpty()) { - const QString &statString = elements.at(2); - const int openParenPos = statString.indexOf(QLatin1Char('(')); - const int closedParenPos = statString.indexOf(QLatin1Char(')'), openParenPos); - if (openParenPos == -1 || closedParenPos == -1) - continue; - command = QLatin1Char('[') - + statString.mid(openParenPos + 1, closedParenPos - openParenPos - 1) - + QLatin1Char(']'); - } - - ProcessInfo process; - process.processId = pid; - process.commandLine = command; - process.executable = elements.at(3); - processes.append(process); - } - - return Utils::sorted(std::move(processes)); - } -}; - - // LinuxDevicePrivate class ShellThreadHandler; @@ -382,11 +309,13 @@ public: Environment getEnvironment(); void invalidateEnvironmentCache(); + void checkOsType(); + void queryOsType(std::function<RunResult(const CommandLine &)> run); + LinuxDevice *q = nullptr; QThread m_shellThread; ShellThreadHandler *m_handler = nullptr; mutable QMutex m_shellMutex; - QList<QtcProcess *> m_terminals; LinuxDeviceFileAccess m_fileAccess{this}; QReadWriteLock m_environmentCacheLock; @@ -410,11 +339,8 @@ Environment LinuxDevicePrivate::getEnvironment() if (m_environmentCache.has_value()) return m_environmentCache.value(); - QtcProcess getEnvProc; - getEnvProc.setCommand({FilePath("env").onDevice(q->rootPath()), {}}); - Environment inEnv; - inEnv.setCombineWithDeviceEnvironment(false); - getEnvProc.setEnvironment(inEnv); + Process getEnvProc; + getEnvProc.setCommand({q->filePath("env"), {}}); getEnvProc.runBlocking(); const QString remoteOutput = getEnvProc.cleanedStdOut(); @@ -437,10 +363,8 @@ Environment LinuxDeviceFileAccess::deviceEnvironment() const class SshProcessInterfacePrivate : public QObject { - Q_OBJECT - public: - SshProcessInterfacePrivate(SshProcessInterface *sshInterface, LinuxDevicePrivate *devicePrivate); + SshProcessInterfacePrivate(SshProcessInterface *sshInterface, const IDevice::ConstPtr &device); void start(); @@ -463,47 +387,32 @@ public: // as this object is alive. IDevice::ConstPtr m_device; std::unique_ptr<SshConnectionHandle> m_connectionHandle; - QtcProcess m_process; - LinuxDevicePrivate *m_devicePrivate = nullptr; + Process m_process; QString m_socketFilePath; SshParameters m_sshParameters; + bool m_connecting = false; bool m_killed = false; ProcessResultData m_result; + + QByteArray m_output; + QByteArray m_error; + bool m_pidParsed = false; + bool m_useConnectionSharing = false; }; -SshProcessInterface::SshProcessInterface(const LinuxDevice *linuxDevice) - : d(new SshProcessInterfacePrivate(this, linuxDevice->d)) -{ -} +SshProcessInterface::SshProcessInterface(const IDevice::ConstPtr &device) + : d(new SshProcessInterfacePrivate(this, device)) +{} SshProcessInterface::~SshProcessInterface() { + killIfRunning(); delete d; } -void SshProcessInterface::handleStarted(qint64 processId) -{ - emitStarted(processId); -} - -void SshProcessInterface::handleDone(const ProcessResultData &resultData) -{ - emit done(resultData); -} - -void SshProcessInterface::handleReadyReadStandardOutput(const QByteArray &outputData) -{ - emit readyRead(outputData, {}); -} - -void SshProcessInterface::handleReadyReadStandardError(const QByteArray &errorData) -{ - emit readyRead({}, errorData); -} - void SshProcessInterface::emitStarted(qint64 processId) { d->m_processId = processId; @@ -525,7 +434,7 @@ qint64 SshProcessInterface::processId() const bool SshProcessInterface::runInShell(const CommandLine &command, const QByteArray &data) { - QtcProcess process; + Process process; CommandLine cmd = {d->m_device->filePath("/bin/sh"), {"-c"}}; QString tmp; ProcessArgs::addArg(&tmp, command.executable().path()); @@ -556,7 +465,7 @@ void SshProcessInterface::sendControlSignal(ControlSignal controlSignal) d->m_process.closeWriteChannel(); return; } - if (d->m_process.usesTerminal()) { + if (d->m_process.usesTerminal() || d->m_process.ptyData().has_value()) { switch (controlSignal) { case ControlSignal::Terminate: d->m_process.terminate(); break; case ControlSignal::Kill: d->m_process.kill(); break; @@ -569,17 +478,7 @@ void SshProcessInterface::sendControlSignal(ControlSignal controlSignal) handleSendControlSignal(controlSignal); } -LinuxProcessInterface::LinuxProcessInterface(const LinuxDevice *linuxDevice) - : SshProcessInterface(linuxDevice) -{ -} - -LinuxProcessInterface::~LinuxProcessInterface() -{ - killIfRunning(); -} - -void LinuxProcessInterface::handleSendControlSignal(ControlSignal controlSignal) +void SshProcessInterface::handleSendControlSignal(ControlSignal controlSignal) { QTC_ASSERT(controlSignal != ControlSignal::KickOff, return); QTC_ASSERT(controlSignal != ControlSignal::CloseWriteChannel, return); @@ -592,75 +491,58 @@ void LinuxProcessInterface::handleSendControlSignal(ControlSignal controlSignal) runInShell(command); } -QString LinuxProcessInterface::fullCommandLine(const CommandLine &commandLine) const +void SshProcessInterfacePrivate::handleStarted() { - CommandLine cmd; - - if (!commandLine.isEmpty()) { - 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_environment; - 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"); - - if (!commandLine.isEmpty()) - cmd.addCommandLineAsArgs(commandLine, CommandLine::Raw); - return cmd.arguments(); -} + const qint64 processId = m_process.usesTerminal() ? m_process.processId() : 0; -void LinuxProcessInterface::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) + if (q->m_setup.m_terminalMode == TerminalMode::Off && !q->m_setup.m_ptyData) return; m_pidParsed = true; - emitStarted(processId); + q->emitStarted(processId); } -void LinuxProcessInterface::handleDone(const ProcessResultData &resultData) +void SshProcessInterfacePrivate::handleDone() { - ProcessResultData finalData = resultData; + if (m_connectionHandle) // TODO: should it disconnect from signals first? + m_connectionHandle.release()->deleteLater(); + + ProcessResultData finalData = m_process.resultData(); if (!m_pidParsed) { finalData.m_error = QProcess::FailedToStart; finalData.m_errorString = Utils::joinStrings({finalData.m_errorString, QString::fromLocal8Bit(m_error)}, '\n'); } - emit done(finalData); + emit q->done(finalData); } -void LinuxProcessInterface::handleReadyReadStandardOutput(const QByteArray &outputData) +void SshProcessInterfacePrivate::handleReadyReadStandardOutput() { + // By default this forwards readyRead immediately, but only buffers the + // output in case the start signal is not emitted yet. + // In case the pid can be parsed now, the delayed started() is + // emitted, and any previously buffered output emitted now. + const QByteArray outputData = m_process.readAllRawStandardOutput(); + if (m_pidParsed) { - emit readyRead(outputData, {}); + emit q->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; + 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; @@ -670,59 +552,109 @@ 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); + q->emitStarted(processId); if (!m_output.isEmpty() || !m_error.isEmpty()) - emit readyRead(m_output, m_error); + emit q->readyRead(m_output, m_error); m_output.clear(); m_error.clear(); } -void LinuxProcessInterface::handleReadyReadStandardError(const QByteArray &errorData) +void SshProcessInterfacePrivate::handleReadyReadStandardError() { + // By default forwards readyRead immediately, but buffers it in + // case the start signal is not emitted yet. + const QByteArray errorData = m_process.readAllRawStandardError(); + if (m_pidParsed) { - emit readyRead({}, errorData); + emit q->readyRead({}, errorData); return; } m_error.append(errorData); } SshProcessInterfacePrivate::SshProcessInterfacePrivate(SshProcessInterface *sshInterface, - LinuxDevicePrivate *devicePrivate) + const IDevice::ConstPtr &device) : QObject(sshInterface) , q(sshInterface) - , m_device(devicePrivate->q->sharedFromThis()) + , m_device(device) , m_process(this) - , m_devicePrivate(devicePrivate) { - connect(&m_process, &QtcProcess::started, this, &SshProcessInterfacePrivate::handleStarted); - connect(&m_process, &QtcProcess::done, this, &SshProcessInterfacePrivate::handleDone); - connect(&m_process, &QtcProcess::readyReadStandardOutput, + connect(&m_process, &Process::started, this, &SshProcessInterfacePrivate::handleStarted); + connect(&m_process, &Process::done, this, &SshProcessInterfacePrivate::handleDone); + connect(&m_process, &Process::readyReadStandardOutput, this, &SshProcessInterfacePrivate::handleReadyReadStandardOutput); - connect(&m_process, &QtcProcess::readyReadStandardError, + connect(&m_process, &Process::readyReadStandardError, this, &SshProcessInterfacePrivate::handleReadyReadStandardError); } void SshProcessInterfacePrivate::start() { clearForStart(); + m_sshParameters = m_device->sshParameters(); + + const Id linkDeviceId = Id::fromSetting(m_device->extraData(Constants::LinkDevice)); + if (const IDevice::ConstPtr linkDevice = DeviceManager::instance()->find(linkDeviceId)) { + CommandLine cmd{linkDevice->filePath("ssh")}; + if (!m_sshParameters.userName().isEmpty()) { + cmd.addArg("-l"); + cmd.addArg(m_sshParameters.userName()); + } + cmd.addArg(m_sshParameters.host()); + + const bool useTerminal = q->m_setup.m_terminalMode != TerminalMode::Off + || q->m_setup.m_ptyData; + if (useTerminal) + cmd.addArg("-tt"); + + const CommandLine full = q->m_setup.m_commandLine; + if (!full.isEmpty()) { // Empty is ok in case of opening a terminal. + CommandLine inner; + const QString wd = q->m_setup.m_workingDirectory.path(); + if (!wd.isEmpty()) + inner.addCommandLineWithAnd({"cd", {wd}}); + if (!useTerminal) { + const QString pidArg = QString("%1\\$\\$%1").arg(QString::fromLatin1(s_pidMarker)); + inner.addCommandLineWithAnd({"echo", pidArg, CommandLine::Raw}); + } + inner.addCommandLineWithAnd(full); + cmd.addCommandLineAsSingleArg(inner); + } + + m_process.setProcessImpl(q->m_setup.m_processImpl); + m_process.setProcessMode(q->m_setup.m_processMode); + m_process.setTerminalMode(q->m_setup.m_terminalMode); + m_process.setPtyData(q->m_setup.m_ptyData); + m_process.setReaperTimeout(q->m_setup.m_reaperTimeout); + m_process.setWriteData(q->m_setup.m_writeData); + m_process.setCreateConsoleOnWindows(q->m_setup.m_createConsoleOnWindows); + m_process.setExtraData(q->m_setup.m_extraData); + + m_process.setCommand(cmd); + m_process.start(); + return; + } + + m_useConnectionSharing = SshSettings::connectionSharingEnabled(); - 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()) { + if (m_useConnectionSharing) { m_connecting = true; - m_connectionHandle.reset(new SshConnectionHandle(m_devicePrivate->q->sharedFromThis())); + m_connectionHandle.reset(new SshConnectionHandle(m_device)); m_connectionHandle->setParent(this); 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); + auto linuxDevice = m_device.dynamicCast<const LinuxDevice>(); + QTC_ASSERT(linuxDevice, handleDone(); return); + linuxDevice->connectionAccess() + ->attachToSharedConnection(m_connectionHandle.get(), m_sshParameters); } else { doStart(); } @@ -749,34 +681,6 @@ void SshProcessInterfacePrivate::handleDisconnected(const ProcessResultData &res 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() -{ - if (m_connectionHandle) // TODO: should it disconnect from signals first? - m_connectionHandle.release()->deleteLater(); - q->handleDone(m_process.resultData()); -} - -void SshProcessInterfacePrivate::handleReadyReadStandardOutput() -{ - // By default emits signal. LinuxProcessImpl does custom parsing for processId - // and emits delayed start() - only when terminal is off. - q->handleReadyReadStandardOutput(m_process.readAllRawStandardOutput()); -} - -void SshProcessInterfacePrivate::handleReadyReadStandardError() -{ - // By default emits signal. LinuxProcessImpl buffers the error channel until - // it emits delayed start() - only when terminal is off. - q->handleReadyReadStandardError(m_process.readAllRawStandardError()); -} - void SshProcessInterfacePrivate::clearForStart() { m_result = {}; @@ -787,8 +691,11 @@ 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); + m_process.setPtyData(q->m_setup.m_ptyData); m_process.setReaperTimeout(q->m_setup.m_reaperTimeout); m_process.setWriteData(q->m_setup.m_writeData); + m_process.setCreateConsoleOnWindows(q->m_setup.m_createConsoleOnWindows); + // TODO: what about other fields from m_setup? SshParameters::setupSshEnvironment(&m_process); if (!m_sshParameters.x11DisplayName.isEmpty()) { @@ -798,32 +705,72 @@ void SshProcessInterfacePrivate::doStart() env.set("DISPLAY", m_sshParameters.x11DisplayName); m_process.setControlEnvironment(env); } + m_process.setExtraData(q->m_setup.m_extraData); m_process.setCommand(fullLocalCommandLine()); m_process.start(); } CommandLine SshProcessInterfacePrivate::fullLocalCommandLine() const { - CommandLine cmd{SshSettings::sshFilePath()}; + const FilePath sshBinary = SshSettings::sshFilePath(); + const bool useTerminal = q->m_setup.m_terminalMode != TerminalMode::Off || q->m_setup.m_ptyData; + const bool usePidMarker = !useTerminal; + const bool sourceProfile = m_device->extraData(Constants::SourceProfile).toBool(); + const bool useX = !m_sshParameters.x11DisplayName.isEmpty(); + + CommandLine cmd{sshBinary}; - if (!m_sshParameters.x11DisplayName.isEmpty()) + if (useX) cmd.addArg("-X"); - if (q->m_setup.m_terminalMode != TerminalMode::Off) + if (useTerminal) cmd.addArg("-tt"); cmd.addArg("-q"); - QStringList options = m_sshParameters.connectionOptions(SshSettings::sshFilePath()); + cmd.addArgs(m_sshParameters.connectionOptions(sshBinary)); if (!m_socketFilePath.isEmpty()) - options << "-o" << ("ControlPath=" + m_socketFilePath); - options << m_sshParameters.host(); - cmd.addArgs(options); + cmd.addArgs({"-o", "ControlPath=" + m_socketFilePath}); + + cmd.addArg(m_sshParameters.host()); - CommandLine remoteWithLocalPath = q->m_setup.m_commandLine; - FilePath executable = FilePath::fromParts({}, {}, remoteWithLocalPath.executable().path()); - remoteWithLocalPath.setExecutable(executable); + CommandLine commandLine = q->m_setup.m_commandLine; + FilePath executable = FilePath::fromParts({}, {}, commandLine.executable().path()); + commandLine.setExecutable(executable); + + CommandLine inner; + + if (!commandLine.isEmpty() && sourceProfile) { + const QStringList rcFilesToSource = {"/etc/profile", "$HOME/.profile"}; + for (const QString &filePath : rcFilesToSource) { + inner.addArgs({"test", "-f", filePath}); + inner.addArgs("&&", CommandLine::Raw); + inner.addArgs({".", filePath}); + inner.addArgs(";", CommandLine::Raw); + } + } + + const FilePath &workingDirectory = q->m_setup.m_workingDirectory; + if (!workingDirectory.isEmpty()) { + inner.addArgs({"cd", workingDirectory.path()}); + inner.addArgs("&&", CommandLine::Raw); + } + + if (usePidMarker) + inner.addArgs(QString("echo ") + s_pidMarker + "$$" + s_pidMarker + " && ", CommandLine::Raw); + + const Environment &env = q->m_setup.m_environment; + env.forEachEntry([&](const QString &key, const QString &value, bool) { + inner.addArgs(key + "='" + env.expandVariables(value) + '\'', CommandLine::Raw); + }); + + if (!useTerminal && !commandLine.isEmpty()) + inner.addArg("exec"); + + if (!commandLine.isEmpty()) + inner.addCommandLineAsArgs(commandLine, CommandLine::Raw); + + cmd.addArg(inner.arguments()); - cmd.addArg(q->fullCommandLine(remoteWithLocalPath)); return cmd; } @@ -848,7 +795,7 @@ class ShellThreadHandler : public QObject } private: - void setupShellProcess(QtcProcess *shellProcess) override + void setupShellProcess(Process *shellProcess) override { SshParameters::setupSshEnvironment(shellProcess); shellProcess->setCommand(m_cmdLine); @@ -857,7 +804,7 @@ class ShellThreadHandler : public QObject CommandLine createFallbackCommand(const CommandLine &cmdLine) override { CommandLine result = cmdLine; - result.setExecutable(cmdLine.executable().onDevice(m_devicePath)); + result.setExecutable(m_devicePath.withNewMappedPath(cmdLine.executable())); // Needed? return result; } @@ -995,9 +942,16 @@ LinuxDevice::LinuxDevice() { setFileAccess(&d->m_fileAccess); setDisplayType(Tr::tr("Remote Linux")); - setDefaultDisplayName(Tr::tr("Remote Linux Device")); setOsType(OsTypeLinux); + setupId(IDevice::ManuallyAdded, Utils::Id()); + setType(Constants::GenericLinuxOsType); + setMachineType(IDevice::Hardware); + setFreePorts(PortList::fromString(QLatin1String("10000-10100"))); + SshParameters sshParams; + sshParams.timeout = 10; + setSshParameters(sshParams); + addDeviceAction({Tr::tr("Deploy Public Key..."), [](const IDevice::Ptr &device, QWidget *parent) { if (auto d = PublicKeyDeploymentDialog::createDialog(device, parent)) { d->exec(); @@ -1006,45 +960,31 @@ LinuxDevice::LinuxDevice() }}); setOpenTerminal([this](const Environment &env, const FilePath &workingDir) { - QtcProcess * const proc = new QtcProcess; - d->m_terminals.append(proc); - QObject::connect(proc, &QtcProcess::done, proc, [this, proc] { - if (proc->error() != QProcess::UnknownError) { - const QString errorString = proc->errorString(); - QString message; - if (proc->error() == QProcess::FailedToStart) - message = Tr::tr("Error starting remote shell."); - else if (errorString.isEmpty()) - message = Tr::tr("Error running remote shell."); - else - message = Tr::tr("Error running remote shell: %1").arg(errorString); - Core::MessageManager::writeDisrupting(message); - } - proc->deleteLater(); - d->m_terminals.removeOne(proc); - }); + Process proc; - // We recreate the same way that QtcProcess uses to create the actual environment. - const Environment finalEnv = (!env.hasChanges() && env.combineWithDeviceEnvironment()) - ? d->getEnvironment() - : env; // If we will not set any environment variables, we can leave out the shell executable // as the "ssh ..." call will automatically launch the default shell if there are // no arguments. But if we will set environment variables, we need to explicitly // specify the shell executable. - const QString shell = finalEnv.hasChanges() ? finalEnv.value_or("SHELL", "/bin/sh") - : QString(); - - proc->setCommand({filePath(shell), {}}); - proc->setTerminalMode(TerminalMode::On); - proc->setEnvironment(env); - proc->setWorkingDirectory(workingDir); - proc->start(); + const QString shell = env.hasChanges() ? env.value_or("SHELL", "/bin/sh") : QString(); + + proc.setCommand({filePath(shell), {}}); + proc.setTerminalMode(TerminalMode::Detached); + proc.setEnvironment(env); + proc.setWorkingDirectory(workingDir); + proc.start(); }); addDeviceAction({Tr::tr("Open Remote Shell"), [](const IDevice::Ptr &device, QWidget *) { device->openTerminal(Environment(), FilePath()); }}); + setQmlRunCommand(filePath("qml")); +} + +void LinuxDevice::_setOsType(Utils::OsType osType) +{ + qCDebug(linuxDeviceLog) << "Setting OS type to" << osType << "for" << displayName(); + IDevice::setOsType(osType); } LinuxDevice::~LinuxDevice() @@ -1057,38 +997,9 @@ IDeviceWidget *LinuxDevice::createWidget() return new Internal::GenericLinuxDeviceConfigurationWidget(sharedFromThis()); } -bool LinuxDevice::canAutoDetectPorts() const -{ - return true; -} - -PortsGatheringMethod LinuxDevice::portsGatheringMethod() const -{ - return { - [this](QAbstractSocket::NetworkLayerProtocol protocol) -> CommandLine { - // We might encounter the situation that protocol is given IPv6 - // but the consumer of the free port information decides to open - // an IPv4(only) port. As a result the next IPv6 scan will - // report the port again as open (in IPv6 namespace), while the - // same port in IPv4 namespace might still be blocked, and - // re-use of this port fails. - // GDBserver behaves exactly like this. - - Q_UNUSED(protocol) - - // /proc/net/tcp* covers /proc/net/tcp and /proc/net/tcp6 - return {filePath("sed"), - "-e 's/.*: [[:xdigit:]]*:\\([[:xdigit:]]\\{4\\}\\).*/\\1/g' /proc/net/tcp*", - CommandLine::Raw}; - }, - - &Port::parseFromSedOutput - }; -} - DeviceProcessList *LinuxDevice::createProcessListModel(QObject *parent) const { - return new LinuxDeviceProcessList(sharedFromThis(), parent); + return new ProcessList(sharedFromThis(), parent); } DeviceTester *LinuxDevice::createDeviceTester() const @@ -1127,7 +1038,7 @@ bool LinuxDevice::handlesFile(const FilePath &filePath) const ProcessInterface *LinuxDevice::createProcessInterface() const { - return new LinuxProcessInterface(this); + return new SshProcessInterface(sharedFromThis()); } LinuxDevicePrivate::LinuxDevicePrivate(LinuxDevice *parent) @@ -1142,7 +1053,6 @@ LinuxDevicePrivate::LinuxDevicePrivate(LinuxDevice *parent) LinuxDevicePrivate::~LinuxDevicePrivate() { - qDeleteAll(m_terminals); auto closeShell = [this] { m_shellThread.quit(); m_shellThread.wait(); @@ -1153,6 +1063,23 @@ LinuxDevicePrivate::~LinuxDevicePrivate() QMetaObject::invokeMethod(&m_shellThread, closeShell, Qt::BlockingQueuedConnection); } +void LinuxDevicePrivate::queryOsType(std::function<RunResult(const CommandLine &)> runInShell) +{ + const RunResult result = runInShell({"uname", {"-s"}, OsType::OsTypeLinux}); + if (result.exitCode != 0) + q->_setOsType(OsTypeOtherUnix); + const QString osName = QString::fromUtf8(result.stdOut).trimmed(); + if (osName == "Darwin") + q->_setOsType(OsTypeMac); + if (osName == "Linux") + q->_setOsType(OsTypeLinux); +} + +void LinuxDevicePrivate::checkOsType() +{ + queryOsType([this](const CommandLine &cmd) { return runInShell(cmd); }); +} + // Call me with shell mutex locked bool LinuxDevicePrivate::setupShell() { @@ -1166,6 +1093,10 @@ bool LinuxDevicePrivate::setupShell() QMetaObject::invokeMethod(m_handler, [this, sshParameters] { return m_handler->start(sshParameters); }, Qt::BlockingQueuedConnection, &ok); + + if (ok) { + queryOsType([this](const CommandLine &cmd) { return m_handler->runInShell(cmd); }); + } return ok; } @@ -1208,13 +1139,9 @@ static FilePaths dirsToCreate(const FilesToTransfer &files) return sorted(std::move(dirs)); } -static QByteArray transferCommand(const FileTransferDirection direction, bool link) +static QByteArray transferCommand(bool link) { - if (direction == FileTransferDirection::Upload) - return link ? "ln -s" : "put"; - if (direction == FileTransferDirection::Download) - return "get"; - return {}; + return link ? "ln -s" : "put"; } class SshTransferInterface : public FileTransferInterface @@ -1222,21 +1149,20 @@ class SshTransferInterface : public FileTransferInterface Q_OBJECT protected: - SshTransferInterface(const FileTransferSetupData &setup, LinuxDevicePrivate *devicePrivate) + SshTransferInterface(const FileTransferSetupData &setup, const IDevice::ConstPtr &device) : FileTransferInterface(setup) - , m_device(devicePrivate->q->sharedFromThis()) - , m_devicePrivate(devicePrivate) + , m_device(device) , m_process(this) { - m_direction = m_setup.m_files.isEmpty() ? FileTransferDirection::Invalid - : m_setup.m_files.first().direction(); SshParameters::setupSshEnvironment(&m_process); - connect(&m_process, &QtcProcess::readyReadStandardOutput, this, [this] { + connect(&m_process, &Process::readyReadStandardOutput, this, [this] { emit progress(QString::fromLocal8Bit(m_process.readAllRawStandardOutput())); }); - connect(&m_process, &QtcProcess::done, this, &SshTransferInterface::doneImpl); + connect(&m_process, &Process::done, this, &SshTransferInterface::doneImpl); } + IDevice::ConstPtr device() const { return m_device; } + bool handleError() { ProcessResultData resultData = m_process.resultData(); @@ -1270,10 +1196,9 @@ protected: } QString host() const { return m_sshParameters.host(); } - QString userAtHost() const { return m_sshParameters.userName() + '@' + m_sshParameters.host(); } + QString userAtHost() const { return m_sshParameters.userAtHost(); } - QtcProcess &process() { return m_process; } - FileTransferDirection direction() const { return m_direction; } + Process &process() { return m_process; } private: virtual void startImpl() = 0; @@ -1282,7 +1207,11 @@ private: void start() final { m_sshParameters = displayless(m_device->sshParameters()); - if (SshSettings::connectionSharingEnabled()) { + const Id linkDeviceId = Id::fromSetting(m_device->extraData(Constants::LinkDevice)); + const auto linkDevice = DeviceManager::instance()->find(linkDeviceId); + const bool useConnectionSharing = !linkDevice && SshSettings::connectionSharingEnabled(); + + if (useConnectionSharing) { m_connecting = true; m_connectionHandle.reset(new SshConnectionHandle(m_device)); m_connectionHandle->setParent(this); @@ -1290,7 +1219,10 @@ private: this, &SshTransferInterface::handleConnected); connect(m_connectionHandle.get(), &SshConnectionHandle::disconnected, this, &SshTransferInterface::handleDisconnected); - m_devicePrivate->attachToSharedConnection(m_connectionHandle.get(), m_sshParameters); + auto linuxDevice = m_device.dynamicCast<const LinuxDevice>(); + QTC_ASSERT(linuxDevice, startFailed("No Linux device"); return); + linuxDevice->connectionAccess() + ->attachToSharedConnection(m_connectionHandle.get(), m_sshParameters); } else { startImpl(); } @@ -1318,28 +1250,33 @@ private: } IDevice::ConstPtr m_device; - LinuxDevicePrivate *m_devicePrivate = nullptr; SshParameters m_sshParameters; - FileTransferDirection m_direction = FileTransferDirection::Invalid; // helper // ssh shared connection related std::unique_ptr<SshConnectionHandle> m_connectionHandle; QString m_socketFilePath; bool m_connecting = false; - QtcProcess m_process; + Process m_process; }; class SftpTransferImpl : public SshTransferInterface { public: - SftpTransferImpl(const FileTransferSetupData &setup, LinuxDevicePrivate *devicePrivate) - : SshTransferInterface(setup, devicePrivate) { } + SftpTransferImpl(const FileTransferSetupData &setup, const IDevice::ConstPtr &device) + : SshTransferInterface(setup, device) + {} private: void startImpl() final { - const FilePath sftpBinary = SshSettings::sftpFilePath(); + FilePath sftpBinary = SshSettings::sftpFilePath(); + + // This is a hack. We only test the last hop here. + const Id linkDeviceId = Id::fromSetting(device()->extraData(Constants::LinkDevice)); + if (const auto linkDevice = DeviceManager::instance()->find(linkDeviceId)) + sftpBinary = linkDevice->filePath(sftpBinary.fileName()).searchInPath(); + if (!sftpBinary.exists()) { startFailed(Tr::tr("\"sftp\" binary \"%1\" does not exist.") .arg(sftpBinary.toUserOutput())); @@ -1349,35 +1286,26 @@ private: QByteArray batchData; const FilePaths dirs = dirsToCreate(m_setup.m_files); - for (const FilePath &dir : dirs) { - if (direction() == FileTransferDirection::Upload) { - batchData += "-mkdir " + ProcessArgs::quoteArgUnix(dir.path()).toLocal8Bit() + '\n'; - } else if (direction() == FileTransferDirection::Download) { - if (!QDir::root().mkpath(dir.path())) { - startFailed(Tr::tr("Failed to create local directory \"%1\".") - .arg(QDir::toNativeSeparators(dir.path()))); - return; - } - } - } + for (const FilePath &dir : dirs) + batchData += "-mkdir " + ProcessArgs::quoteArgUnix(dir.path()).toLocal8Bit() + '\n'; for (const FileToTransfer &file : m_setup.m_files) { FilePath sourceFileOrLinkTarget = file.m_source; bool link = false; - if (direction() == FileTransferDirection::Upload) { - const QFileInfo fi(file.m_source.toFileInfo()); - if (fi.isSymLink()) { - link = true; - batchData += "-rm " + ProcessArgs::quoteArgUnix( - file.m_target.path()).toLocal8Bit() + '\n'; - // see QTBUG-5817. - sourceFileOrLinkTarget = - sourceFileOrLinkTarget.withNewPath(fi.dir().relativeFilePath(fi.symLinkTarget())); - } - } - batchData += transferCommand(direction(), link) + ' ' - + ProcessArgs::quoteArgUnix(sourceFileOrLinkTarget.path()).toLocal8Bit() + ' ' - + ProcessArgs::quoteArgUnix(file.m_target.path()).toLocal8Bit() + '\n'; + + const QFileInfo fi(file.m_source.toFileInfo()); + if (fi.isSymLink()) { + link = true; + batchData += "-rm " + ProcessArgs::quoteArgUnix( + file.m_target.path()).toLocal8Bit() + '\n'; + // see QTBUG-5817. + sourceFileOrLinkTarget = + sourceFileOrLinkTarget.withNewPath(fi.dir().relativeFilePath(fi.symLinkTarget())); + } + + batchData += transferCommand(link) + ' ' + + ProcessArgs::quoteArgUnix(sourceFileOrLinkTarget.path()).toLocal8Bit() + ' ' + + ProcessArgs::quoteArgUnix(file.m_target.path()).toLocal8Bit() + '\n'; } process().setCommand({sftpBinary, fullConnectionOptions() << "-b" << "-" << host()}); process().setWriteData(batchData); @@ -1390,8 +1318,8 @@ private: class RsyncTransferImpl : public SshTransferInterface { public: - RsyncTransferImpl(const FileTransferSetupData &setup, LinuxDevicePrivate *devicePrivate) - : SshTransferInterface(setup, devicePrivate) + RsyncTransferImpl(const FileTransferSetupData &setup, const IDevice::ConstPtr &device) + : SshTransferInterface(setup, device) { } private: @@ -1442,8 +1370,7 @@ private: if (!HostOsInfo::isWindowsHost()) return file; - QString localFilePath = direction() == FileTransferDirection::Upload - ? file.m_source.path() : file.m_target.path(); + QString localFilePath = file.m_source.path(); localFilePath = '/' + localFilePath.at(0) + localFilePath.mid(2); if (anyOf(options, [](const QString &opt) { return opt.contains("cygwin", Qt::CaseInsensitive); })) { @@ -1451,30 +1378,19 @@ private: } FileToTransfer fixedFile = file; - if (direction() == FileTransferDirection::Upload) - fixedFile.m_source = fixedFile.m_source.withNewPath(localFilePath); - else - fixedFile.m_target = fixedFile.m_target.withNewPath(localFilePath); + fixedFile.m_source = fixedFile.m_source.withNewPath(localFilePath); return fixedFile; } QPair<QString, QString> fixPaths(const FileToTransfer &file, const QString &remoteHost) const { - FilePath localPath; - FilePath remotePath; - if (direction() == FileTransferDirection::Upload) { - localPath = file.m_source; - remotePath = file.m_target; - } else { - remotePath = file.m_source; - localPath = file.m_target; - } + FilePath localPath = file.m_source; + FilePath remotePath = file.m_target; const QString local = (localPath.isDir() && localPath.path().back() != '/') ? localPath.path() + '/' : localPath.path(); const QString remote = remoteHost + ':' + remotePath.path(); - return direction() == FileTransferDirection::Upload ? qMakePair(local, remote) - : qMakePair(remote, local); + return qMakePair(local, remote); } int m_currentIndex = 0; @@ -1483,7 +1399,7 @@ private: class GenericTransferImpl : public FileTransferInterface { public: - GenericTransferImpl(const FileTransferSetupData &setup, LinuxDevicePrivate *) + GenericTransferImpl(const FileTransferSetupData &setup) : FileTransferInterface(setup) {} @@ -1525,8 +1441,9 @@ private: .arg(m_currentIndex) .arg(m_fileCount) .arg(source.toUserOutput(), target.toUserOutput())); - if (!source.copyFile(target)) { - result.m_errorString = Tr::tr("Failed."); + expected_str<void> copyResult = source.copyFile(target); + if (!copyResult) { + result.m_errorString = Tr::tr("Failed: %1").arg(copyResult.error()); result.m_exitCode = -1; // Random pick emit done(result); return; @@ -1545,14 +1462,24 @@ FileTransferInterface *LinuxDevice::createFileTransferInterface( const FileTransferSetupData &setup) const { switch (setup.m_method) { - case FileTransferMethod::Sftp: return new SftpTransferImpl(setup, d); - case FileTransferMethod::Rsync: return new RsyncTransferImpl(setup, d); - case FileTransferMethod::GenericCopy: return new GenericTransferImpl(setup, d); + case FileTransferMethod::Sftp: return new SftpTransferImpl(setup, sharedFromThis()); + case FileTransferMethod::Rsync: return new RsyncTransferImpl(setup, sharedFromThis()); + case FileTransferMethod::GenericCopy: return new GenericTransferImpl(setup); } QTC_CHECK(false); return {}; } +LinuxDevicePrivate *LinuxDevice::connectionAccess() const +{ + return d; +} + +void LinuxDevice::checkOsType() +{ + d->checkOsType(); +} + namespace Internal { // Factory @@ -1563,6 +1490,7 @@ LinuxDeviceFactory::LinuxDeviceFactory() setDisplayName(Tr::tr("Remote Linux Device")); setIcon(QIcon()); setConstructionFunction(&LinuxDevice::create); + setQuickCreationAllowed(true); setCreator([] { GenericLinuxDeviceConfigurationWizard wizard(Core::ICore::dialogParent()); if (wizard.exec() != QDialog::Accepted) |