forked from qt-creator/qt-creator
RemoteLinux: Separate file transfer out of LinuxDevice
... into a new file pair again. The interface is essentially just SshConnectionHandle. Change-Id: I115fcefbfca4606c6440f97efb3e71121c87ee52 Reviewed-by: Christian Kandeler <christian.kandeler@qt.io>
This commit is contained in:
@@ -21,6 +21,7 @@ add_qtc_plugin(RemoteLinux
|
||||
remotelinuxdebugsupport.cpp remotelinuxdebugsupport.h
|
||||
remotelinuxdeploysupport.cpp remotelinuxdeploysupport.h
|
||||
remotelinuxenvironmentaspect.cpp remotelinuxenvironmentaspect.h
|
||||
remotelinuxfiletransfer.cpp remotelinuxfiletransfer.h
|
||||
remotelinuxplugin.cpp
|
||||
remotelinuxrunconfiguration.cpp remotelinuxrunconfiguration.h
|
||||
remotelinuxsignaloperation.cpp remotelinuxsignaloperation.h
|
||||
|
@@ -8,6 +8,7 @@
|
||||
#include "linuxprocessinterface.h"
|
||||
#include "publickeydeploymentdialog.h"
|
||||
#include "remotelinux_constants.h"
|
||||
#include "remotelinuxfiletransfer.h"
|
||||
#include "remotelinuxsignaloperation.h"
|
||||
#include "remotelinuxtr.h"
|
||||
#include "sshdevicewizard.h"
|
||||
@@ -16,15 +17,11 @@
|
||||
#include <coreplugin/messagemanager.h>
|
||||
|
||||
#include <projectexplorer/devicesupport/devicemanager.h>
|
||||
#include <projectexplorer/devicesupport/filetransfer.h>
|
||||
#include <projectexplorer/devicesupport/filetransferinterface.h>
|
||||
#include <projectexplorer/devicesupport/processlist.h>
|
||||
#include <projectexplorer/devicesupport/sshparameters.h>
|
||||
#include <projectexplorer/devicesupport/sshsettings.h>
|
||||
#include <projectexplorer/projectexplorerconstants.h>
|
||||
|
||||
#include <solutions/tasking/tasktreerunner.h>
|
||||
|
||||
#include <utils/algorithm.h>
|
||||
#include <utils/async.h>
|
||||
#include <utils/devicefileaccess.h>
|
||||
@@ -56,6 +53,8 @@
|
||||
using namespace ProjectExplorer;
|
||||
using namespace Utils;
|
||||
|
||||
using namespace RemoteLinux::Internal;
|
||||
|
||||
namespace RemoteLinux {
|
||||
|
||||
const QByteArray s_pidMarker = "__qtc";
|
||||
@@ -261,28 +260,6 @@ QStringList SshSharedConnection::connectionArgs(const FilePath &binary) const
|
||||
<< m_sshParameters.host();
|
||||
}
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
// LinuxDevicePrivate
|
||||
|
||||
class ShellThreadHandler;
|
||||
@@ -728,8 +705,7 @@ void SshProcessInterfacePrivate::start()
|
||||
this, &SshProcessInterfacePrivate::handleConnected);
|
||||
connect(m_connectionHandle.get(), &SshConnectionHandle::disconnected,
|
||||
this, &SshProcessInterfacePrivate::handleDisconnected);
|
||||
linuxDevice->connectionAccess()
|
||||
->attachToSharedConnection(m_connectionHandle.get(), m_sshParameters);
|
||||
linuxDevice->attachToSharedConnection(m_connectionHandle.get(), m_sshParameters);
|
||||
} else {
|
||||
doStart();
|
||||
}
|
||||
@@ -1275,403 +1251,16 @@ void LinuxDeviceAccess::attachToSharedConnection(SshConnectionHandle *connection
|
||||
emit connectionHandle->connected(socketFilePath);
|
||||
}
|
||||
|
||||
static FilePaths dirsToCreate(const FilesToTransfer &files)
|
||||
{
|
||||
FilePaths dirs;
|
||||
for (const FileToTransfer &file : files) {
|
||||
FilePath parentDir = file.m_target.parentDir();
|
||||
while (true) {
|
||||
if (dirs.contains(parentDir) || QDir(parentDir.path()).isRoot())
|
||||
break;
|
||||
dirs << parentDir;
|
||||
parentDir = parentDir.parentDir();
|
||||
}
|
||||
}
|
||||
return sorted(std::move(dirs));
|
||||
}
|
||||
|
||||
static QByteArray transferCommand(bool link)
|
||||
{
|
||||
return link ? "ln -s" : "put -R";
|
||||
}
|
||||
|
||||
class SshTransferInterface : public FileTransferInterface
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
protected:
|
||||
SshTransferInterface(const FileTransferSetupData &setup, const IDevice::ConstPtr &device)
|
||||
: FileTransferInterface(setup)
|
||||
, m_device(device)
|
||||
, m_process(this)
|
||||
{
|
||||
SshParameters::setupSshEnvironment(&m_process);
|
||||
connect(&m_process, &Process::readyReadStandardOutput, this, [this] {
|
||||
emit progress(QString::fromLocal8Bit(m_process.readAllRawStandardOutput()));
|
||||
});
|
||||
connect(&m_process, &Process::done, this, &SshTransferInterface::doneImpl);
|
||||
}
|
||||
|
||||
IDevice::ConstPtr device() const { return m_device; }
|
||||
|
||||
bool handleError()
|
||||
{
|
||||
ProcessResultData resultData = m_process.resultData();
|
||||
if (resultData.m_error == QProcess::FailedToStart) {
|
||||
resultData.m_errorString = Tr::tr("\"%1\" failed to start: %2")
|
||||
.arg(FileTransfer::transferMethodName(m_setup.m_method), resultData.m_errorString);
|
||||
} else if (resultData.m_exitStatus != QProcess::NormalExit) {
|
||||
resultData.m_errorString = Tr::tr("\"%1\" crashed.")
|
||||
.arg(FileTransfer::transferMethodName(m_setup.m_method));
|
||||
} else if (resultData.m_exitCode != 0) {
|
||||
resultData.m_errorString = QString::fromLocal8Bit(m_process.readAllRawStandardError());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
emit done(resultData);
|
||||
return true;
|
||||
}
|
||||
|
||||
void handleDone()
|
||||
{
|
||||
if (!handleError())
|
||||
emit done(m_process.resultData());
|
||||
}
|
||||
|
||||
QStringList fullConnectionOptions() const
|
||||
{
|
||||
QStringList options = m_sshParameters.connectionOptions(SshSettings::sshFilePath());
|
||||
if (!m_socketFilePath.isEmpty())
|
||||
options << "-o" << ("ControlPath=" + m_socketFilePath);
|
||||
return options;
|
||||
}
|
||||
|
||||
QString host() const { return m_sshParameters.host(); }
|
||||
QString userAtHost() const { return m_sshParameters.userAtHost(); }
|
||||
|
||||
Process &process() { return m_process; }
|
||||
|
||||
private:
|
||||
virtual void startImpl() = 0;
|
||||
virtual void doneImpl() = 0;
|
||||
|
||||
void start() final
|
||||
{
|
||||
m_sshParameters = displayless(m_device->sshParameters());
|
||||
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);
|
||||
connect(m_connectionHandle.get(), &SshConnectionHandle::connected,
|
||||
this, &SshTransferInterface::handleConnected);
|
||||
connect(m_connectionHandle.get(), &SshConnectionHandle::disconnected,
|
||||
this, &SshTransferInterface::handleDisconnected);
|
||||
auto linuxDevice = std::dynamic_pointer_cast<const LinuxDevice>(m_device);
|
||||
QTC_ASSERT(linuxDevice, startFailed("No Linux device"); return);
|
||||
linuxDevice->connectionAccess()
|
||||
->attachToSharedConnection(m_connectionHandle.get(), m_sshParameters);
|
||||
} else {
|
||||
startImpl();
|
||||
}
|
||||
}
|
||||
|
||||
void handleConnected(const QString &socketFilePath)
|
||||
{
|
||||
m_connecting = false;
|
||||
m_socketFilePath = socketFilePath;
|
||||
startImpl();
|
||||
}
|
||||
|
||||
void 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 done(resultData); // TODO: don't emit done() on process finished afterwards
|
||||
}
|
||||
|
||||
IDevice::ConstPtr m_device;
|
||||
SshParameters m_sshParameters;
|
||||
|
||||
// ssh shared connection related
|
||||
std::unique_ptr<SshConnectionHandle> m_connectionHandle;
|
||||
QString m_socketFilePath;
|
||||
bool m_connecting = false;
|
||||
|
||||
Process m_process;
|
||||
};
|
||||
|
||||
class SftpTransferImpl : public SshTransferInterface
|
||||
{
|
||||
public:
|
||||
SftpTransferImpl(const FileTransferSetupData &setup, const IDevice::ConstPtr &device)
|
||||
: SshTransferInterface(setup, device)
|
||||
{}
|
||||
|
||||
private:
|
||||
void startImpl() final
|
||||
{
|
||||
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()));
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray batchData;
|
||||
|
||||
const FilePaths dirs = dirsToCreate(m_setup.m_files);
|
||||
for (const FilePath &dir : dirs) {
|
||||
if (!dir.exists())
|
||||
batchData += "-mkdir " + ProcessArgs::quoteArgUnix(dir.path()).toLocal8Bit() + '\n';
|
||||
}
|
||||
|
||||
for (const FileToTransfer &file : m_setup.m_files) {
|
||||
FilePath sourceFileOrLinkTarget = file.m_source;
|
||||
bool link = false;
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
const QByteArray source = ProcessArgs::quoteArgUnix(sourceFileOrLinkTarget.path())
|
||||
.toLocal8Bit();
|
||||
const QByteArray target = ProcessArgs::quoteArgUnix(file.m_target.path()).toLocal8Bit();
|
||||
|
||||
batchData += transferCommand(link) + ' ' + source + ' ' + target + '\n';
|
||||
if (file.m_targetPermissions == FilePermissions::ForceExecutable)
|
||||
batchData += "chmod 1775 " + target + '\n';
|
||||
}
|
||||
process().setCommand({sftpBinary, {fullConnectionOptions(), "-b", "-", host()}});
|
||||
process().setWriteData(batchData);
|
||||
process().start();
|
||||
}
|
||||
|
||||
void doneImpl() final { handleDone(); }
|
||||
};
|
||||
|
||||
class RsyncTransferImpl : public SshTransferInterface
|
||||
{
|
||||
public:
|
||||
RsyncTransferImpl(const FileTransferSetupData &setup, const IDevice::ConstPtr &device)
|
||||
: SshTransferInterface(setup, device)
|
||||
{ }
|
||||
|
||||
private:
|
||||
void startImpl() final
|
||||
{
|
||||
// Note: This assumes that files do not get renamed when transferring.
|
||||
for (auto it = m_setup.m_files.cbegin(); it != m_setup.m_files.cend(); ++it)
|
||||
m_batches[it->m_target.parentDir()] << *it;
|
||||
startNextBatch();
|
||||
}
|
||||
|
||||
void doneImpl() final
|
||||
{
|
||||
if (m_batches.isEmpty())
|
||||
return handleDone();
|
||||
|
||||
if (handleError())
|
||||
return;
|
||||
|
||||
startNextBatch();
|
||||
}
|
||||
|
||||
void startNextBatch()
|
||||
{
|
||||
process().close();
|
||||
|
||||
const QString sshCmdLine = ProcessArgs::joinArgs(
|
||||
QStringList{SshSettings::sshFilePath().toUserOutput()}
|
||||
<< fullConnectionOptions(), OsTypeLinux);
|
||||
QStringList options{"-e", sshCmdLine};
|
||||
options << ProcessArgs::splitArgs(m_setup.m_rsyncFlags, HostOsInfo::hostOs());
|
||||
|
||||
if (!m_batches.isEmpty()) { // NormalRun
|
||||
const auto batchIt = m_batches.begin();
|
||||
for (auto filesIt = batchIt->cbegin(); filesIt != batchIt->cend(); ++filesIt) {
|
||||
const FileToTransfer fixedFile = fixLocalFileOnWindows(*filesIt, options);
|
||||
options << fixedFile.m_source.path();
|
||||
}
|
||||
options << fixedRemotePath(batchIt.key(), userAtHost());
|
||||
m_batches.erase(batchIt);
|
||||
} else { // TestRun
|
||||
options << "-n" << "--exclude=*" << (userAtHost() + ":/tmp");
|
||||
}
|
||||
// TODO: Get rsync location from settings?
|
||||
process().setCommand(CommandLine("rsync", options));
|
||||
process().start();
|
||||
}
|
||||
|
||||
// On Windows, rsync is either from msys or cygwin. Neither work with the other's ssh.exe.
|
||||
FileToTransfer fixLocalFileOnWindows(const FileToTransfer &file, const QStringList &options) const
|
||||
{
|
||||
if (!HostOsInfo::isWindowsHost())
|
||||
return file;
|
||||
|
||||
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); })) {
|
||||
localFilePath.prepend("/cygdrive");
|
||||
}
|
||||
|
||||
FileToTransfer fixedFile = file;
|
||||
fixedFile.m_source = fixedFile.m_source.withNewPath(localFilePath);
|
||||
return fixedFile;
|
||||
}
|
||||
|
||||
QString fixedRemotePath(const FilePath &file, const QString &remoteHost) const
|
||||
{
|
||||
return remoteHost + ':' + file.path();
|
||||
}
|
||||
|
||||
QHash<FilePath, FilesToTransfer> m_batches;
|
||||
};
|
||||
|
||||
static void createDir(QPromise<Result> &promise, const FilePath &pathToCreate)
|
||||
{
|
||||
const Result result = pathToCreate.ensureWritableDir();
|
||||
promise.addResult(result);
|
||||
|
||||
if (!result)
|
||||
promise.future().cancel();
|
||||
};
|
||||
|
||||
static void copyFile(QPromise<Result> &promise, const FileToTransfer &file)
|
||||
{
|
||||
const Result result = file.m_source.copyFile(file.m_target);
|
||||
promise.addResult(result);
|
||||
|
||||
if (!result)
|
||||
promise.future().cancel();
|
||||
};
|
||||
|
||||
class GenericTransferImpl : public FileTransferInterface
|
||||
{
|
||||
Tasking::TaskTreeRunner m_taskTree;
|
||||
|
||||
public:
|
||||
GenericTransferImpl(const FileTransferSetupData &setup)
|
||||
: FileTransferInterface(setup)
|
||||
{}
|
||||
|
||||
private:
|
||||
void start() final
|
||||
{
|
||||
using namespace Tasking;
|
||||
|
||||
const QSet<FilePath> allParentDirs
|
||||
= Utils::transform<QSet>(m_setup.m_files, [](const FileToTransfer &f) {
|
||||
return f.m_target.parentDir();
|
||||
});
|
||||
|
||||
const LoopList iteratorParentDirs(QList(allParentDirs.cbegin(), allParentDirs.cend()));
|
||||
|
||||
const auto onCreateDirSetup = [iteratorParentDirs](Async<Result> &async) {
|
||||
async.setConcurrentCallData(createDir, *iteratorParentDirs);
|
||||
};
|
||||
|
||||
const auto onCreateDirDone = [this,
|
||||
iteratorParentDirs](const Async<Result> &async) {
|
||||
const Result result = async.result();
|
||||
if (result)
|
||||
emit progress(
|
||||
Tr::tr("Created directory: \"%1\".\n").arg(iteratorParentDirs->toUserOutput()));
|
||||
else
|
||||
emit progress(result.error());
|
||||
};
|
||||
|
||||
const LoopList iterator(m_setup.m_files);
|
||||
const Storage<int> counterStorage;
|
||||
|
||||
const auto onCopySetup = [iterator](Async<Result> &async) {
|
||||
async.setConcurrentCallData(copyFile, *iterator);
|
||||
};
|
||||
|
||||
const auto onCopyDone = [this, iterator, counterStorage](
|
||||
const Async<Result> &async) {
|
||||
const Result result = async.result();
|
||||
int &counter = *counterStorage;
|
||||
++counter;
|
||||
|
||||
if (result) {
|
||||
//: %1/%2 = progress in the form 4/15, %3 and %4 = source and target file paths
|
||||
emit progress(Tr::tr("Copied %1/%2: \"%3\" -> \"%4\".\n")
|
||||
.arg(counter)
|
||||
.arg(m_setup.m_files.size())
|
||||
.arg(iterator->m_source.toUserOutput())
|
||||
.arg(iterator->m_target.toUserOutput()));
|
||||
} else {
|
||||
emit progress(result.error() + "\n");
|
||||
}
|
||||
};
|
||||
|
||||
const Group recipe {
|
||||
For (iteratorParentDirs) >> Do {
|
||||
parallelIdealThreadCountLimit,
|
||||
AsyncTask<Result>(onCreateDirSetup, onCreateDirDone),
|
||||
},
|
||||
For (iterator) >> Do {
|
||||
parallelLimit(2),
|
||||
counterStorage,
|
||||
AsyncTask<Result>(onCopySetup, onCopyDone),
|
||||
},
|
||||
};
|
||||
|
||||
m_taskTree.start(recipe, {}, [this](DoneWith result) {
|
||||
ProcessResultData resultData;
|
||||
if (result != DoneWith::Success) {
|
||||
resultData.m_exitCode = -1;
|
||||
resultData.m_errorString = Tr::tr("Failed to deploy files.");
|
||||
}
|
||||
emit done(resultData);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
FileTransferInterface *LinuxDevice::createFileTransferInterface(
|
||||
const FileTransferSetupData &setup) const
|
||||
{
|
||||
if (Utils::anyOf(setup.m_files,
|
||||
[](const FileToTransfer &f) { return !f.m_source.isLocal(); })) {
|
||||
return new GenericTransferImpl(setup);
|
||||
return Internal::createRemoteLinuxFileTransferInterface(*this, setup);
|
||||
}
|
||||
|
||||
switch (setup.m_method) {
|
||||
case FileTransferMethod::Sftp: return new SftpTransferImpl(setup, shared_from_this());
|
||||
case FileTransferMethod::Rsync: return new RsyncTransferImpl(setup, shared_from_this());
|
||||
case FileTransferMethod::GenericCopy: return new GenericTransferImpl(setup);
|
||||
}
|
||||
QTC_CHECK(false);
|
||||
return {};
|
||||
}
|
||||
|
||||
LinuxDeviceAccess *LinuxDevice::connectionAccess() const
|
||||
void LinuxDevice::attachToSharedConnection(SshConnectionHandle *sshConnectionHandle,
|
||||
const SshParameters &sshParams) const
|
||||
{
|
||||
return &d->m_scriptAccess;
|
||||
return d->m_scriptAccess.attachToSharedConnection(sshConnectionHandle, sshParams);
|
||||
}
|
||||
|
||||
void LinuxDevice::checkOsType()
|
||||
|
@@ -8,7 +8,30 @@
|
||||
#include <projectexplorer/devicesupport/idevice.h>
|
||||
#include <projectexplorer/devicesupport/idevicefactory.h>
|
||||
|
||||
namespace Utils { class ProcessResultData; }
|
||||
|
||||
namespace RemoteLinux {
|
||||
namespace Internal {
|
||||
|
||||
class SshConnectionHandle : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SshConnectionHandle(const ProjectExplorer::DeviceConstRef &device) : m_device(device) {}
|
||||
~SshConnectionHandle() override { emit detachFromSharedConnection(); }
|
||||
|
||||
signals:
|
||||
void connected(const QString &socketFilePath);
|
||||
void disconnected(const Utils::ProcessResultData &result);
|
||||
|
||||
void detachFromSharedConnection();
|
||||
|
||||
private:
|
||||
ProjectExplorer::DeviceConstRef m_device;
|
||||
};
|
||||
|
||||
} // Internal
|
||||
|
||||
class REMOTELINUX_EXPORT LinuxDevice : public ProjectExplorer::IDevice
|
||||
{
|
||||
@@ -38,7 +61,6 @@ public:
|
||||
ProjectExplorer::FileTransferInterface *createFileTransferInterface(
|
||||
const ProjectExplorer::FileTransferSetupData &setup) const override;
|
||||
|
||||
class LinuxDeviceAccess *connectionAccess() const;
|
||||
void checkOsType() override;
|
||||
|
||||
DeviceState deviceState() const override;
|
||||
@@ -47,6 +69,8 @@ public:
|
||||
bool isDisconnected() const;
|
||||
bool tryToConnect();
|
||||
|
||||
void attachToSharedConnection(Internal::SshConnectionHandle *sshConnectionHandle,
|
||||
const ProjectExplorer::SshParameters &sshParams) const;
|
||||
protected:
|
||||
LinuxDevice();
|
||||
|
||||
|
@@ -44,6 +44,8 @@ QtcPlugin {
|
||||
"remotelinuxdeploysupport.h",
|
||||
"remotelinuxenvironmentaspect.cpp",
|
||||
"remotelinuxenvironmentaspect.h",
|
||||
"remotelinuxfiletransfer.cpp",
|
||||
"remotelinuxfiletransfer.h",
|
||||
"remotelinuxplugin.cpp",
|
||||
"remotelinuxrunconfiguration.cpp",
|
||||
"remotelinuxrunconfiguration.h",
|
||||
|
434
src/plugins/remotelinux/remotelinuxfiletransfer.cpp
Normal file
434
src/plugins/remotelinux/remotelinuxfiletransfer.cpp
Normal file
@@ -0,0 +1,434 @@
|
||||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include "linuxdevice.h"
|
||||
|
||||
#include "remotelinux_constants.h"
|
||||
#include "remotelinuxtr.h"
|
||||
|
||||
#include <projectexplorer/devicesupport/devicemanager.h>
|
||||
#include <projectexplorer/devicesupport/filetransfer.h>
|
||||
#include <projectexplorer/devicesupport/filetransferinterface.h>
|
||||
#include <projectexplorer/devicesupport/processlist.h>
|
||||
#include <projectexplorer/devicesupport/sshparameters.h>
|
||||
#include <projectexplorer/devicesupport/sshsettings.h>
|
||||
#include <projectexplorer/projectexplorerconstants.h>
|
||||
|
||||
#include <solutions/tasking/tasktreerunner.h>
|
||||
|
||||
#include <utils/algorithm.h>
|
||||
#include <utils/async.h>
|
||||
#include <utils/processinfo.h>
|
||||
#include <utils/processinterface.h>
|
||||
#include <utils/qtcassert.h>
|
||||
#include <utils/qtcprocess.h>
|
||||
#include <utils/stringutils.h>
|
||||
#include <utils/temporaryfile.h>
|
||||
#include <utils/threadutils.h>
|
||||
|
||||
using namespace ProjectExplorer;
|
||||
using namespace Utils;
|
||||
|
||||
namespace RemoteLinux::Internal {
|
||||
|
||||
const QByteArray s_pidMarker = "__qtc";
|
||||
|
||||
static SshParameters displayless(const SshParameters &sshParameters)
|
||||
{
|
||||
SshParameters parameters = sshParameters;
|
||||
parameters.x11DisplayName.clear();
|
||||
return parameters;
|
||||
}
|
||||
|
||||
static FilePaths dirsToCreate(const FilesToTransfer &files)
|
||||
{
|
||||
FilePaths dirs;
|
||||
for (const FileToTransfer &file : files) {
|
||||
FilePath parentDir = file.m_target.parentDir();
|
||||
while (true) {
|
||||
if (dirs.contains(parentDir) || QDir(parentDir.path()).isRoot())
|
||||
break;
|
||||
dirs << parentDir;
|
||||
parentDir = parentDir.parentDir();
|
||||
}
|
||||
}
|
||||
return sorted(std::move(dirs));
|
||||
}
|
||||
|
||||
static QByteArray transferCommand(bool link)
|
||||
{
|
||||
return link ? "ln -s" : "put -R";
|
||||
}
|
||||
|
||||
class SshTransferInterface : public FileTransferInterface
|
||||
{
|
||||
protected:
|
||||
SshTransferInterface(const FileTransferSetupData &setup, const DeviceConstRef &device)
|
||||
: FileTransferInterface(setup)
|
||||
, m_device(device)
|
||||
, m_process(this)
|
||||
{
|
||||
SshParameters::setupSshEnvironment(&m_process);
|
||||
connect(&m_process, &Process::readyReadStandardOutput, this, [this] {
|
||||
emit progress(QString::fromLocal8Bit(m_process.readAllRawStandardOutput()));
|
||||
});
|
||||
connect(&m_process, &Process::done, this, &SshTransferInterface::doneImpl);
|
||||
}
|
||||
|
||||
DeviceConstRef device() const { return m_device; }
|
||||
|
||||
bool handleError()
|
||||
{
|
||||
ProcessResultData resultData = m_process.resultData();
|
||||
if (resultData.m_error == QProcess::FailedToStart) {
|
||||
resultData.m_errorString = Tr::tr("\"%1\" failed to start: %2")
|
||||
.arg(FileTransfer::transferMethodName(m_setup.m_method), resultData.m_errorString);
|
||||
} else if (resultData.m_exitStatus != QProcess::NormalExit) {
|
||||
resultData.m_errorString = Tr::tr("\"%1\" crashed.")
|
||||
.arg(FileTransfer::transferMethodName(m_setup.m_method));
|
||||
} else if (resultData.m_exitCode != 0) {
|
||||
resultData.m_errorString = QString::fromLocal8Bit(m_process.readAllRawStandardError());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
emit done(resultData);
|
||||
return true;
|
||||
}
|
||||
|
||||
void handleDone()
|
||||
{
|
||||
if (!handleError())
|
||||
emit done(m_process.resultData());
|
||||
}
|
||||
|
||||
QStringList fullConnectionOptions() const
|
||||
{
|
||||
QStringList options = m_sshParameters.connectionOptions(SshSettings::sshFilePath());
|
||||
if (!m_socketFilePath.isEmpty())
|
||||
options << "-o" << ("ControlPath=" + m_socketFilePath);
|
||||
return options;
|
||||
}
|
||||
|
||||
QString host() const { return m_sshParameters.host(); }
|
||||
QString userAtHost() const { return m_sshParameters.userAtHost(); }
|
||||
|
||||
Process &process() { return m_process; }
|
||||
|
||||
private:
|
||||
virtual void startImpl() = 0;
|
||||
virtual void doneImpl() = 0;
|
||||
|
||||
void start() final
|
||||
{
|
||||
m_sshParameters = displayless(m_device.sshParameters());
|
||||
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);
|
||||
connect(m_connectionHandle.get(), &SshConnectionHandle::connected,
|
||||
this, &SshTransferInterface::handleConnected);
|
||||
connect(m_connectionHandle.get(), &SshConnectionHandle::disconnected,
|
||||
this, &SshTransferInterface::handleDisconnected);
|
||||
auto linuxDevice = std::dynamic_pointer_cast<const LinuxDevice>(m_device.lock());
|
||||
QTC_ASSERT(linuxDevice, startFailed("No Linux device"); return);
|
||||
linuxDevice->attachToSharedConnection(m_connectionHandle.get(), m_sshParameters);
|
||||
} else {
|
||||
startImpl();
|
||||
}
|
||||
}
|
||||
|
||||
void handleConnected(const QString &socketFilePath)
|
||||
{
|
||||
m_connecting = false;
|
||||
m_socketFilePath = socketFilePath;
|
||||
startImpl();
|
||||
}
|
||||
|
||||
void 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 done(resultData); // TODO: don't emit done() on process finished afterwards
|
||||
}
|
||||
|
||||
DeviceConstRef m_device;
|
||||
SshParameters m_sshParameters;
|
||||
|
||||
// ssh shared connection related
|
||||
std::unique_ptr<SshConnectionHandle> m_connectionHandle;
|
||||
QString m_socketFilePath;
|
||||
bool m_connecting = false;
|
||||
|
||||
Process m_process;
|
||||
};
|
||||
|
||||
class SftpTransferImpl : public SshTransferInterface
|
||||
{
|
||||
public:
|
||||
SftpTransferImpl(const FileTransferSetupData &setup, const DeviceConstRef &device)
|
||||
: SshTransferInterface(setup, device)
|
||||
{}
|
||||
|
||||
private:
|
||||
void startImpl() final
|
||||
{
|
||||
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()));
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray batchData;
|
||||
|
||||
const FilePaths dirs = dirsToCreate(m_setup.m_files);
|
||||
for (const FilePath &dir : dirs) {
|
||||
if (!dir.exists())
|
||||
batchData += "-mkdir " + ProcessArgs::quoteArgUnix(dir.path()).toLocal8Bit() + '\n';
|
||||
}
|
||||
|
||||
for (const FileToTransfer &file : m_setup.m_files) {
|
||||
FilePath sourceFileOrLinkTarget = file.m_source;
|
||||
bool link = false;
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
const QByteArray source = ProcessArgs::quoteArgUnix(sourceFileOrLinkTarget.path())
|
||||
.toLocal8Bit();
|
||||
const QByteArray target = ProcessArgs::quoteArgUnix(file.m_target.path()).toLocal8Bit();
|
||||
|
||||
batchData += transferCommand(link) + ' ' + source + ' ' + target + '\n';
|
||||
if (file.m_targetPermissions == FilePermissions::ForceExecutable)
|
||||
batchData += "chmod 1775 " + target + '\n';
|
||||
}
|
||||
process().setCommand({sftpBinary, {fullConnectionOptions(), "-b", "-", host()}});
|
||||
process().setWriteData(batchData);
|
||||
process().start();
|
||||
}
|
||||
|
||||
void doneImpl() final { handleDone(); }
|
||||
};
|
||||
|
||||
class RsyncTransferImpl : public SshTransferInterface
|
||||
{
|
||||
public:
|
||||
RsyncTransferImpl(const FileTransferSetupData &setup, const DeviceConstRef &device)
|
||||
: SshTransferInterface(setup, device)
|
||||
{ }
|
||||
|
||||
private:
|
||||
void startImpl() final
|
||||
{
|
||||
// Note: This assumes that files do not get renamed when transferring.
|
||||
for (auto it = m_setup.m_files.cbegin(); it != m_setup.m_files.cend(); ++it)
|
||||
m_batches[it->m_target.parentDir()] << *it;
|
||||
startNextBatch();
|
||||
}
|
||||
|
||||
void doneImpl() final
|
||||
{
|
||||
if (m_batches.isEmpty())
|
||||
return handleDone();
|
||||
|
||||
if (handleError())
|
||||
return;
|
||||
|
||||
startNextBatch();
|
||||
}
|
||||
|
||||
void startNextBatch()
|
||||
{
|
||||
process().close();
|
||||
|
||||
const QString sshCmdLine = ProcessArgs::joinArgs(
|
||||
QStringList{SshSettings::sshFilePath().toUserOutput()}
|
||||
<< fullConnectionOptions(), OsTypeLinux);
|
||||
QStringList options{"-e", sshCmdLine};
|
||||
options << ProcessArgs::splitArgs(m_setup.m_rsyncFlags, HostOsInfo::hostOs());
|
||||
|
||||
if (!m_batches.isEmpty()) { // NormalRun
|
||||
const auto batchIt = m_batches.begin();
|
||||
for (auto filesIt = batchIt->cbegin(); filesIt != batchIt->cend(); ++filesIt) {
|
||||
const FileToTransfer fixedFile = fixLocalFileOnWindows(*filesIt, options);
|
||||
options << fixedFile.m_source.path();
|
||||
}
|
||||
options << fixedRemotePath(batchIt.key(), userAtHost());
|
||||
m_batches.erase(batchIt);
|
||||
} else { // TestRun
|
||||
options << "-n" << "--exclude=*" << (userAtHost() + ":/tmp");
|
||||
}
|
||||
// TODO: Get rsync location from settings?
|
||||
process().setCommand(CommandLine("rsync", options));
|
||||
process().start();
|
||||
}
|
||||
|
||||
// On Windows, rsync is either from msys or cygwin. Neither work with the other's ssh.exe.
|
||||
FileToTransfer fixLocalFileOnWindows(const FileToTransfer &file, const QStringList &options) const
|
||||
{
|
||||
if (!HostOsInfo::isWindowsHost())
|
||||
return file;
|
||||
|
||||
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); })) {
|
||||
localFilePath.prepend("/cygdrive");
|
||||
}
|
||||
|
||||
FileToTransfer fixedFile = file;
|
||||
fixedFile.m_source = fixedFile.m_source.withNewPath(localFilePath);
|
||||
return fixedFile;
|
||||
}
|
||||
|
||||
QString fixedRemotePath(const FilePath &file, const QString &remoteHost) const
|
||||
{
|
||||
return remoteHost + ':' + file.path();
|
||||
}
|
||||
|
||||
QHash<FilePath, FilesToTransfer> m_batches;
|
||||
};
|
||||
|
||||
static void createDir(QPromise<Result> &promise, const FilePath &pathToCreate)
|
||||
{
|
||||
const Result result = pathToCreate.ensureWritableDir();
|
||||
promise.addResult(result);
|
||||
|
||||
if (!result)
|
||||
promise.future().cancel();
|
||||
};
|
||||
|
||||
static void copyFile(QPromise<Result> &promise, const FileToTransfer &file)
|
||||
{
|
||||
const Result result = file.m_source.copyFile(file.m_target);
|
||||
promise.addResult(result);
|
||||
|
||||
if (!result)
|
||||
promise.future().cancel();
|
||||
};
|
||||
|
||||
class GenericTransferImpl : public FileTransferInterface
|
||||
{
|
||||
Tasking::TaskTreeRunner m_taskTree;
|
||||
|
||||
public:
|
||||
GenericTransferImpl(const FileTransferSetupData &setup)
|
||||
: FileTransferInterface(setup)
|
||||
{}
|
||||
|
||||
private:
|
||||
void start() final
|
||||
{
|
||||
using namespace Tasking;
|
||||
|
||||
const QSet<FilePath> allParentDirs
|
||||
= Utils::transform<QSet>(m_setup.m_files, [](const FileToTransfer &f) {
|
||||
return f.m_target.parentDir();
|
||||
});
|
||||
|
||||
const LoopList iteratorParentDirs(QList(allParentDirs.cbegin(), allParentDirs.cend()));
|
||||
|
||||
const auto onCreateDirSetup = [iteratorParentDirs](Async<Result> &async) {
|
||||
async.setConcurrentCallData(createDir, *iteratorParentDirs);
|
||||
};
|
||||
|
||||
const auto onCreateDirDone = [this,
|
||||
iteratorParentDirs](const Async<Result> &async) {
|
||||
const Result result = async.result();
|
||||
if (result)
|
||||
emit progress(
|
||||
Tr::tr("Created directory: \"%1\".\n").arg(iteratorParentDirs->toUserOutput()));
|
||||
else
|
||||
emit progress(result.error());
|
||||
};
|
||||
|
||||
const LoopList iterator(m_setup.m_files);
|
||||
const Storage<int> counterStorage;
|
||||
|
||||
const auto onCopySetup = [iterator](Async<Result> &async) {
|
||||
async.setConcurrentCallData(copyFile, *iterator);
|
||||
};
|
||||
|
||||
const auto onCopyDone = [this, iterator, counterStorage](
|
||||
const Async<Result> &async) {
|
||||
const Result result = async.result();
|
||||
int &counter = *counterStorage;
|
||||
++counter;
|
||||
|
||||
if (result) {
|
||||
//: %1/%2 = progress in the form 4/15, %3 and %4 = source and target file paths
|
||||
emit progress(Tr::tr("Copied %1/%2: \"%3\" -> \"%4\".\n")
|
||||
.arg(counter)
|
||||
.arg(m_setup.m_files.size())
|
||||
.arg(iterator->m_source.toUserOutput())
|
||||
.arg(iterator->m_target.toUserOutput()));
|
||||
} else {
|
||||
emit progress(result.error() + "\n");
|
||||
}
|
||||
};
|
||||
|
||||
const Group recipe {
|
||||
For (iteratorParentDirs) >> Do {
|
||||
parallelIdealThreadCountLimit,
|
||||
AsyncTask<Result>(onCreateDirSetup, onCreateDirDone),
|
||||
},
|
||||
For (iterator) >> Do {
|
||||
parallelLimit(2),
|
||||
counterStorage,
|
||||
AsyncTask<Result>(onCopySetup, onCopyDone),
|
||||
},
|
||||
};
|
||||
|
||||
m_taskTree.start(recipe, {}, [this](DoneWith result) {
|
||||
ProcessResultData resultData;
|
||||
if (result != DoneWith::Success) {
|
||||
resultData.m_exitCode = -1;
|
||||
resultData.m_errorString = Tr::tr("Failed to deploy files.");
|
||||
}
|
||||
emit done(resultData);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
FileTransferInterface *createRemoteLinuxFileTransferInterface
|
||||
(const LinuxDevice &device, const FileTransferSetupData &setup)
|
||||
{
|
||||
if (Utils::anyOf(setup.m_files,
|
||||
[](const FileToTransfer &f) { return !f.m_source.isLocal(); })) {
|
||||
return new GenericTransferImpl(setup);
|
||||
}
|
||||
|
||||
switch (setup.m_method) {
|
||||
case FileTransferMethod::Sftp: return new SftpTransferImpl(setup, device.shared_from_this());
|
||||
case FileTransferMethod::Rsync: return new RsyncTransferImpl(setup, device.shared_from_this());
|
||||
case FileTransferMethod::GenericCopy: return new GenericTransferImpl(setup);
|
||||
}
|
||||
QTC_CHECK(false);
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace RemoteLinux::Internal
|
15
src/plugins/remotelinux/remotelinuxfiletransfer.h
Normal file
15
src/plugins/remotelinux/remotelinuxfiletransfer.h
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "linuxdevice.h"
|
||||
|
||||
namespace RemoteLinux::Internal {
|
||||
|
||||
ProjectExplorer::FileTransferInterface *
|
||||
createRemoteLinuxFileTransferInterface(
|
||||
const LinuxDevice &device,
|
||||
const ProjectExplorer::FileTransferSetupData &setup);
|
||||
|
||||
} // namespace RemoteLinux
|
Reference in New Issue
Block a user