GenericDirectUploadService: Reuse TaskTree

Change-Id: Ib450274d3e73059bbc5e8f8e0d586ebb2a7a0dc4
Reviewed-by: Christian Kandeler <christian.kandeler@qt.io>
This commit is contained in:
Jarek Kobus
2022-11-23 11:37:34 +01:00
parent a6015a6216
commit f2d50ba6ff
3 changed files with 228 additions and 227 deletions

View File

@@ -62,11 +62,11 @@ class TreeStorage : public TreeStorageBase
public: public:
TreeStorage() : TreeStorageBase(TreeStorage::ctor(), TreeStorage::dtor()) {} TreeStorage() : TreeStorageBase(TreeStorage::ctor(), TreeStorage::dtor()) {}
StorageStruct *operator->() const noexcept { return activeStorage(); } StorageStruct *operator->() const noexcept { return activeStorage(); }
private:
StorageStruct *activeStorage() const { StorageStruct *activeStorage() const {
return static_cast<StorageStruct *>(activeStorageVoid()); return static_cast<StorageStruct *>(activeStorageVoid());
} }
private:
static StorageConstructor ctor() { return [] { return new StorageStruct; }; } static StorageConstructor ctor() { return [] { return new StorageStruct; }; }
static StorageDestructor dtor() { static StorageDestructor dtor() {
return [](void *storage) { delete static_cast<StorageStruct *>(storage); }; return [](void *storage) { delete static_cast<StorageStruct *>(storage); };

View File

@@ -16,65 +16,82 @@
#include <QDateTime> #include <QDateTime>
#include <QDir> #include <QDir>
#include <QFileInfo>
#include <QHash>
#include <QList> #include <QList>
#include <QQueue>
#include <QString>
using namespace ProjectExplorer; using namespace ProjectExplorer;
using namespace Utils; using namespace Utils;
using namespace Utils::Tasking;
namespace RemoteLinux { namespace RemoteLinux {
namespace Internal { namespace Internal {
enum State { Inactive, PreChecking, Uploading, PostProcessing };
const int MaxConcurrentStatCalls = 10; const int MaxConcurrentStatCalls = 10;
struct UploadStorage
{
QList<DeployableFile> filesToUpload;
FilesToTransfer filesToTransfer;
};
class GenericDirectUploadServicePrivate class GenericDirectUploadServicePrivate
{ {
public: public:
DeployableFile getFileForProcess(QtcProcess *proc) GenericDirectUploadServicePrivate(GenericDirectUploadService *service) : q(service) {}
{
const auto it = remoteProcs.find(proc);
QTC_ASSERT(it != remoteProcs.end(), return DeployableFile());
const DeployableFile file = *it;
remoteProcs.erase(it);
return file;
}
QDateTime timestampFromStat(const DeployableFile &file, QtcProcess *statProc);
using FilesToStat = std::function<QList<DeployableFile>(UploadStorage *)>;
using StatEndHandler
= std::function<void(UploadStorage *, const DeployableFile &, const QDateTime &)>;
TaskItem statTask(UploadStorage *storage, const DeployableFile &file,
StatEndHandler statEndHandler);
TaskItem statTree(const TreeStorage<UploadStorage> &storage, FilesToStat filesToStat,
StatEndHandler statEndHandler);
TaskItem uploadGroup(const TreeStorage<UploadStorage> &storage);
TaskItem chmodTask(const DeployableFile &file);
TaskItem chmodTree(const TreeStorage<UploadStorage> &storage);
GenericDirectUploadService *q = nullptr;
IncrementalDeployment incremental = IncrementalDeployment::NotSupported; IncrementalDeployment incremental = IncrementalDeployment::NotSupported;
bool ignoreMissingFiles = false; bool ignoreMissingFiles = false;
QHash<QtcProcess *, DeployableFile> remoteProcs;
QQueue<DeployableFile> filesToStat;
State state = Inactive;
QList<DeployableFile> filesToUpload;
FileTransfer uploader;
QList<DeployableFile> deployableFiles; QList<DeployableFile> deployableFiles;
std::unique_ptr<TaskTree> m_taskTree;
}; };
QList<DeployableFile> collectFilesToUpload(const DeployableFile &deployable)
{
QList<DeployableFile> collected;
FilePath localFile = deployable.localFilePath();
if (localFile.isDir()) {
const FilePaths files = localFile.dirEntries(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
const QString remoteDir = deployable.remoteDirectory() + '/' + localFile.fileName();
for (const FilePath &localFilePath : files)
collected.append(collectFilesToUpload(DeployableFile(localFilePath, remoteDir)));
} else {
collected << deployable;
}
return collected;
}
static Group packIntoOptionalParallelGroups(const QList<TaskItem> &tasks)
{
QList<TaskItem> groups;
int i = 0;
while (i < tasks.size()) {
const QList<TaskItem> subTasks = tasks.mid(i, MaxConcurrentStatCalls);
i += subTasks.size();
groups.append(Group { QList<TaskItem> {optional, parallel} + subTasks });
}
return Group { QList<TaskItem> {optional} + groups };
}
} // namespace Internal } // namespace Internal
using namespace Internal; using namespace Internal;
GenericDirectUploadService::GenericDirectUploadService(QObject *parent) GenericDirectUploadService::GenericDirectUploadService(QObject *parent)
: AbstractRemoteLinuxDeployService(parent), d(new GenericDirectUploadServicePrivate) : AbstractRemoteLinuxDeployService(parent), d(new GenericDirectUploadServicePrivate(this))
{ {
connect(&d->uploader, &FileTransfer::done, this, [this](const ProcessResultData &result) {
QTC_ASSERT(d->state == Uploading, return);
if (result.m_error != QProcess::UnknownError || result.m_exitCode != 0) {
emit errorMessage(result.m_errorString);
setFinished();
handleDeploymentDone();
return;
}
d->state = PostProcessing;
chmod();
queryFiles();
});
connect(&d->uploader, &FileTransfer::progress,
this, &GenericDirectUploadService::progressMessage);
} }
GenericDirectUploadService::~GenericDirectUploadService() GenericDirectUploadService::~GenericDirectUploadService()
@@ -99,7 +116,6 @@ void GenericDirectUploadService::setIgnoreMissingFiles(bool ignoreMissingFiles)
bool GenericDirectUploadService::isDeploymentNecessary() const bool GenericDirectUploadService::isDeploymentNecessary() const
{ {
QTC_ASSERT(d->filesToUpload.isEmpty(), d->filesToUpload.clear());
QList<DeployableFile> collected; QList<DeployableFile> collected;
for (int i = 0; i < d->deployableFiles.count(); ++i) for (int i = 0; i < d->deployableFiles.count(); ++i)
collected.append(collectFilesToUpload(d->deployableFiles.at(i))); collected.append(collectFilesToUpload(d->deployableFiles.at(i)));
@@ -109,14 +125,13 @@ bool GenericDirectUploadService::isDeploymentNecessary() const
return !d->deployableFiles.isEmpty(); return !d->deployableFiles.isEmpty();
} }
void GenericDirectUploadService::doDeploy() void GenericDirectUploadService::stopDeployment()
{ {
QTC_ASSERT(d->state == Inactive, setFinished(); return); d->m_taskTree.reset();
d->state = PreChecking; handleDeploymentDone();
queryFiles();
} }
QDateTime GenericDirectUploadService::timestampFromStat(const DeployableFile &file, QDateTime GenericDirectUploadServicePrivate::timestampFromStat(const DeployableFile &file,
QtcProcess *statProc) QtcProcess *statProc)
{ {
bool succeeded = false; bool succeeded = false;
@@ -132,204 +147,208 @@ QDateTime GenericDirectUploadService::timestampFromStat(const DeployableFile &fi
succeeded = true; succeeded = true;
} }
if (!succeeded) { if (!succeeded) {
emit warningMessage(Tr::tr("Failed to retrieve remote timestamp for file \"%1\". " emit q->warningMessage(Tr::tr("Failed to retrieve remote timestamp for file \"%1\". "
"Incremental deployment will not work. Error message was: %2") "Incremental deployment will not work. Error message was: %2")
.arg(file.remoteFilePath(), error)); .arg(file.remoteFilePath(), error));
return QDateTime(); return {};
} }
const QByteArray output = statProc->readAllStandardOutput().trimmed(); const QByteArray output = statProc->readAllStandardOutput().trimmed();
const QString warningString(Tr::tr("Unexpected stat output for remote file \"%1\": %2") const QString warningString(Tr::tr("Unexpected stat output for remote file \"%1\": %2")
.arg(file.remoteFilePath()).arg(QString::fromUtf8(output))); .arg(file.remoteFilePath()).arg(QString::fromUtf8(output)));
if (!output.startsWith(file.remoteFilePath().toUtf8())) { if (!output.startsWith(file.remoteFilePath().toUtf8())) {
emit warningMessage(warningString); emit q->warningMessage(warningString);
return QDateTime(); return {};
} }
const QByteArrayList columns = output.mid(file.remoteFilePath().toUtf8().size() + 1).split(' '); const QByteArrayList columns = output.mid(file.remoteFilePath().toUtf8().size() + 1).split(' ');
if (columns.size() < 14) { // Normal Linux stat: 16 columns in total, busybox stat: 15 columns if (columns.size() < 14) { // Normal Linux stat: 16 columns in total, busybox stat: 15 columns
emit warningMessage(warningString); emit q->warningMessage(warningString);
return QDateTime(); return {};
} }
bool isNumber; bool isNumber;
const qint64 secsSinceEpoch = columns.at(11).toLongLong(&isNumber); const qint64 secsSinceEpoch = columns.at(11).toLongLong(&isNumber);
if (!isNumber) { if (!isNumber) {
emit warningMessage(warningString); emit q->warningMessage(warningString);
return QDateTime(); return {};
} }
return QDateTime::fromSecsSinceEpoch(secsSinceEpoch); return QDateTime::fromSecsSinceEpoch(secsSinceEpoch);
} }
void GenericDirectUploadService::checkForStateChangeOnRemoteProcFinished() TaskItem GenericDirectUploadServicePrivate::statTask(UploadStorage *storage,
{ const DeployableFile &file,
if (d->remoteProcs.size() < MaxConcurrentStatCalls && !d->filesToStat.isEmpty()) StatEndHandler statEndHandler)
runStat(d->filesToStat.dequeue());
if (!d->remoteProcs.isEmpty())
return;
if (d->state == PreChecking) {
uploadFiles();
return;
}
QTC_ASSERT(d->state == PostProcessing, return);
emit progressMessage(Tr::tr("All files successfully deployed."));
setFinished();
handleDeploymentDone();
}
void GenericDirectUploadService::stopDeployment()
{
QTC_ASSERT(d->state != Inactive, return);
setFinished();
handleDeploymentDone();
}
void GenericDirectUploadService::runStat(const DeployableFile &file)
{ {
const auto setupHandler = [=](QtcProcess &process) {
// We'd like to use --format=%Y, but it's not supported by busybox. // We'd like to use --format=%Y, but it's not supported by busybox.
QtcProcess * const statProc = new QtcProcess(this); process.setCommand({q->deviceConfiguration()->filePath("stat"),
statProc->setCommand({deviceConfiguration()->filePath("stat"),
{"-t", Utils::ProcessArgs::quoteArgUnix(file.remoteFilePath())}}); {"-t", Utils::ProcessArgs::quoteArgUnix(file.remoteFilePath())}});
connect(statProc, &QtcProcess::done, this, [this, statProc, state = d->state] { };
QTC_ASSERT(d->state == state, return); const auto endHandler = [=](const QtcProcess &process) {
const DeployableFile file = d->getFileForProcess(statProc); QtcProcess *proc = const_cast<QtcProcess *>(&process);
QTC_ASSERT(file.isValid(), return); const QDateTime timestamp = timestampFromStat(file, proc);
const QDateTime timestamp = timestampFromStat(file, statProc); statEndHandler(storage, file, timestamp);
statProc->deleteLater(); };
switch (state) { return Process(setupHandler, endHandler, endHandler);
case PreChecking:
if (!timestamp.isValid() || hasRemoteFileChanged(file, timestamp))
d->filesToUpload.append(file);
break;
case PostProcessing:
if (timestamp.isValid())
saveDeploymentTimeStamp(file, timestamp);
break;
case Inactive:
case Uploading:
QTC_CHECK(false);
break;
}
checkForStateChangeOnRemoteProcFinished();
});
d->remoteProcs.insert(statProc, file);
statProc->start();
} }
QList<DeployableFile> GenericDirectUploadService::collectFilesToUpload( TaskItem GenericDirectUploadServicePrivate::statTree(const TreeStorage<UploadStorage> &storage,
const DeployableFile &deployable) const FilesToStat filesToStat, StatEndHandler statEndHandler)
{ {
QList<DeployableFile> collected; const auto setupHandler = [=](TaskTree &tree) {
FilePath localFile = deployable.localFilePath(); UploadStorage *storagePtr = storage.activeStorage();
if (localFile.isDir()) { const QList<DeployableFile> files = filesToStat(storagePtr);
const FilePaths files = localFile.dirEntries(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); QList<TaskItem> statList;
const QString remoteDir = deployable.remoteDirectory() + '/' + localFile.fileName(); for (const DeployableFile &file : std::as_const(files)) {
for (const FilePath &localFilePath : files) QTC_ASSERT(file.isValid(), continue);
collected.append(collectFilesToUpload(DeployableFile(localFilePath, remoteDir))); statList.append(statTask(storagePtr, file, statEndHandler));
} else {
collected << deployable;
} }
return collected; tree.setupRoot(packIntoOptionalParallelGroups(statList));
};
return Tree(setupHandler);
} }
void GenericDirectUploadService::setFinished() TaskItem GenericDirectUploadServicePrivate::uploadGroup(const TreeStorage<UploadStorage> &storage)
{ {
d->state = Inactive; const auto groupSetupHandler = [=] {
d->filesToStat.clear(); if (storage->filesToUpload.isEmpty()) {
for (auto it = d->remoteProcs.begin(); it != d->remoteProcs.end(); ++it) { emit q->progressMessage(Tr::tr("No files need to be uploaded."));
it.key()->disconnect(); return GroupConfig{GroupAction::StopWithDone};
it.key()->terminate();
} }
d->remoteProcs.clear(); emit q->progressMessage(Tr::tr("%n file(s) need to be uploaded.", "",
d->uploader.stop(); storage->filesToUpload.size()));
d->filesToUpload.clear(); FilesToTransfer files;
for (const DeployableFile &file : std::as_const(storage->filesToUpload)) {
if (!file.localFilePath().exists()) {
const QString message = Tr::tr("Local file \"%1\" does not exist.")
.arg(file.localFilePath().toUserOutput());
if (ignoreMissingFiles) {
emit q->warningMessage(message);
continue;
}
emit q->errorMessage(message);
return GroupConfig{GroupAction::StopWithError};
}
files.append({file.localFilePath(),
q->deviceConfiguration()->filePath(file.remoteFilePath())});
}
storage->filesToTransfer = files;
if (storage->filesToTransfer.isEmpty()) {
emit q->progressMessage(Tr::tr("No files need to be uploaded."));
return GroupConfig{GroupAction::StopWithDone};
}
return GroupConfig();
};
const auto setupHandler = [this, storage](FileTransfer &transfer) {
transfer.setFilesToTransfer(storage->filesToTransfer);
QObject::connect(&transfer, &FileTransfer::progress,
q, &GenericDirectUploadService::progressMessage);
};
const auto errorHandler = [this](const FileTransfer &transfer) {
emit q->errorMessage(transfer.resultData().m_errorString);
};
const Group group {
DynamicSetup(groupSetupHandler),
Transfer(setupHandler, {}, errorHandler)
};
return group;
} }
void GenericDirectUploadService::queryFiles() TaskItem GenericDirectUploadServicePrivate::chmodTask(const DeployableFile &file)
{ {
QTC_ASSERT(d->state == PreChecking || d->state == PostProcessing, return); const auto setupHandler = [=](QtcProcess &process) {
QTC_ASSERT(d->state == PostProcessing || d->remoteProcs.isEmpty(), return); process.setCommand({q->deviceConfiguration()->filePath("chmod"),
{"a+x", Utils::ProcessArgs::quoteArgUnix(file.remoteFilePath())}});
};
const auto errorHandler = [=](const QtcProcess &process) {
const QString error = process.errorString();
if (!error.isEmpty()) {
emit q->warningMessage(Tr::tr("Remote chmod failed for file \"%1\": %2")
.arg(file.remoteFilePath(), error));
} else if (process.exitCode() != 0) {
emit q->warningMessage(Tr::tr("Remote chmod failed for file \"%1\": %2")
.arg(file.remoteFilePath(), process.cleanedStdErr()));
}
};
return Process(setupHandler, {}, errorHandler);
}
const QList<DeployableFile> &filesToCheck = d->state == PreChecking TaskItem GenericDirectUploadServicePrivate::chmodTree(const TreeStorage<UploadStorage> &storage)
? d->deployableFiles : d->filesToUpload; {
for (const DeployableFile &file : filesToCheck) { const auto setupChmodHandler = [=](TaskTree &tree) {
if (d->state == PreChecking && (d->incremental != IncrementalDeployment::Enabled QList<DeployableFile> filesToChmod;
|| hasLocalFileChanged(file))) { for (const DeployableFile &file : std::as_const(storage->filesToUpload)) {
d->filesToUpload.append(file); if (file.isExecutable())
filesToChmod << file;
}
QList<TaskItem> chmodList;
for (const DeployableFile &file : std::as_const(filesToChmod)) {
QTC_ASSERT(file.isValid(), continue);
chmodList.append(chmodTask(file));
}
tree.setupRoot(packIntoOptionalParallelGroups(chmodList));
};
return Tree {setupChmodHandler};
}
void GenericDirectUploadService::doDeploy()
{
QTC_ASSERT(!d->m_taskTree, return);
const auto endHandler = [this] {
d->m_taskTree.release()->deleteLater();
stopDeployment();
};
const auto doneHandler = [this, endHandler] {
emit progressMessage(Tr::tr("All files successfully deployed."));
endHandler();
};
const auto preFilesToStat = [this](UploadStorage *storage) {
QList<DeployableFile> filesToStat;
for (const DeployableFile &file : std::as_const(d->deployableFiles)) {
if (d->incremental != IncrementalDeployment::Enabled || hasLocalFileChanged(file)) {
storage->filesToUpload.append(file);
continue; continue;
} }
if (d->incremental == IncrementalDeployment::NotSupported) if (d->incremental == IncrementalDeployment::NotSupported)
continue; continue;
if (d->remoteProcs.size() >= MaxConcurrentStatCalls) filesToStat << file;
d->filesToStat << file;
else
runStat(file);
}
checkForStateChangeOnRemoteProcFinished();
} }
return filesToStat;
};
const auto preStatEndHandler = [this](UploadStorage *storage, const DeployableFile &file,
const QDateTime &timestamp) {
if (!timestamp.isValid() || hasRemoteFileChanged(file, timestamp))
storage->filesToUpload.append(file);
};
void GenericDirectUploadService::uploadFiles() const auto postFilesToStat = [this](UploadStorage *storage) {
{ return d->incremental == IncrementalDeployment::NotSupported
QTC_ASSERT(d->state == PreChecking, return); ? QList<DeployableFile>() : storage->filesToUpload;
d->state = Uploading; };
if (d->filesToUpload.empty()) { const auto postStatEndHandler = [this](UploadStorage *storage, const DeployableFile &file,
emit progressMessage(Tr::tr("No files need to be uploaded.")); const QDateTime &timestamp) {
setFinished(); Q_UNUSED(storage)
handleDeploymentDone(); if (timestamp.isValid())
return; saveDeploymentTimeStamp(file, timestamp);
} };
emit progressMessage(Tr::tr("%n file(s) need to be uploaded.", "", d->filesToUpload.size()));
FilesToTransfer files;
for (const DeployableFile &file : std::as_const(d->filesToUpload)) {
if (!file.localFilePath().exists()) {
const QString message = Tr::tr("Local file \"%1\" does not exist.")
.arg(file.localFilePath().toUserOutput());
if (d->ignoreMissingFiles) {
emit warningMessage(message);
continue;
} else {
emit errorMessage(message);
setFinished();
handleDeploymentDone();
return;
}
}
files.append({file.localFilePath(),
deviceConfiguration()->filePath(file.remoteFilePath())});
}
d->uploader.setFilesToTransfer(files); const TreeStorage<UploadStorage> storage;
d->uploader.start(); const Group root {
} Storage(storage),
d->statTree(storage, preFilesToStat, preStatEndHandler),
d->uploadGroup(storage),
Group {
d->chmodTree(storage),
d->statTree(storage, postFilesToStat, postStatEndHandler)
},
OnGroupDone(doneHandler),
OnGroupError(endHandler)
};
void GenericDirectUploadService::chmod() d->m_taskTree.reset(new TaskTree(root));
{ d->m_taskTree->start();
QTC_ASSERT(d->state == PostProcessing, return);
if (!Utils::HostOsInfo::isWindowsHost())
return;
for (const DeployableFile &f : std::as_const(d->filesToUpload)) {
if (!f.isExecutable())
continue;
QtcProcess * const chmodProc = new QtcProcess(this);
chmodProc->setCommand({deviceConfiguration()->filePath("chmod"),
{"a+x", Utils::ProcessArgs::quoteArgUnix(f.remoteFilePath())}});
connect(chmodProc, &QtcProcess::done, this, [this, chmodProc, state = d->state] {
QTC_ASSERT(state == d->state, return);
const DeployableFile file = d->getFileForProcess(chmodProc);
QTC_ASSERT(file.isValid(), return);
const QString error = chmodProc->errorString();
if (!error.isEmpty()) {
emit warningMessage(Tr::tr("Remote chmod failed for file \"%1\": %2")
.arg(file.remoteFilePath(), error));
} else if (chmodProc->exitCode() != 0) {
emit warningMessage(Tr::tr("Remote chmod failed for file \"%1\": %2")
.arg(file.remoteFilePath(),
QString::fromUtf8(chmodProc->readAllStandardError())));
}
chmodProc->deleteLater();
checkForStateChangeOnRemoteProcFinished();
});
d->remoteProcs.insert(chmodProc, f);
chmodProc->start();
}
} }
} //namespace RemoteLinux } //namespace RemoteLinux

View File

@@ -9,13 +9,7 @@
#include <QList> #include <QList>
QT_BEGIN_NAMESPACE
class QDateTime;
class QString;
QT_END_NAMESPACE
namespace ProjectExplorer { class DeployableFile; } namespace ProjectExplorer { class DeployableFile; }
namespace Utils { class QtcProcess; }
namespace RemoteLinux { namespace RemoteLinux {
namespace Internal { class GenericDirectUploadServicePrivate; } namespace Internal { class GenericDirectUploadServicePrivate; }
@@ -35,23 +29,11 @@ public:
protected: protected:
bool isDeploymentNecessary() const override; bool isDeploymentNecessary() const override;
void doDeploy() override; void doDeploy() override;
void stopDeployment() override; void stopDeployment() override;
private: private:
void runStat(const ProjectExplorer::DeployableFile &file); friend class Internal::GenericDirectUploadServicePrivate;
QDateTime timestampFromStat(const ProjectExplorer::DeployableFile &file,
Utils::QtcProcess *statProc);
void checkForStateChangeOnRemoteProcFinished();
QList<ProjectExplorer::DeployableFile> collectFilesToUpload(
const ProjectExplorer::DeployableFile &file) const;
void setFinished();
void queryFiles();
void uploadFiles();
void chmod();
Internal::GenericDirectUploadServicePrivate * const d; Internal::GenericDirectUploadServicePrivate * const d;
}; };