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:
hjk
2024-12-10 10:15:02 +01:00
parent e4d23646f3
commit a952ab4dd7
6 changed files with 485 additions and 420 deletions

View File

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

View File

@@ -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);
}
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 {};
return Internal::createRemoteLinuxFileTransferInterface(*this, setup);
}
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()

View File

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

View File

@@ -44,6 +44,8 @@ QtcPlugin {
"remotelinuxdeploysupport.h",
"remotelinuxenvironmentaspect.cpp",
"remotelinuxenvironmentaspect.h",
"remotelinuxfiletransfer.cpp",
"remotelinuxfiletransfer.h",
"remotelinuxplugin.cpp",
"remotelinuxrunconfiguration.cpp",
"remotelinuxrunconfiguration.h",

View 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

View 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