diff options
Diffstat (limited to 'src/libs/utils/tasktree.cpp')
-rw-r--r-- | src/libs/utils/tasktree.cpp | 1401 |
1 files changed, 0 insertions, 1401 deletions
diff --git a/src/libs/utils/tasktree.cpp b/src/libs/utils/tasktree.cpp deleted file mode 100644 index 90e4073e03..0000000000 --- a/src/libs/utils/tasktree.cpp +++ /dev/null @@ -1,1401 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#include "tasktree.h" - -#include "guard.h" -#include "qtcassert.h" - -#include <QSet> - -namespace Utils { -namespace Tasking { - -static TaskAction toTaskAction(bool success) -{ - return success ? TaskAction::StopWithDone : TaskAction::StopWithError; -} - -bool TreeStorageBase::isValid() const -{ - return m_storageData && m_storageData->m_constructor && m_storageData->m_destructor; -} - -TreeStorageBase::TreeStorageBase(StorageConstructor ctor, StorageDestructor dtor) - : m_storageData(new StorageData{ctor, dtor}) { } - -TreeStorageBase::StorageData::~StorageData() -{ - QTC_CHECK(m_storageHash.isEmpty()); - for (void *ptr : std::as_const(m_storageHash)) - m_destructor(ptr); -} - -void *TreeStorageBase::activeStorageVoid() const -{ - QTC_ASSERT(m_storageData->m_activeStorage, return nullptr); - const auto it = m_storageData->m_storageHash.constFind(m_storageData->m_activeStorage); - QTC_ASSERT(it != m_storageData->m_storageHash.constEnd(), return nullptr); - return it.value(); -} - -int TreeStorageBase::createStorage() const -{ - QTC_ASSERT(m_storageData->m_constructor, return 0); // TODO: add isValid()? - QTC_ASSERT(m_storageData->m_destructor, return 0); - QTC_ASSERT(m_storageData->m_activeStorage == 0, return 0); // TODO: should be allowed? - const int newId = ++m_storageData->m_storageCounter; - m_storageData->m_storageHash.insert(newId, m_storageData->m_constructor()); - return newId; -} - -void TreeStorageBase::deleteStorage(int id) const -{ - QTC_ASSERT(m_storageData->m_constructor, return); // TODO: add isValid()? - QTC_ASSERT(m_storageData->m_destructor, return); - QTC_ASSERT(m_storageData->m_activeStorage == 0, return); // TODO: should be allowed? - const auto it = m_storageData->m_storageHash.constFind(id); - QTC_ASSERT(it != m_storageData->m_storageHash.constEnd(), return); - m_storageData->m_destructor(it.value()); - m_storageData->m_storageHash.erase(it); -} - -// passing 0 deactivates currently active storage -void TreeStorageBase::activateStorage(int id) const -{ - if (id == 0) { - QTC_ASSERT(m_storageData->m_activeStorage, return); - m_storageData->m_activeStorage = 0; - return; - } - QTC_ASSERT(m_storageData->m_activeStorage == 0, return); - const auto it = m_storageData->m_storageHash.find(id); - QTC_ASSERT(it != m_storageData->m_storageHash.end(), return); - m_storageData->m_activeStorage = id; -} - -ParallelLimit sequential(1); -ParallelLimit parallel(0); -Workflow stopOnError(WorkflowPolicy::StopOnError); -Workflow continueOnError(WorkflowPolicy::ContinueOnError); -Workflow stopOnDone(WorkflowPolicy::StopOnDone); -Workflow continueOnDone(WorkflowPolicy::ContinueOnDone); -Workflow optional(WorkflowPolicy::Optional); - -void TaskItem::addChildren(const QList<TaskItem> &children) -{ - QTC_ASSERT(m_type == Type::Group, qWarning("Only Task may have children, skipping..."); return); - for (const TaskItem &child : children) { - switch (child.m_type) { - case Type::Group: - m_children.append(child); - break; - case Type::Limit: - QTC_ASSERT(m_type == Type::Group, - qWarning("Mode may only be a child of Group, skipping..."); return); - m_parallelLimit = child.m_parallelLimit; // TODO: Assert on redefinition? - break; - case Type::Policy: - QTC_ASSERT(m_type == Type::Group, - qWarning("Workflow Policy may only be a child of Group, skipping..."); return); - m_workflowPolicy = child.m_workflowPolicy; // TODO: Assert on redefinition? - break; - case Type::TaskHandler: - QTC_ASSERT(child.m_taskHandler.m_createHandler, - qWarning("Task Create Handler can't be null, skipping..."); return); - QTC_ASSERT(child.m_taskHandler.m_setupHandler, - qWarning("Task Setup Handler can't be null, skipping..."); return); - m_children.append(child); - break; - case Type::GroupHandler: - QTC_ASSERT(m_type == Type::Group, qWarning("Group Handler may only be a " - "child of Group, skipping..."); break); - QTC_ASSERT(!child.m_groupHandler.m_setupHandler - || !m_groupHandler.m_setupHandler, - qWarning("Group Setup Handler redefinition, overriding...")); - QTC_ASSERT(!child.m_groupHandler.m_doneHandler - || !m_groupHandler.m_doneHandler, - qWarning("Group Done Handler redefinition, overriding...")); - QTC_ASSERT(!child.m_groupHandler.m_errorHandler - || !m_groupHandler.m_errorHandler, - qWarning("Group Error Handler redefinition, overriding...")); - if (child.m_groupHandler.m_setupHandler) - m_groupHandler.m_setupHandler = child.m_groupHandler.m_setupHandler; - if (child.m_groupHandler.m_doneHandler) - m_groupHandler.m_doneHandler = child.m_groupHandler.m_doneHandler; - if (child.m_groupHandler.m_errorHandler) - m_groupHandler.m_errorHandler = child.m_groupHandler.m_errorHandler; - break; - case Type::Storage: - m_storageList.append(child.m_storageList); - break; - } - } -} - -} // namespace Tasking - -using namespace Tasking; - -class TaskTreePrivate; -class TaskNode; - -class TaskContainer -{ -public: - TaskContainer(TaskTreePrivate *taskTreePrivate, const TaskItem &task, - TaskContainer *parentContainer) - : m_constData(taskTreePrivate, task, parentContainer, this) {} - TaskAction start(); - TaskAction continueStart(TaskAction startAction, int nextChild); - TaskAction startChildren(int nextChild); - TaskAction childDone(bool success); - void stop(); - void invokeEndHandler(); - bool isRunning() const { return m_runtimeData.has_value(); } - bool isStarting() const { return isRunning() && m_runtimeData->m_startGuard.isLocked(); } - - struct ConstData { - ConstData(TaskTreePrivate *taskTreePrivate, const TaskItem &task, - TaskContainer *parentContainer, TaskContainer *thisContainer); - ~ConstData() { qDeleteAll(m_children); } - TaskTreePrivate * const m_taskTreePrivate = nullptr; - TaskContainer * const m_parentContainer = nullptr; - - const int m_parallelLimit = 1; - const WorkflowPolicy m_workflowPolicy = WorkflowPolicy::StopOnError; - const TaskItem::GroupHandler m_groupHandler; - const QList<TreeStorageBase> m_storageList; - const QList<TaskNode *> m_children; - const int m_taskCount = 0; - }; - - struct RuntimeData { - RuntimeData(const ConstData &constData); - ~RuntimeData(); - - static QList<int> createStorages(const TaskContainer::ConstData &constData); - void callStorageDoneHandlers(); - bool updateSuccessBit(bool success); - int currentLimit() const; - - const ConstData &m_constData; - const QList<int> m_storageIdList; - int m_doneCount = 0; - bool m_successBit = true; - Guard m_startGuard; - }; - - const ConstData m_constData; - std::optional<RuntimeData> m_runtimeData; -}; - -class TaskNode : public QObject -{ -public: - TaskNode(TaskTreePrivate *taskTreePrivate, const TaskItem &task, - TaskContainer *parentContainer) - : m_taskHandler(task.taskHandler()) - , m_container(taskTreePrivate, task, parentContainer) - {} - - // If returned value != Continue, childDone() needs to be called in parent container (in caller) - // in order to unwind properly. - TaskAction start(); - void stop(); - void invokeEndHandler(bool success); - bool isRunning() const { return m_task || m_container.isRunning(); } - bool isTask() const { return m_taskHandler.m_createHandler && m_taskHandler.m_setupHandler; } - int taskCount() const { return isTask() ? 1 : m_container.m_constData.m_taskCount; } - TaskContainer *parentContainer() const { return m_container.m_constData.m_parentContainer; } - -private: - const TaskItem::TaskHandler m_taskHandler; - TaskContainer m_container; - std::unique_ptr<TaskInterface> m_task; -}; - -class TaskTreePrivate -{ -public: - TaskTreePrivate(TaskTree *taskTree) - : q(taskTree) {} - - void start() { - QTC_ASSERT(m_root, return); - m_progressValue = 0; - emitStartedAndProgress(); - // TODO: check storage handlers for not existing storages in tree - for (auto it = m_storageHandlers.cbegin(); it != m_storageHandlers.cend(); ++it) { - QTC_ASSERT(m_storages.contains(it.key()), qWarning("The registered storage doesn't " - "exist in task tree. Its handlers will never be called.")); - } - m_root->start(); - } - void stop() { - QTC_ASSERT(m_root, return); - if (!m_root->isRunning()) - return; - // TODO: should we have canceled flag (passed to handler)? - // Just one done handler with result flag: - // FinishedWithSuccess, FinishedWithError, Canceled, TimedOut. - // Canceled either directly by user, or by workflow policy - doesn't matter, in both - // cases canceled from outside. - m_root->stop(); - emitError(); - } - void advanceProgress(int byValue) { - if (byValue == 0) - return; - QTC_CHECK(byValue > 0); - QTC_CHECK(m_progressValue + byValue <= m_root->taskCount()); - m_progressValue += byValue; - emitProgress(); - } - void emitStartedAndProgress() { - GuardLocker locker(m_guard); - emit q->started(); - emit q->progressValueChanged(m_progressValue); - } - void emitProgress() { - GuardLocker locker(m_guard); - emit q->progressValueChanged(m_progressValue); - } - void emitDone() { - QTC_CHECK(m_progressValue == m_root->taskCount()); - GuardLocker locker(m_guard); - emit q->done(); - } - void emitError() { - QTC_CHECK(m_progressValue == m_root->taskCount()); - GuardLocker locker(m_guard); - emit q->errorOccurred(); - } - QList<TreeStorageBase> addStorages(const QList<TreeStorageBase> &storages) { - QList<TreeStorageBase> addedStorages; - for (const TreeStorageBase &storage : storages) { - QTC_ASSERT(!m_storages.contains(storage), qWarning("Can't add the same storage into " - "one TaskTree twice, skipping..."); continue); - addedStorages << storage; - m_storages << storage; - } - return addedStorages; - } - void callSetupHandler(TreeStorageBase storage, int storageId) { - callStorageHandler(storage, storageId, &StorageHandler::m_setupHandler); - } - void callDoneHandler(TreeStorageBase storage, int storageId) { - callStorageHandler(storage, storageId, &StorageHandler::m_doneHandler); - } - struct StorageHandler { - TaskTree::StorageVoidHandler m_setupHandler = {}; - TaskTree::StorageVoidHandler m_doneHandler = {}; - }; - typedef TaskTree::StorageVoidHandler StorageHandler::*HandlerPtr; // ptr to class member - void callStorageHandler(TreeStorageBase storage, int storageId, HandlerPtr ptr) - { - const auto it = m_storageHandlers.constFind(storage); - if (it == m_storageHandlers.constEnd()) - return; - GuardLocker locker(m_guard); - const StorageHandler storageHandler = *it; - storage.activateStorage(storageId); - if (storageHandler.*ptr) - (storageHandler.*ptr)(storage.activeStorageVoid()); - storage.activateStorage(0); - } - - TaskTree *q = nullptr; - Guard m_guard; - int m_progressValue = 0; - QSet<TreeStorageBase> m_storages; - QHash<TreeStorageBase, StorageHandler> m_storageHandlers; - std::unique_ptr<TaskNode> m_root = nullptr; // Keep me last in order to destruct first -}; - -class StorageActivator -{ -public: - StorageActivator(TaskContainer *container) - : m_container(container) { activateStorages(m_container); } - ~StorageActivator() { deactivateStorages(m_container); } - -private: - static void activateStorages(TaskContainer *container) - { - QTC_ASSERT(container && container->isRunning(), return); - const TaskContainer::ConstData &constData = container->m_constData; - if (constData.m_parentContainer) - activateStorages(constData.m_parentContainer); - for (int i = 0; i < constData.m_storageList.size(); ++i) - constData.m_storageList[i].activateStorage(container->m_runtimeData->m_storageIdList.value(i)); - } - static void deactivateStorages(TaskContainer *container) - { - QTC_ASSERT(container && container->isRunning(), return); - const TaskContainer::ConstData &constData = container->m_constData; - for (int i = constData.m_storageList.size() - 1; i >= 0; --i) // iterate in reverse order - constData.m_storageList[i].activateStorage(0); - if (constData.m_parentContainer) - deactivateStorages(constData.m_parentContainer); - } - TaskContainer *m_container = nullptr; -}; - -template <typename Handler, typename ...Args, - typename ReturnType = typename std::invoke_result_t<Handler, Args...>> -ReturnType invokeHandler(TaskContainer *container, Handler &&handler, Args &&...args) -{ - StorageActivator activator(container); - GuardLocker locker(container->m_constData.m_taskTreePrivate->m_guard); - return std::invoke(std::forward<Handler>(handler), std::forward<Args>(args)...); -} - -static QList<TaskNode *> createChildren(TaskTreePrivate *taskTreePrivate, TaskContainer *container, - const TaskItem &task) -{ - QList<TaskNode *> result; - const QList<TaskItem> &children = task.children(); - for (const TaskItem &child : children) - result.append(new TaskNode(taskTreePrivate, child, container)); - return result; -} - -TaskContainer::ConstData::ConstData(TaskTreePrivate *taskTreePrivate, const TaskItem &task, - TaskContainer *parentContainer, TaskContainer *thisContainer) - : m_taskTreePrivate(taskTreePrivate) - , m_parentContainer(parentContainer) - , m_parallelLimit(task.parallelLimit()) - , m_workflowPolicy(task.workflowPolicy()) - , m_groupHandler(task.groupHandler()) - , m_storageList(taskTreePrivate->addStorages(task.storageList())) - , m_children(createChildren(taskTreePrivate, thisContainer, task)) - , m_taskCount(std::accumulate(m_children.cbegin(), m_children.cend(), 0, - [](int r, TaskNode *n) { return r + n->taskCount(); })) -{} - -QList<int> TaskContainer::RuntimeData::createStorages(const TaskContainer::ConstData &constData) -{ - QList<int> storageIdList; - for (const TreeStorageBase &storage : constData.m_storageList) { - const int storageId = storage.createStorage(); - storageIdList.append(storageId); - constData.m_taskTreePrivate->callSetupHandler(storage, storageId); - } - return storageIdList; -} - -void TaskContainer::RuntimeData::callStorageDoneHandlers() -{ - for (int i = m_constData.m_storageList.size() - 1; i >= 0; --i) { // iterate in reverse order - const TreeStorageBase storage = m_constData.m_storageList[i]; - const int storageId = m_storageIdList.value(i); - m_constData.m_taskTreePrivate->callDoneHandler(storage, storageId); - } -} - -TaskContainer::RuntimeData::RuntimeData(const ConstData &constData) - : m_constData(constData) - , m_storageIdList(createStorages(constData)) -{ - m_successBit = m_constData.m_workflowPolicy != WorkflowPolicy::StopOnDone - && m_constData.m_workflowPolicy != WorkflowPolicy::ContinueOnDone; -} - -TaskContainer::RuntimeData::~RuntimeData() -{ - for (int i = m_constData.m_storageList.size() - 1; i >= 0; --i) { // iterate in reverse order - const TreeStorageBase storage = m_constData.m_storageList[i]; - const int storageId = m_storageIdList.value(i); - storage.deleteStorage(storageId); - } -} - -bool TaskContainer::RuntimeData::updateSuccessBit(bool success) -{ - if (m_constData.m_workflowPolicy == WorkflowPolicy::Optional) - return m_successBit; - - const bool donePolicy = m_constData.m_workflowPolicy == WorkflowPolicy::StopOnDone - || m_constData.m_workflowPolicy == WorkflowPolicy::ContinueOnDone; - m_successBit = donePolicy ? (m_successBit || success) : (m_successBit && success); - return m_successBit; -} - -int TaskContainer::RuntimeData::currentLimit() const -{ - const int childCount = m_constData.m_children.size(); - return m_constData.m_parallelLimit - ? qMin(m_doneCount + m_constData.m_parallelLimit, childCount) : childCount; -} - -TaskAction TaskContainer::start() -{ - QTC_CHECK(!isRunning()); - m_runtimeData.emplace(m_constData); - - TaskAction startAction = TaskAction::Continue; - if (m_constData.m_groupHandler.m_setupHandler) { - startAction = invokeHandler(this, m_constData.m_groupHandler.m_setupHandler); - if (startAction != TaskAction::Continue) - m_constData.m_taskTreePrivate->advanceProgress(m_constData.m_taskCount); - } - if (m_constData.m_children.isEmpty() && startAction == TaskAction::Continue) - startAction = TaskAction::StopWithDone; - return continueStart(startAction, 0); -} - -TaskAction TaskContainer::continueStart(TaskAction startAction, int nextChild) -{ - const TaskAction groupAction = startAction == TaskAction::Continue ? startChildren(nextChild) - : startAction; - QTC_CHECK(isRunning()); // TODO: superfluous - if (groupAction != TaskAction::Continue) { - const bool success = m_runtimeData->updateSuccessBit(groupAction == TaskAction::StopWithDone); - invokeEndHandler(); - if (TaskContainer *parentContainer = m_constData.m_parentContainer) { - QTC_CHECK(parentContainer->isRunning()); - if (!parentContainer->isStarting()) - parentContainer->childDone(success); - } else if (success) { - m_constData.m_taskTreePrivate->emitDone(); - } else { - m_constData.m_taskTreePrivate->emitError(); - } - } - return groupAction; -} - -TaskAction TaskContainer::startChildren(int nextChild) -{ - QTC_CHECK(isRunning()); - GuardLocker locker(m_runtimeData->m_startGuard); - for (int i = nextChild; i < m_constData.m_children.size(); ++i) { - const int limit = m_runtimeData->currentLimit(); - if (i >= limit) - break; - - const TaskAction startAction = m_constData.m_children.at(i)->start(); - if (startAction == TaskAction::Continue) - continue; - - const TaskAction finalizeAction = childDone(startAction == TaskAction::StopWithDone); - if (finalizeAction == TaskAction::Continue) - continue; - - int skippedTaskCount = 0; - // Skip scheduled but not run yet. The current (i) was already notified. - for (int j = i + 1; j < limit; ++j) - skippedTaskCount += m_constData.m_children.at(j)->taskCount(); - m_constData.m_taskTreePrivate->advanceProgress(skippedTaskCount); - return finalizeAction; - } - return TaskAction::Continue; -} - -TaskAction TaskContainer::childDone(bool success) -{ - QTC_CHECK(isRunning()); - const int limit = m_runtimeData->currentLimit(); // Read before bumping m_doneCount and stop() - const bool shouldStop = (m_constData.m_workflowPolicy == WorkflowPolicy::StopOnDone && success) - || (m_constData.m_workflowPolicy == WorkflowPolicy::StopOnError && !success); - if (shouldStop) - stop(); - - ++m_runtimeData->m_doneCount; - const bool updatedSuccess = m_runtimeData->updateSuccessBit(success); - const TaskAction startAction - = (shouldStop || m_runtimeData->m_doneCount == m_constData.m_children.size()) - ? toTaskAction(updatedSuccess) : TaskAction::Continue; - - if (isStarting()) - return startAction; - return continueStart(startAction, limit); -} - -void TaskContainer::stop() -{ - if (!isRunning()) - return; - - const int limit = m_runtimeData->currentLimit(); - for (int i = 0; i < limit; ++i) - m_constData.m_children.at(i)->stop(); - - int skippedTaskCount = 0; - for (int i = limit; i < m_constData.m_children.size(); ++i) - skippedTaskCount += m_constData.m_children.at(i)->taskCount(); - - m_constData.m_taskTreePrivate->advanceProgress(skippedTaskCount); -} - -void TaskContainer::invokeEndHandler() -{ - const TaskItem::GroupHandler &groupHandler = m_constData.m_groupHandler; - if (m_runtimeData->m_successBit && groupHandler.m_doneHandler) - invokeHandler(this, groupHandler.m_doneHandler); - else if (!m_runtimeData->m_successBit && groupHandler.m_errorHandler) - invokeHandler(this, groupHandler.m_errorHandler); - m_runtimeData->callStorageDoneHandlers(); - m_runtimeData.reset(); -} - -TaskAction TaskNode::start() -{ - QTC_CHECK(!isRunning()); - if (!isTask()) - return m_container.start(); - - m_task.reset(m_taskHandler.m_createHandler()); - const TaskAction startAction = invokeHandler(parentContainer(), m_taskHandler.m_setupHandler, - *m_task.get()); - if (startAction != TaskAction::Continue) { - m_container.m_constData.m_taskTreePrivate->advanceProgress(1); - m_task.reset(); - return startAction; - } - const std::shared_ptr<TaskAction> unwindAction - = std::make_shared<TaskAction>(TaskAction::Continue); - connect(m_task.get(), &TaskInterface::done, this, [=](bool success) { - invokeEndHandler(success); - disconnect(m_task.get(), &TaskInterface::done, this, nullptr); - m_task.release()->deleteLater(); - QTC_ASSERT(parentContainer() && parentContainer()->isRunning(), return); - if (parentContainer()->isStarting()) - *unwindAction = toTaskAction(success); - else - parentContainer()->childDone(success); - }); - - m_task->start(); - return *unwindAction; -} - -void TaskNode::stop() -{ - if (!isRunning()) - return; - - if (!m_task) { - m_container.stop(); - m_container.invokeEndHandler(); - return; - } - - // TODO: cancelHandler? - // TODO: call TaskInterface::stop() ? - invokeEndHandler(false); - m_task.reset(); -} - -void TaskNode::invokeEndHandler(bool success) -{ - if (success && m_taskHandler.m_doneHandler) - invokeHandler(parentContainer(), m_taskHandler.m_doneHandler, *m_task.get()); - else if (!success && m_taskHandler.m_errorHandler) - invokeHandler(parentContainer(), m_taskHandler.m_errorHandler, *m_task.get()); - m_container.m_constData.m_taskTreePrivate->advanceProgress(1); -} - -/*! - \class Utils::TaskTree - \inheaderfile utils/tasktree.h - \inmodule QtCreator - \ingroup mainclasses - \brief The TaskTree class runs an async task tree structure defined in a - declarative way. - - Use the Tasking namespace to build extensible, declarative task tree - structures that contain possibly asynchronous tasks, such as QtcProcess, - FileTransfer, or AsyncTask<ReturnType>. TaskTree structures enable you - to create a sophisticated mixture of a parallel or sequential flow of tasks - in the form of a tree and to run it any time later. - - \section1 Root Element and Tasks - - The TaskTree has a mandatory Group root element, which may contain - any number of tasks of various types, such as Process, FileTransfer, - or AsyncTask<ReturnType>: - - \code - using namespace Utils; - using namespace Tasking; - - const Group root { - Process(...), - Async<int>(...), - Transfer(...) - }; - - TaskTree *taskTree = new TaskTree(root); - connect(taskTree, &TaskTree::done, ...); // a successfully finished handler - connect(taskTree, &TaskTree::errorOccurred, ...); // an erroneously finished handler - taskTree->start(); - \endcode - - The task tree above has a top level element of the Group type that contains - tasks of the type QtcProcess, FileTransfer, and AsyncTask<int>. - After taskTree->start() is called, the tasks are run in a chain, starting - with Process. When Process finishes successfully, the Async<int> task is - started. Finally, when the asynchronous task finishes successfully, the - FileTransfer task is started. - - When the last running task finishes with success, the task tree is considered - to have run successfully and the TaskTree::done() signal is emitted. - When a task finishes with an error, the execution of the task tree is stopped - and the remaining tasks are skipped. The task tree finishes with an error and - sends the TaskTree::errorOccurred() signal. - - \section1 Groups - - The parent of the Group sees it as a single task. Like other tasks, - the group can be started and it can finish with success or an error. - The Group elements can be nested to create a tree structure: - - \code - const Group root { - Group { - parallel, - Process(...), - Async<int>(...) - }, - Transfer(...) - }; - \endcode - - The example above differs from the first example in that the root element has - a subgroup that contains the Process and Async<int> tasks. The subgroup is a - sibling element of the Transfer task in the root. The subgroup contains an - additional \e parallel element that instructs its Group to execute its tasks - in parallel. - - So, when the tree above is started, the Process and Async<int> tasks start - immediately and run in parallel. Since the root group doesn't contain a - \e parallel element, its direct child tasks are run in sequence. Thus, the - Transfer task starts when the whole subgroup finishes. The group is - considered as finished when all its tasks have finished. The order in which - the tasks finish is not relevant. - - So, depending on which task lasts longer (Process or Async<int>), the - following scenarios can take place: - - \table - \header - \li Scenario 1 - \li Scenario 2 - \row - \li Root Group starts - \li Root Group starts - \row - \li Sub Group starts - \li Sub Group starts - \row - \li Process starts - \li Process starts - \row - \li Async<int> starts - \li Async<int> starts - \row - \li ... - \li ... - \row - \li \b {Process finishes} - \li \b {Async<int> finishes} - \row - \li ... - \li ... - \row - \li \b {Async<int> finishes} - \li \b {Process finishes} - \row - \li Sub Group finishes - \li Sub Group finishes - \row - \li Transfer starts - \li Transfer starts - \row - \li ... - \li ... - \row - \li Transfer finishes - \li Transfer finishes - \row - \li Root Group finishes - \li Root Group finishes - \endtable - - The differences between the scenarios are marked with bold. Three dots mean - that an unspecified amount of time passes between previous and next events - (a task or tasks continue to run). No dots between events - means that they occur synchronously. - - The presented scenarios assume that all tasks run successfully. If a task - fails during execution, the task tree finishes with an error. In particular, - when Process finishes with an error while Async<int> is still being executed, - Async<int> is automatically stopped, the subgroup finishes with an error, - Transfer is skipped, and the tree finishes with an error. - - \section1 Task Types - - Each task type is associated with its corresponding task class that executes - the task. For example, a Process task inside a task tree is associated with - the QtcProcess class that executes the process. The associated objects are - automatically created, started, and destructed exclusively by the task tree - at the appropriate time. - - If a root group consists of five sequential Process tasks, and the task tree - executes the group, it creates an instance of QtcProcess for the first - Process task and starts it. If the QtcProcess instance finishes successfully, - the task tree destructs it and creates a new QtcProcess instance for the - second Process, and so on. If the first task finishes with an error, the task - tree stops creating QtcProcess instances, and the root group finishes with an - error. - - The following table shows examples of task types and their corresponding task - classes: - - \table - \header - \li Task Type (Tasking Namespace) - \li Associated Task Class - \li Brief Description - \row - \li Process - \li Utils::QtcProcess - \li Starts processes. - \row - \li Async<ReturnType> - \li Utils::AsyncTask<ReturnType> - \li Starts asynchronous tasks; run in separate thread. - \row - \li Tree - \li Utils::TaskTree - \li Starts a nested task tree. - \row - \li Transfer - \li ProjectExplorer::FileTransfer - \li Starts file transfer between different devices. - \endtable - - \section1 Task Handlers - - Use Task handlers to set up a task for execution and to enable reading - the output data from the task when it finishes with success or an error. - - \section2 Task Start Handler - - When a corresponding task class object is created and before it's started, - the task tree invokes a mandatory user-provided setup handler. The setup - handler should always take a \e reference to the associated task class object: - - \code - const auto onSetup = [](QtcProcess &process) { - process.setCommand({"sleep", {"3"}}); - }; - const Group root { - Process(onSetup) - }; - \endcode - - You can modify the passed QtcProcess in the setup handler, so that the task - tree can start the process according to your configuration. - You do not need to call \e {process.start();} in the setup handler, - as the task tree calls it when needed. The setup handler is mandatory - and must be the first argument of the task's constructor. - - Optionally, the setup handler may return a TaskAction. The returned - TaskAction influences the further start behavior of a given task. The - possible values are: - - \table - \header - \li TaskAction Value - \li Brief Description - \row - \li Continue - \li The task is started normally. This is the default behavior when the - setup handler doesn't return TaskAction (that is, its return type is - void). - \row - \li StopWithDone - \li The task won't be started and it will report success to its parent. - \row - \li StopWithError - \li The task won't be started and it will report an error to its parent. - \endtable - - This is useful for running a task only when a condition is met and the data - needed to evaluate this condition is not known until previously started tasks - finish. This way, the setup handler dynamically decides whether to start the - corresponding task normally or skip it and report success or an error. - For more information about inter-task data exchange, see \l Storage. - - \section2 Task's Done and Error Handlers - - When a running task finishes, the task tree invokes an optionally provided - done or error handler. Both handlers should always take a \e {const reference} - to the associated task class object: - - \code - const auto onSetup = [](QtcProcess &process) { - process.setCommand({"sleep", {"3"}}); - }; - const auto onDone = [](const QtcProcess &process) { - qDebug() << "Success" << process.cleanedStdOut(); - }; - const auto onError = [](const QtcProcess &process) { - qDebug() << "Failure" << process.cleanedStdErr(); - }; - const Group root { - Process(onSetup, onDone, onError) - }; - \endcode - - The done and error handlers may collect output data from QtcProcess, and store it - for further processing or perform additional actions. The done handler is optional. - When used, it must be the second argument of the task constructor. - The error handler must always be the third argument. - You can omit the handlers or substitute the ones that you do not need with curly braces ({}). - - \note If the task setup handler returns StopWithDone or StopWithError, - neither the done nor error handler is invoked. - - \section1 Group Handlers - - Similarly to task handlers, group handlers enable you to set up a group to - execute and to apply more actions when the whole group finishes with - success or an error. - - \section2 Group's Start Handler - - The task tree invokes the group start handler before it starts the child - tasks. The group handler doesn't take any arguments: - - \code - const auto onGroupSetup = [] { - qDebug() << "Entering the group"; - }; - const Group root { - OnGroupSetup(onGroupSetup), - Process(...) - }; - \endcode - - The group setup handler is optional. To define a group setup handler, add an - OnGroupSetup element to a group. The argument of OnGroupSetup is a user - handler. If you add more than one OnGroupSetup element to a group, an assert - is triggered at runtime that includes an error message. - - Like the task start handler, the group start handler may return TaskAction. - The returned TaskAction value affects the start behavior of the - whole group. If you do not specify a group start handler or its return type - is void, the default group's action is TaskAction::Continue, so that all - tasks are started normally. Otherwise, when the start handler returns - TaskAction::StopWithDone or TaskAction::StopWithError, the tasks are not - started (they are skipped) and the group itself reports success or failure, - depending on the returned value, respectively. - - \code - const Group root { - OnGroupSetup([] { qDebug() << "Root setup"; }), - Group { - OnGroupSetup([] { qDebug() << "Group 1 setup"; return TaskAction::Continue; }), - Process(...) // Process 1 - }, - Group { - OnGroupSetup([] { qDebug() << "Group 2 setup"; return TaskAction::StopWithDone; }), - Process(...) // Process 2 - }, - Group { - OnGroupSetup([] { qDebug() << "Group 3 setup"; return TaskAction::StopWithError; }), - Process(...) // Process 3 - }, - Process(...) // Process 4 - }; - \endcode - - In the above example, all subgroups of a root group define their setup handlers. - The following scenario assumes that all started processes finish with success: - - \table - \header - \li Scenario - \li Comment - \row - \li Root Group starts - \li Doesn't return TaskAction, so its tasks are executed. - \row - \li Group 1 starts - \li Returns Continue, so its tasks are executed. - \row - \li Process 1 starts - \li - \row - \li ... - \li ... - \row - \li Process 1 finishes (success) - \li - \row - \li Group 1 finishes (success) - \li - \row - \li Group 2 starts - \li Returns StopWithDone, so Process 2 is skipped and Group 2 reports - success. - \row - \li Group 2 finishes (success) - \li - \row - \li Group 3 starts - \li Returns StopWithError, so Process 3 is skipped and Group 3 reports - an error. - \row - \li Group 3 finishes (error) - \li - \row - \li Root Group finishes (error) - \li Group 3, which is a direct child of the root group, finished with an - error, so the root group stops executing, skips Process 4, which has - not started yet, and reports an error. - \endtable - - \section2 Groups's Done and Error Handlers - - A Group's done or error handler is executed after the successful or failed - execution of its tasks, respectively. The final value reported by the - group depends on its \l {Workflow Policy}. The handlers can apply other - necessary actions. The done and error handlers are defined inside the - OnGroupDone and OnGroupError elements of a group, respectively. They do not - take arguments: - - \code - const Group root { - OnGroupSetup([] { qDebug() << "Root setup"; }), - Process(...), - OnGroupDone([] { qDebug() << "Root finished with success"; }), - OnGroupError([] { qDebug() << "Root finished with error"; }) - }; - \endcode - - The group done and error handlers are optional. If you add more than one - OnGroupDone or OnGroupError each to a group, an assert is triggered at - runtime that includes an error message. - - \note Even if the group setup handler returns StopWithDone or StopWithError, - one of the task's done or error handlers is invoked. This behavior differs - from that of task handlers and might change in the future. - - \section1 Other Group Elements - - A group can contain other elements that describe the processing flow, such as - the execution mode or workflow policy. It can also contain storage elements - that are responsible for collecting and sharing custom common data gathered - during group execution. - - \section2 Execution Mode - - The execution mode element in a Group specifies how the direct child tasks of - the Group are started. - - \table - \header - \li Execution Mode - \li Description - \row - \li sequential - \li Default. When a Group has no execution mode, it runs in the - sequential mode. All the direct child tasks of a group are started - in a chain, so that when one task finishes, the next one starts. - This enables you to pass the results from the previous task - as input to the next task before it starts. This mode guarantees - that the next task is started only after the previous task finishes. - \row - \li parallel - \li All the direct child tasks of a group are started after the group is - started, without waiting for the previous tasks to finish. In this - mode, all tasks run simultaneously. - \row - \li ParallelLimit(int limit) - \li In this mode, a limited number of direct child tasks run simultaneously. - The \e limit defines the maximum number of tasks running in parallel - in a group. When the group is started, the first batch tasks is - started (the number of tasks in batch equals to passed limit, at most), - while the others are kept waiting. When a running task finishes, - the group starts the next remaining one, so that the \e limit - of simultaneously running tasks inside a group isn't exceeded. - This repeats on every child task's finish until all child tasks are started. - This enables you to limit the maximum number of tasks that - run simultaneously, for example if running too many processes might - block the machine for a long time. The value 1 means \e sequential - execution. The value 0 means unlimited and equals \e parallel. - \endtable - - In all execution modes, a group starts tasks in the oder in which they appear. - - If a child of a group is also a group (in a nested tree), the child group - runs its tasks according to its own execution mode. - - \section2 Workflow Policy - - The workflow policy element in a Group specifies how the group should behave - when its direct child tasks finish: - - \table - \header - \li Workflow Policy - \li Description - \row - \li stopOnError - \li Default. If a task finishes with an error, the group: - \list 1 - \li Stops the running tasks (if any - for example, in parallel - mode). - \li Skips executing tasks it has not started (for example, in the - sequential mode). - \li Immediately finishes with an error. - \endlist - If all child tasks finish successfully or the group is empty, the group - finishes with success. - \row - \li continueOnError - \li Similar to stopOnError, but in case any child finishes with - an error, the execution continues until all tasks finish, - and the group reports an error afterwards, even when some other - tasks in group finished with success. - If a task finishes with an error, the group: - \list 1 - \li Continues executing the tasks that are running or have not - started yet. - \li Finishes with an error when all tasks finish. - \endlist - If all tasks finish successfully or the group is empty, the group - finishes with success. - \row - \li stopOnDone - \li If a task finishes with success, the group: - \list 1 - \li Stops running tasks and skips those that it has not started. - \li Immediately finishes with success. - \endlist - If all tasks finish with an error or the group is empty, the group - finishes with an error. - \row - \li continueOnDone - \li Similar to stopOnDone, but in case any child finishes - successfully, the execution continues until all tasks finish, - and the group reports success afterwards, even when some other - tasks in group finished with an error. - If a task finishes with success, the group: - \list 1 - \li Continues executing the tasks that are running or have not - started yet. - \li Finishes with success when all tasks finish. - \endlist - If all tasks finish with an error or the group is empty, the group - finishes with an error. - \row - \li optional - \li The group executes all tasks and ignores their return state. If all - tasks finish or the group is empty, the group finishes with success. - \endtable - - If a child of a group is also a group (in a nested tree), the child group - runs its tasks according to its own workflow policy. - - \section2 Storage - - Use the Storage element to exchange information between tasks. Especially, - in the sequential execution mode, when a task needs data from another task - before it can start. For example, a task tree that copies data by reading - it from a source and writing it to a destination might look as follows: - - \code - static QByteArray load(const FilePath &fileName) { ... } - static void save(const FilePath &fileName, const QByteArray &array) { ... } - - static TaskItem diffRecipe(const FilePath &source, const FilePath &destination) - { - struct CopyStorage { // [1] custom inter-task struct - QByteArray content; // [2] custom inter-task data - }; - - // [3] instance of custom inter-task struct manageable by task tree - const TreeStorage<CopyStorage> storage; - - const auto onLoaderSetup = [source](Async<QByteArray> &async) { - async.setAsyncCallData(&load, source); - }; - // [4] runtime: task tree activates the instance from [5] before invoking handler - const auto onLoaderDone = [storage](const Async<QByteArray> &async) { - storage->content = async.result(); - }; - - // [4] runtime: task tree activates the instance from [5] before invoking handler - const auto onSaverSetup = [storage, destination](Async<void> &async) { - async.setAsyncCallData(&save, destination, storage->content); - }; - const auto onSaverDone = [](const Async<void> &async) { - qDebug() << "Save done successfully"; - }; - - const Group root { - // [5] runtime: task tree creates an instance of CopyStorage when root is entered - Storage(storage), - Async<QByteArray>(onLoaderSetup, onLoaderDone), - Async<void>(onSaverSetup, onSaverDone) - }; - return root; - } - \endcode - - In the example above, the inter-task data consists of a QByteArray content - variable [2] enclosed in a CopyStorage custom struct [1]. If the loader - finishes successfully, it stores the data in a CopyStorage::content - variable. The saver then uses the variable to configure the saving task. - - To enable a task tree to manage the CopyStorage struct, an instance of - TreeStorage<CopyStorage> is created [3]. If a copy of this object is - inserted as group's child task [5], an instance of CopyStorage struct is - created dynamically when the task tree enters this group. When the task - tree leaves this group, the existing instance of CopyStorage struct is - destructed as it's no longer needed. - - If several task trees that hold a copy of the common TreeStorage<CopyStorage> - instance run simultaneously, each task tree contains its own copy of the - CopyStorage struct. - - You can access CopyStorage from any handler in the group with a storage object. - This includes all handlers of all descendant tasks of the group with - a storage object. To access the custom struct in a handler, pass the - copy of the TreeStorage<CopyStorage> object to the handler (for example, in - a lambda capture) [4]. - - When the task tree invokes a handler in a subtree containing the storage [5], - the task tree activates its own CopyStorage instance inside the - TreeStorage<CopyStorage> object. Therefore, the CopyStorage struct may be - accessed only from within the handler body. To access the currently active - CopyStorage from within TreeStorage<CopyStorage>, use the TreeStorage::operator->() - or TreeStorage::activeStorage() method. - - The following list summarizes how to employ a Storage object into the task - tree: - \list 1 - \li Define the custom structure MyStorage with custom data [1], [2] - \li Create an instance of TreeStorage<MyStorage> storage [3] - \li Pass the TreeStorage<MyStorage> instance to handlers [4] - \li Insert the TreeStorage<MyStorage> instance into a group [5] - \endlist - - \note The current implementation assumes that all running task trees - containing copies of the same TreeStorage run in the same thread. Otherwise, - the behavior is undefined. - - \section1 TaskTree - - TaskTree executes the tree structure of asynchronous tasks according to the - recipe described by the Group root element. - - As TaskTree is also an asynchronous task, it can be a part of another TaskTree. - To place a nested TaskTree inside another TaskTree, insert the Tasking::Tree - element into other tree's Group element. - - TaskTree reports progress of completed tasks when running. The progress value - is increased when a task finishes or is skipped or stopped. - When TaskTree is finished and the TaskTree::done() or TaskTree::errorOccurred() - signal is emitted, the current value of the progress equals the maximum - progress value. Maximum progress equals the total number of tasks in a tree. - A nested TaskTree is counted as a single task, and its child tasks are not - counted in the top level tree. Groups themselves are not counted as tasks, - but their tasks are counted. - - To set additional initial data for the running tree, modify the storage - instances in a tree when it creates them by installing a storage setup - handler: - - \code - TreeStorage<CopyStorage> storage; - Group root = ...; // storage placed inside root's group and inside handlers - TaskTree taskTree(root); - auto initStorage = [](CopyStorage *storage){ - storage->content = "initial content"; - }; - taskTree.onStorageSetup(storage, initStorage); - taskTree.start(); - \endcode - - When the running task tree creates a CopyStorage instance, and before any - handler inside a tree is called, the task tree calls the initStorage handler, - to enable setting up initial data of the storage, unique to this particular - run of taskTree. - - Similarly, to collect some additional result data from the running tree, - read it from storage instances in the tree when they are about to be - destroyed. To do this, install a storage done handler: - - \code - TreeStorage<CopyStorage> storage; - Group root = ...; // storage placed inside root's group and inside handlers - TaskTree taskTree(root); - auto collectStorage = [](CopyStorage *storage){ - qDebug() << "final content" << storage->content; - }; - taskTree.onStorageDone(storage, collectStorage); - taskTree.start(); - \endcode - - When the running task tree is about to destroy a CopyStorage instance, the - task tree calls the collectStorage handler, to enable reading the final data - from the storage, unique to this particular run of taskTree. - - \section1 Task Adapters - - To extend a TaskTree with new a task type, implement a simple adapter class - derived from the TaskAdapter class template. The following class is an - adapter for a single shot timer, which may be considered as a new - asynchronous task: - - \code - class TimeoutAdapter : public Utils::Tasking::TaskAdapter<QTimer> - { - public: - TimeoutAdapter() { - task()->setSingleShot(true); - task()->setInterval(1000); - connect(task(), &QTimer::timeout, this, [this] { emit done(true); }); - } - void start() final { task()->start(); } - }; - - QTC_DECLARE_CUSTOM_TASK(Timeout, TimeoutAdapter); - \endcode - - You must derive the custom adapter from the TaskAdapter class template - instantiated with a template parameter of the class implementing a running - task. The code above uses QTimer to run the task. This class appears - later as an argument to the task's handlers. The instance of this class - parameter automatically becomes a member of the TaskAdapter template, and is - accessible through the TaskAdapter::task() method. The constructor - of TimeoutAdapter initially configures the QTimer object and connects - to the QTimer::timeout signal. When the signal is triggered, TimeoutAdapter - emits the done(true) signal to inform the task tree that the task finished - successfully. If it emits done(false), the task finished with an error. - The TaskAdapter::start() method starts the timer. - - To make QTimer accessible inside TaskTree under the \e Timeout name, - register it with QTC_DECLARE_CUSTOM_TASK(Timeout, TimeoutAdapter). Timeout - becomes a new task type inside Utils::Tasking namespace, using TimeoutAdapter. - - The new task type is now registered, and you can use it in TaskTree: - - \code - const auto onTimeoutSetup = [](QTimer &task) { - task.setInterval(2000); - }; - const auto onTimeoutDone = [](const QTimer &task) { - qDebug() << "timeout triggered"; - }; - - const Group root { - Timeout(onTimeoutSetup, onTimeoutDone) - }; - \endcode - - When a task tree containing the root from the above example is started, it - prints a debug message within two seconds and then finishes successfully. - - \note The class implementing the running task should have a default constructor, - and objects of this class should be freely destructible. It should be allowed - to destroy a running object, preferably without waiting for the running task - to finish (that is, safe non-blocking destructor of a running task). -*/ - -TaskTree::TaskTree() - : d(new TaskTreePrivate(this)) -{ -} - -TaskTree::TaskTree(const Group &root) : TaskTree() -{ - setupRoot(root); -} - -TaskTree::~TaskTree() -{ - QTC_ASSERT(!d->m_guard.isLocked(), qWarning("Deleting TaskTree instance directly from " - "one of its handlers will lead to crash!")); - delete d; -} - -void TaskTree::setupRoot(const Tasking::Group &root) -{ - QTC_ASSERT(!isRunning(), qWarning("The TaskTree is already running, ignoring..."); return); - QTC_ASSERT(!d->m_guard.isLocked(), qWarning("The setupRoot() is called from one of the" - "TaskTree handlers, ingoring..."); return); - d->m_storages.clear(); - d->m_root.reset(new TaskNode(d, root, nullptr)); -} - -void TaskTree::start() -{ - QTC_ASSERT(!isRunning(), qWarning("The TaskTree is already running, ignoring..."); return); - QTC_ASSERT(!d->m_guard.isLocked(), qWarning("The start() is called from one of the" - "TaskTree handlers, ingoring..."); return); - d->start(); -} - -void TaskTree::stop() -{ - QTC_ASSERT(!d->m_guard.isLocked(), qWarning("The stop() is called from one of the" - "TaskTree handlers, ingoring..."); return); - d->stop(); -} - -bool TaskTree::isRunning() const -{ - return d->m_root && d->m_root->isRunning(); -} - -int TaskTree::taskCount() const -{ - return d->m_root ? d->m_root->taskCount() : 0; -} - -int TaskTree::progressValue() const -{ - return d->m_progressValue; -} - -void TaskTree::setupStorageHandler(const Tasking::TreeStorageBase &storage, - StorageVoidHandler setupHandler, - StorageVoidHandler doneHandler) -{ - auto it = d->m_storageHandlers.find(storage); - if (it == d->m_storageHandlers.end()) { - d->m_storageHandlers.insert(storage, {setupHandler, doneHandler}); - return; - } - if (setupHandler) { - QTC_ASSERT(!it->m_setupHandler, - qWarning("The storage has its setup handler defined, overriding...")); - it->m_setupHandler = setupHandler; - } - if (doneHandler) { - QTC_ASSERT(!it->m_doneHandler, - qWarning("The storage has its done handler defined, overriding...")); - it->m_doneHandler = doneHandler; - } -} - -TaskTreeAdapter::TaskTreeAdapter() -{ - connect(task(), &TaskTree::done, this, [this] { emit done(true); }); - connect(task(), &TaskTree::errorOccurred, this, [this] { emit done(false); }); -} - -void TaskTreeAdapter::start() -{ - task()->start(); -} - -} // namespace Utils |