diff --git a/src/plugins/remotelinux/CMakeLists.txt b/src/plugins/remotelinux/CMakeLists.txt index dc08806c748..ffce8abdcb5 100644 --- a/src/plugins/remotelinux/CMakeLists.txt +++ b/src/plugins/remotelinux/CMakeLists.txt @@ -7,6 +7,7 @@ add_qtc_plugin(RemoteLinux abstractremotelinuxdeploystep.cpp abstractremotelinuxdeploystep.h abstractuploadandinstallpackageservice.cpp abstractuploadandinstallpackageservice.h deploymenttimeinfo.cpp deploymenttimeinfo.h + filetransfer.h genericdirectuploadservice.cpp genericdirectuploadservice.h genericdirectuploadstep.cpp genericdirectuploadstep.h genericlinuxdeviceconfigurationwidget.cpp genericlinuxdeviceconfigurationwidget.h genericlinuxdeviceconfigurationwidget.ui diff --git a/src/plugins/remotelinux/abstractuploadandinstallpackageservice.cpp b/src/plugins/remotelinux/abstractuploadandinstallpackageservice.cpp index 04ee35768db..1326c484301 100644 --- a/src/plugins/remotelinux/abstractuploadandinstallpackageservice.cpp +++ b/src/plugins/remotelinux/abstractuploadandinstallpackageservice.cpp @@ -25,10 +25,11 @@ #include "abstractuploadandinstallpackageservice.h" -#include "linuxdevice.h" +#include "filetransfer.h" #include "remotelinuxpackageinstaller.h" #include +#include #include #include @@ -47,7 +48,7 @@ class AbstractUploadAndInstallPackageServicePrivate { public: State state = Inactive; - std::unique_ptr uploader; + FileTransfer uploader; Utils::FilePath packageFilePath; }; @@ -58,6 +59,10 @@ using namespace Internal; AbstractUploadAndInstallPackageService::AbstractUploadAndInstallPackageService() : d(new AbstractUploadAndInstallPackageServicePrivate) { + connect(&d->uploader, &FileTransfer::done, this, + &AbstractUploadAndInstallPackageService::handleUploadFinished); + connect(&d->uploader, &FileTransfer::progress, this, + &AbstractUploadAndInstallPackageService::progressMessage); } AbstractUploadAndInstallPackageService::~AbstractUploadAndInstallPackageService() @@ -98,17 +103,12 @@ void AbstractUploadAndInstallPackageService::doDeploy() d->state = Uploading; - LinuxDevice::ConstPtr linuxDevice = deviceConfiguration().dynamicCast(); - QTC_ASSERT(linuxDevice, return); const QString remoteFilePath = uploadDir() + QLatin1Char('/') + d->packageFilePath.fileName(); const FilesToTransfer files {{d->packageFilePath, deviceConfiguration()->filePath(remoteFilePath)}}; - d->uploader.reset(linuxDevice->createFileTransfer(files)); - connect(d->uploader.get(), &FileTransfer::done, this, - &AbstractUploadAndInstallPackageService::handleUploadFinished); - connect(d->uploader.get(), &FileTransfer::progress, this, - &AbstractUploadAndInstallPackageService::progressMessage); - d->uploader->start(); + d->uploader.setDevice(deviceConfiguration()); + d->uploader.setFilesToTransfer(files); + d->uploader.start(); } void AbstractUploadAndInstallPackageService::stopDeployment() @@ -118,7 +118,7 @@ void AbstractUploadAndInstallPackageService::stopDeployment() qWarning("%s: Unexpected state 'Inactive'.", Q_FUNC_INFO); break; case Uploading: - d->uploader->stop(); + d->uploader.stop(); setFinished(); break; case Installing: @@ -167,7 +167,7 @@ void AbstractUploadAndInstallPackageService::handleInstallationFinished(const QS void AbstractUploadAndInstallPackageService::setFinished() { d->state = Inactive; - d->uploader->stop(); + d->uploader.stop(); disconnect(packageInstaller(), nullptr, this, nullptr); handleDeploymentDone(); } diff --git a/src/plugins/remotelinux/filetransfer.h b/src/plugins/remotelinux/filetransfer.h new file mode 100644 index 00000000000..a41f740e293 --- /dev/null +++ b/src/plugins/remotelinux/filetransfer.h @@ -0,0 +1,85 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "remotelinux_export.h" + +#include + +#include + +namespace Utils { class ProcessResultData; } + +namespace RemoteLinux { + +enum class TransferDirection { + Upload, + Download, + Invalid +}; + +class REMOTELINUX_EXPORT FileToTransfer +{ +public: + Utils::FilePath m_source; + Utils::FilePath m_target; + TransferDirection transferDirection() const; +}; +using FilesToTransfer = QList; + +enum class FileTransferMethod { + Sftp, + Rsync, + Default = Sftp +}; + +class FileTransferPrivate; + +class REMOTELINUX_EXPORT FileTransfer : public QObject +{ + Q_OBJECT + +public: + FileTransfer(); + ~FileTransfer(); + + void setDevice(const ProjectExplorer::IDeviceConstPtr &device); + void setTransferMethod(FileTransferMethod method); + void setFilesToTransfer(const FilesToTransfer &files); + void setRsyncFlags(const QString &flags); + + void start(); + void stop(); + +signals: + void progress(const QString &progressMessage); + void done(const Utils::ProcessResultData &resultData); + +private: + FileTransferPrivate *d; +}; + +} // namespace RemoteLinux diff --git a/src/plugins/remotelinux/genericdirectuploadservice.cpp b/src/plugins/remotelinux/genericdirectuploadservice.cpp index 9241f8e8bb9..15958c901e0 100644 --- a/src/plugins/remotelinux/genericdirectuploadservice.cpp +++ b/src/plugins/remotelinux/genericdirectuploadservice.cpp @@ -24,9 +24,11 @@ ****************************************************************************/ #include "genericdirectuploadservice.h" -#include "linuxdevice.h" + +#include "filetransfer.h" #include +#include #include #include #include @@ -69,7 +71,7 @@ public: QQueue filesToStat; State state = Inactive; QList filesToUpload; - std::unique_ptr uploader; + FileTransfer uploader; QList deployableFiles; }; @@ -80,6 +82,20 @@ using namespace Internal; GenericDirectUploadService::GenericDirectUploadService(QObject *parent) : AbstractRemoteLinuxDeployService(parent), d(new GenericDirectUploadServicePrivate) { + connect(&d->uploader, &FileTransfer::done, this, [this](const ProcessResultData &result) { + QTC_ASSERT(d->state == Uploading, return); + if (result.m_error != QProcess::UnknownError) { + emit errorMessage(result.m_errorString); + setFinished(); + handleDeploymentDone(); + return; + } + d->state = PostProcessing; + chmod(); + queryFiles(); + }); + connect(&d->uploader, &FileTransfer::progress, + this, &GenericDirectUploadService::progressMessage); } GenericDirectUploadService::~GenericDirectUploadService() @@ -250,7 +266,7 @@ void GenericDirectUploadService::setFinished() it.key()->terminate(); } d->remoteProcs.clear(); - d->uploader->stop(); + d->uploader.stop(); d->filesToUpload.clear(); } @@ -306,25 +322,10 @@ void GenericDirectUploadService::uploadFiles() files.append({file.localFilePath(), deviceConfiguration()->filePath(file.remoteFilePath())}); } - LinuxDevice::ConstPtr linuxDevice = deviceConfiguration().dynamicCast(); - QTC_ASSERT(linuxDevice, return); - d->uploader.reset(linuxDevice->createFileTransfer(files)); - connect(d->uploader.get(), &FileTransfer::done, this, [this](const ProcessResultData &result) { - QTC_ASSERT(d->state == Uploading, return); - if (result.m_error != QProcess::UnknownError) { - emit errorMessage(result.m_errorString); - setFinished(); - handleDeploymentDone(); - return; - } - d->state = PostProcessing; - chmod(); - queryFiles(); - }); - connect(d->uploader.get(), &FileTransfer::progress, - this, &GenericDirectUploadService::progressMessage); - d->uploader->start(); + d->uploader.setDevice(deviceConfiguration()); + d->uploader.setFilesToTransfer(files); + d->uploader.start(); } void GenericDirectUploadService::chmod() diff --git a/src/plugins/remotelinux/linuxdevice.cpp b/src/plugins/remotelinux/linuxdevice.cpp index bdaf8a7d4f5..4348b36c210 100644 --- a/src/plugins/remotelinux/linuxdevice.cpp +++ b/src/plugins/remotelinux/linuxdevice.cpp @@ -25,6 +25,7 @@ #include "linuxdevice.h" +#include "filetransfer.h" #include "genericlinuxdeviceconfigurationwidget.h" #include "genericlinuxdeviceconfigurationwizard.h" #include "linuxdevicetester.h" @@ -1458,60 +1459,196 @@ static QByteArray transferCommand(const TransferDirection transferDirection, boo return {}; } -// TODO: Provide 2 subclasses that handle sftp and rsync respectively +static QString methodName(FileTransferMethod method) +{ + switch (method) { + case FileTransferMethod::Sftp: return FileTransfer::tr("sftp"); break; + case FileTransferMethod::Rsync: return FileTransfer::tr("rsync"); break; + } + QTC_CHECK(false); + return {}; +} + class FileTransferInterface : public QObject +{ + Q_OBJECT +public: + void setDevice(const ProjectExplorer::IDeviceConstPtr &device) { m_device = device; } + void setFilesToTransfer(const FilesToTransfer &files, TransferDirection direction) + { + m_files = files; + m_direction = direction; + } + + void start() { startImpl(); } + +signals: + void progress(const QString &progressMessage); + void done(const Utils::ProcessResultData &resultData); + +protected: + FileTransferInterface(FileTransferMethod method) + : m_method(method) + , m_process(this) + { + SshConnectionParameters::setupSshEnvironment(&m_process); + connect(&m_process, &QtcProcess::readyReadStandardOutput, this, [this] { + emit progress(QString::fromLocal8Bit(m_process.readAllStandardOutput())); + }); + connect(&m_process, &QtcProcess::done, this, &FileTransferInterface::doneImpl); + } + + void startFailed(const QString &errorString) + { + emit done({0, QProcess::NormalExit, QProcess::FailedToStart, errorString}); + } + + bool handleError() + { + ProcessResultData resultData = m_process.resultData(); + if (resultData.m_error == QProcess::FailedToStart) { + resultData.m_errorString = tr("\"%1\" failed to start: %2") + .arg(methodName(m_method), resultData.m_errorString); + } else if (resultData.m_exitStatus != QProcess::NormalExit) { + resultData.m_errorString = tr("\"%1\" crashed.").arg(methodName(m_method)); + } else if (resultData.m_exitCode != 0) { + resultData.m_errorString = QString::fromLocal8Bit(m_process.readAllStandardError()); + } else { + return false; + } + return true; + } + + void handleDone() + { + if (!handleError()) + emit done(m_process.resultData()); + } + + FileTransferMethod m_method = FileTransferMethod::Default; + IDevice::ConstPtr m_device; + FilesToTransfer m_files; + QtcProcess m_process; + TransferDirection m_direction; + +private: + virtual void startImpl() = 0; + virtual void doneImpl() = 0; +}; + +class SftpTransferImpl : public FileTransferInterface +{ +public: + SftpTransferImpl() : FileTransferInterface(FileTransferMethod::Sftp) { } + +private: + void startImpl() + { + const FilePath sftpBinary = SshSettings::sftpFilePath(); + if (!sftpBinary.exists()) { + startFailed(tr("\"sftp\" binary \"%1\" does not exist.").arg(sftpBinary.toUserOutput())); + return; + } + + m_batchFile.reset(new QTemporaryFile(this)); + if (!m_batchFile->isOpen() && !m_batchFile->open()) { + startFailed(tr("Could not create temporary file: %1").arg(m_batchFile->errorString())); + return; + } + + const FilePaths dirs = dirsToCreate(m_files); + for (const FilePath &dir : dirs) { + if (m_direction == TransferDirection::Upload) { + m_batchFile->write("-mkdir " + ProcessArgs::quoteArgUnix(dir.path()).toLocal8Bit() + + '\n'); + } else if (m_direction == TransferDirection::Download) { + if (!QDir::root().mkpath(dir.path())) { + startFailed(tr("Failed to create local directory \"%1\".") + .arg(QDir::toNativeSeparators(dir.path()))); + return; + } + } + } + + for (const FileToTransfer &file : m_files) { + FilePath sourceFileOrLinkTarget = file.m_source; + bool link = false; + if (m_direction == TransferDirection::Upload) { + const QFileInfo fi(file.m_source.toFileInfo()); + if (fi.isSymLink()) { + link = true; + m_batchFile->write("-rm " + ProcessArgs::quoteArgUnix( + file.m_target.path()).toLocal8Bit() + '\n'); + // see QTBUG-5817. + sourceFileOrLinkTarget.setPath(fi.dir().relativeFilePath(fi.symLinkTarget())); + } + } + m_batchFile->write(transferCommand(m_direction, link) + ' ' + + ProcessArgs::quoteArgUnix(sourceFileOrLinkTarget.path()).toLocal8Bit() + ' ' + + ProcessArgs::quoteArgUnix(file.m_target.path()).toLocal8Bit() + '\n'); + } + m_batchFile->close(); + m_process.setStandardInputFile(m_batchFile->fileName()); + + // TODO: Add support for shared ssh connection + const SshConnectionParameters params = displayless(m_device->sshParameters()); + m_process.setCommand(CommandLine(sftpBinary, + params.connectionOptions(sftpBinary) << params.host())); + m_process.start(); + } + + void doneImpl() final { handleDone(); } + + std::unique_ptr m_batchFile; +}; + +class RsyncTransferImpl : public FileTransferInterface +{ +public: + RsyncTransferImpl(const QString &flags) + : FileTransferInterface(FileTransferMethod::Sftp) + , m_flags(flags) + { } + +private: + void startImpl() {} + void doneImpl() {} + + QString m_flags; +}; + +class FileTransferPrivate : public QObject { Q_OBJECT public: - FileTransferInterface(const LinuxDevice::ConstPtr &device, const FilesToTransfer &files); + void setDevice(const ProjectExplorer::IDeviceConstPtr &device) { m_device = device; } + void setTransferMethod(FileTransferMethod method) { m_method = method; } + void setFilesToTransfer(const FilesToTransfer &files) { m_files = files; } + void setRsyncFlags(const QString &flags) { m_rsyncFlags = flags; } void start(); - void stop() { m_process.close(); } + void stop(); signals: void progress(const QString &progressMessage); void done(const Utils::ProcessResultData &resultData); private: - LinuxDevice::ConstPtr m_device; - FilesToTransfer m_files; + void startFailed(const QString &errorString); - QTemporaryFile m_batchFile; - QtcProcess m_process; + FileTransferMethod m_method = FileTransferMethod::Default; + IDevice::ConstPtr m_device; + FilesToTransfer m_files; + QString m_rsyncFlags = {"-av"}; // TODO: see RsyncDeployStep::defaultFlags(), reuse it + + std::unique_ptr m_transfer; }; -FileTransferInterface::FileTransferInterface(const LinuxDevice::ConstPtr &device, const FilesToTransfer &files) - : m_device(device) - , m_files(files) - , m_batchFile(this) - , m_process(this) -{ - SshConnectionParameters::setupSshEnvironment(&m_process); - - connect(&m_process, &QtcProcess::readyReadStandardOutput, this, [this] { - emit progress(QString::fromLocal8Bit(m_process.readAllStandardOutput())); - }); - connect(&m_process, &QtcProcess::done, this, [this] { - ProcessResultData resultData = m_process.resultData(); - if (resultData.m_error == QProcess::FailedToStart) - resultData.m_errorString = tr("\"sftp\" failed to start: %1").arg(resultData.m_errorString); - else if (resultData.m_exitStatus != QProcess::NormalExit) - resultData.m_errorString = tr("\"sftp\" crashed."); - else if (resultData.m_exitCode != 0) - resultData.m_errorString = QString::fromLocal8Bit(m_process.readAllStandardError()); - emit done(resultData); - }); -} - -void FileTransferInterface::start() +void FileTransferPrivate::start() { stop(); - auto startFailed = [this](const QString &errorString) { - emit done({0, QProcess::NormalExit, QProcess::FailedToStart, errorString}); - }; - if (m_files.isEmpty()) return startFailed(tr("No files to transfer.")); @@ -1522,60 +1659,40 @@ void FileTransferInterface::start() if (!isDeviceMatched(m_files, m_device->id().toString())) return startFailed(tr("Trying to transfer into / from not matching device.")); - const FilePath sftpBinary = SshSettings::sftpFilePath(); - if (!sftpBinary.exists()) - return startFailed(tr("\"sftp\" binary \"%1\" does not exist.").arg(sftpBinary.toUserOutput())); - - if (!m_batchFile.isOpen() && !m_batchFile.open()) - return startFailed(tr("Could not create temporary file: %1").arg(m_batchFile.errorString())); - - const FilePaths dirs = dirsToCreate(m_files); - for (const FilePath &dir : dirs) { - if (direction == TransferDirection::Upload) { - m_batchFile.write("-mkdir " + ProcessArgs::quoteArgUnix(dir.path()).toLocal8Bit() - + '\n'); - } else if (direction == TransferDirection::Download) { - if (!QDir::root().mkpath(dir.path())) { - return startFailed(tr("Failed to create local directory \"%1\".") - .arg(dir.toUserOutput())); - } - } + switch (m_method) { + case FileTransferMethod::Sftp: + m_transfer.reset(new SftpTransferImpl()); + break; + case FileTransferMethod::Rsync: + m_transfer.reset(new RsyncTransferImpl(m_rsyncFlags)); + break; } - - for (const FileToTransfer &file : m_files) { - FilePath sourceFileOrLinkTarget = file.m_source; - bool link = false; - if (direction == TransferDirection::Upload) { - const QFileInfo fi(file.m_source.toFileInfo()); - if (fi.isSymLink()) { - link = true; - m_batchFile.write("-rm " + ProcessArgs::quoteArgUnix( - file.m_target.path()).toLocal8Bit() + '\n'); - // see QTBUG-5817. - sourceFileOrLinkTarget.setPath(fi.dir().relativeFilePath(fi.symLinkTarget())); - } - } - m_batchFile.write(transferCommand(direction, link) + ' ' - + ProcessArgs::quoteArgUnix(sourceFileOrLinkTarget.path()).toLocal8Bit() + ' ' - + ProcessArgs::quoteArgUnix(file.m_target.path()).toLocal8Bit() + '\n'); - } - m_batchFile.close(); - - m_process.setStandardInputFile(m_batchFile.fileName()); - - // TODO: Add support for shared ssh connection - const SshConnectionParameters params = displayless(m_device->sshParameters()); - m_process.setCommand(CommandLine(sftpBinary, - params.connectionOptions(sftpBinary) << params.host())); - m_process.start(); + QTC_ASSERT(m_transfer, startFailed(tr("Missing transfer implementation."))); + m_transfer->setDevice(m_device); + m_transfer->setFilesToTransfer(m_files, direction); + connect(m_transfer.get(), &FileTransferInterface::progress, + this, &FileTransferPrivate::progress); + connect(m_transfer.get(), &FileTransferInterface::done, + this, &FileTransferPrivate::done); + m_transfer->start(); } -FileTransfer::FileTransfer(FileTransferInterface *transferInterface) - : d(transferInterface) +void FileTransferPrivate::stop() +{ + m_transfer.reset(); +} + +void FileTransferPrivate::startFailed(const QString &errorString) +{ + emit done({0, QProcess::NormalExit, QProcess::FailedToStart, errorString}); +} + +FileTransfer::FileTransfer() + : d(new FileTransferPrivate) { d->setParent(this); - connect(d, &FileTransferInterface::progress, this, &FileTransfer::progress); - connect(d, &FileTransferInterface::done, this, &FileTransfer::done); + connect(d, &FileTransferPrivate::progress, this, &FileTransfer::progress); + connect(d, &FileTransferPrivate::done, this, &FileTransfer::done); } FileTransfer::~FileTransfer() @@ -1584,6 +1701,26 @@ FileTransfer::~FileTransfer() delete d; } +void FileTransfer::setDevice(const ProjectExplorer::IDeviceConstPtr &device) +{ + d->setDevice(device); +} + +void FileTransfer::setTransferMethod(FileTransferMethod method) +{ + d->setTransferMethod(method); +} + +void FileTransfer::setFilesToTransfer(const FilesToTransfer &files) +{ + d->setFilesToTransfer(files); +} + +void FileTransfer::setRsyncFlags(const QString &flags) +{ + d->setRsyncFlags(flags); +} + void FileTransfer::start() { d->start(); @@ -1594,12 +1731,6 @@ void FileTransfer::stop() d->stop(); } -FileTransfer *LinuxDevice::createFileTransfer(const FilesToTransfer &files) const -{ - return new FileTransfer(new FileTransferInterface( - sharedFromThis().dynamicCast(), files)); -} - namespace Internal { // Factory diff --git a/src/plugins/remotelinux/linuxdevice.h b/src/plugins/remotelinux/linuxdevice.h index 8c5fd29fc8e..3272e46e575 100644 --- a/src/plugins/remotelinux/linuxdevice.h +++ b/src/plugins/remotelinux/linuxdevice.h @@ -30,48 +30,8 @@ #include #include -namespace Utils { class ProcessResultData; } - namespace RemoteLinux { -enum class TransferDirection { - Upload, - Download, - Invalid -}; - -class FileToTransfer -{ -public: - Utils::FilePath m_source; - Utils::FilePath m_target; - TransferDirection transferDirection() const; -}; -using FilesToTransfer = QList; - -class FileTransferInterface; - -class REMOTELINUX_EXPORT FileTransfer : public QObject -{ - Q_OBJECT - -public: - ~FileTransfer(); - - void start(); - void stop(); - -signals: - void progress(const QString &progressMessage); - void done(const Utils::ProcessResultData &resultData); - -private: - FileTransfer(FileTransferInterface *transferInterface); - FileTransferInterface *d; - - friend class LinuxDevice; -}; - class REMOTELINUX_EXPORT LinuxDevice : public ProjectExplorer::IDevice { Q_DECLARE_TR_FUNCTIONS(RemoteLinux::Internal::LinuxDevice) @@ -127,8 +87,6 @@ public: QFileDevice::Permissions permissions(const Utils::FilePath &filePath) const override; bool setPermissions(const Utils::FilePath &filePath, QFileDevice::Permissions permissions) const override; - FileTransfer *createFileTransfer(const FilesToTransfer &files) const; - protected: LinuxDevice(); diff --git a/src/plugins/remotelinux/remotelinux.qbs b/src/plugins/remotelinux/remotelinux.qbs index f703d023ee8..f6a3a0ecc9f 100644 --- a/src/plugins/remotelinux/remotelinux.qbs +++ b/src/plugins/remotelinux/remotelinux.qbs @@ -24,6 +24,7 @@ Project { "abstractuploadandinstallpackageservice.h", "deploymenttimeinfo.cpp", "deploymenttimeinfo.h", + "filetransfer.h", "genericdirectuploadservice.cpp", "genericdirectuploadservice.h", "genericdirectuploadstep.cpp", diff --git a/src/plugins/remotelinux/rsyncdeploystep.cpp b/src/plugins/remotelinux/rsyncdeploystep.cpp index 92b97962097..9a407c03da7 100644 --- a/src/plugins/remotelinux/rsyncdeploystep.cpp +++ b/src/plugins/remotelinux/rsyncdeploystep.cpp @@ -161,6 +161,7 @@ void RsyncDeployService::deployNextFile() } } + // TODO: consider adding "--progress" option for reporting the progress. const QStringList args = QStringList(cmdLine.options) << (localFilePath + (file.localFilePath().isDir() ? "/" : QString())) << (cmdLine.remoteHostSpec + ':' + file.remoteFilePath());