// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #pragma once #include "tasking_global.h" #include #include #include #include QT_BEGIN_NAMESPACE template class QFuture; QT_END_NAMESPACE namespace Tasking { Q_NAMESPACE_EXPORT(TASKING_EXPORT) class ExecutionContextActivator; class TaskContainer; class TaskTreePrivate; class TASKING_EXPORT TaskInterface : public QObject { Q_OBJECT signals: void done(bool success); private: template friend class TaskAdapter; friend class TaskNode; TaskInterface() = default; #ifdef Q_QDOC protected: #endif virtual void start() = 0; }; class TASKING_EXPORT TreeStorageBase { public: bool isValid() const; private: using StorageConstructor = std::function; using StorageDestructor = std::function; TreeStorageBase(StorageConstructor ctor, StorageDestructor dtor); void *activeStorageVoid() const; int createStorage() const; void deleteStorage(int id) const; void activateStorage(int id) const; friend bool operator==(const TreeStorageBase &first, const TreeStorageBase &second) { return first.m_storageData == second.m_storageData; } friend bool operator!=(const TreeStorageBase &first, const TreeStorageBase &second) { return first.m_storageData != second.m_storageData; } friend size_t qHash(const TreeStorageBase &storage, uint seed = 0) { return size_t(storage.m_storageData.get()) ^ seed; } struct StorageData { ~StorageData(); StorageConstructor m_constructor = {}; StorageDestructor m_destructor = {}; QHash m_storageHash = {}; int m_activeStorage = 0; // 0 means no active storage int m_storageCounter = 0; }; QSharedPointer m_storageData; template friend class TreeStorage; friend ExecutionContextActivator; friend TaskContainer; friend TaskTreePrivate; }; template class TreeStorage final : public TreeStorageBase { public: TreeStorage() : TreeStorageBase(TreeStorage::ctor(), TreeStorage::dtor()) {} StorageStruct &operator*() const noexcept { return *activeStorage(); } StorageStruct *operator->() const noexcept { return activeStorage(); } StorageStruct *activeStorage() const { return static_cast(activeStorageVoid()); } private: static StorageConstructor ctor() { return [] { return new StorageStruct; }; } static StorageDestructor dtor() { return [](void *storage) { delete static_cast(storage); }; } }; // WorkflowPolicy: // 1. When all children finished with done -> report done, otherwise: // a) Report error on first error and stop executing other children (including their subtree). // b) On first error - continue executing all children and report error afterwards. // 2. When all children finished with error -> report error, otherwise: // a) Report done on first done and stop executing other children (including their subtree). // b) On first done - continue executing all children and report done afterwards. // 3. Stops on first finished child. In sequential mode it will never run other children then the first one. // Useful only in parallel mode. // 4. Always run all children, let them finish, ignore their results and report done afterwards. // 5. Always run all children, let them finish, ignore their results and report error afterwards. enum class WorkflowPolicy { StopOnError, // 1a - Reports error on first child error, otherwise done (if all children were done). ContinueOnError, // 1b - The same, but children execution continues. Reports done when no children. StopOnDone, // 2a - Reports done on first child done, otherwise error (if all children were error). ContinueOnDone, // 2b - The same, but children execution continues. Reports error when no children. StopOnFinished, // 3 - Stops on first finished child and report its result. FinishAllAndDone, // 4 - Reports done after all children finished. FinishAllAndError // 5 - Reports error after all children finished. }; Q_ENUM_NS(WorkflowPolicy); enum class SetupResult { Continue, StopWithDone, StopWithError }; Q_ENUM_NS(SetupResult); class TASKING_EXPORT GroupItem { public: // Internal, provided by QTC_DECLARE_CUSTOM_TASK using TaskCreateHandler = std::function; // Called prior to task start, just after createHandler using TaskSetupHandler = std::function; // Called on task done / error using TaskEndHandler = std::function; // Called when group entered using GroupSetupHandler = std::function; // Called when group done / error using GroupEndHandler = std::function; struct TaskHandler { TaskCreateHandler m_createHandler; TaskSetupHandler m_setupHandler = {}; TaskEndHandler m_doneHandler = {}; TaskEndHandler m_errorHandler = {}; }; struct GroupHandler { GroupSetupHandler m_setupHandler; GroupEndHandler m_doneHandler = {}; GroupEndHandler m_errorHandler = {}; }; struct GroupData { GroupHandler m_groupHandler = {}; std::optional m_parallelLimit = {}; std::optional m_workflowPolicy = {}; }; QList children() const { return m_children; } GroupData groupData() const { return m_groupData; } QList storageList() const { return m_storageList; } TaskHandler taskHandler() const { return m_taskHandler; } protected: enum class Type { Group, GroupData, Storage, TaskHandler }; GroupItem() = default; GroupItem(const GroupData &data) : m_type(Type::GroupData) , m_groupData(data) {} GroupItem(const TreeStorageBase &storage) : m_type(Type::Storage) , m_storageList{storage} {} GroupItem(const TaskHandler &handler) : m_type(Type::TaskHandler) , m_taskHandler(handler) {} void addChildren(const QList &children); static GroupItem groupHandler(const GroupHandler &handler) { return GroupItem({handler}); } static GroupItem parallelLimit(int limit) { return GroupItem({{}, limit}); } static GroupItem workflowPolicy(WorkflowPolicy policy) { return GroupItem({{}, {}, policy}); } static GroupItem withTimeout(const GroupItem &item, std::chrono::milliseconds timeout, const GroupEndHandler &handler = {}); private: Type m_type = Type::Group; QList m_children; GroupData m_groupData; QList m_storageList; TaskHandler m_taskHandler; }; class TASKING_EXPORT Group final : public GroupItem { public: Group(const QList &children) { addChildren(children); } Group(std::initializer_list children) { addChildren(children); } // GroupData related: template static GroupItem onGroupSetup(SetupHandler &&handler) { return groupHandler({wrapGroupSetup(std::forward(handler))}); } static GroupItem onGroupDone(const GroupEndHandler &handler) { return groupHandler({{}, handler}); } static GroupItem onGroupError(const GroupEndHandler &handler) { return groupHandler({{}, {}, handler}); } using GroupItem::parallelLimit; // Default: 1 (sequential). 0 means unlimited (parallel). using GroupItem::workflowPolicy; // Default: WorkflowPolicy::StopOnError. GroupItem withTimeout(std::chrono::milliseconds timeout, const GroupEndHandler &handler = {}) const { return GroupItem::withTimeout(*this, timeout, handler); } private: template static GroupSetupHandler wrapGroupSetup(SetupHandler &&handler) { static constexpr bool isDynamic = std::is_same_v>>; constexpr bool isVoid = std::is_same_v>>; static_assert(isDynamic || isVoid, "Group setup handler needs to take no arguments and has to return " "void or SetupResult. The passed handler doesn't fulfill these requirements."); return [=] { if constexpr (isDynamic) return std::invoke(handler); std::invoke(handler); return SetupResult::Continue; }; }; }; template static GroupItem onGroupSetup(SetupHandler &&handler) { return Group::onGroupSetup(std::forward(handler)); } TASKING_EXPORT GroupItem onGroupDone(const GroupItem::GroupEndHandler &handler); TASKING_EXPORT GroupItem onGroupError(const GroupItem::GroupEndHandler &handler); TASKING_EXPORT GroupItem parallelLimit(int limit); TASKING_EXPORT GroupItem workflowPolicy(WorkflowPolicy policy); TASKING_EXPORT extern const GroupItem sequential; TASKING_EXPORT extern const GroupItem parallel; TASKING_EXPORT extern const GroupItem stopOnError; TASKING_EXPORT extern const GroupItem continueOnError; TASKING_EXPORT extern const GroupItem stopOnDone; TASKING_EXPORT extern const GroupItem continueOnDone; TASKING_EXPORT extern const GroupItem stopOnFinished; TASKING_EXPORT extern const GroupItem finishAllAndDone; TASKING_EXPORT extern const GroupItem finishAllAndError; class TASKING_EXPORT Storage final : public GroupItem { public: Storage(const TreeStorageBase &storage) : GroupItem(storage) { } }; // Synchronous invocation. Similarly to Group - isn't counted as a task inside taskCount() class TASKING_EXPORT Sync final : public GroupItem { public: template Sync(Function &&function) { addChildren({init(std::forward(function))}); } private: template static GroupItem init(Function &&function) { constexpr bool isInvocable = std::is_invocable_v>; static_assert(isInvocable, "Sync element: The synchronous function can't take any arguments."); constexpr bool isBool = std::is_same_v>>; constexpr bool isVoid = std::is_same_v>>; static_assert(isBool || isVoid, "Sync element: The synchronous function has to return void or bool."); if constexpr (isBool) { return onGroupSetup([function] { return function() ? SetupResult::StopWithDone : SetupResult::StopWithError; }); } return onGroupSetup([function] { function(); return SetupResult::StopWithDone; }); }; }; template > class TaskAdapter : public TaskInterface { protected: TaskAdapter() : m_task(new Task) {} Task *task() { return m_task.get(); } const Task *task() const { return m_task.get(); } private: using TaskType = Task; using DeleterType = Deleter; template friend class CustomTask; std::unique_ptr m_task; }; template class CustomTask final : public GroupItem { public: using Task = typename Adapter::TaskType; using Deleter = typename Adapter::DeleterType; static_assert(std::is_base_of_v, Adapter>, "The Adapter type for the CustomTask needs to be derived from " "TaskAdapter."); using EndHandler = std::function; static Adapter *createAdapter() { return new Adapter; } CustomTask() : GroupItem({&createAdapter}) {} template CustomTask(SetupHandler &&setup, const EndHandler &done = {}, const EndHandler &error = {}) : GroupItem({&createAdapter, wrapSetup(std::forward(setup)), wrapEnd(done), wrapEnd(error)}) {} GroupItem withTimeout(std::chrono::milliseconds timeout, const GroupEndHandler &handler = {}) const { return GroupItem::withTimeout(*this, timeout, handler); } private: template static GroupItem::TaskSetupHandler wrapSetup(SetupHandler &&handler) { static constexpr bool isDynamic = std::is_same_v, typename Adapter::TaskType &>>; constexpr bool isVoid = std::is_same_v, typename Adapter::TaskType &>>; static_assert(isDynamic || isVoid, "Task setup handler needs to take (Task &) as an argument and has to return " "void or SetupResult. The passed handler doesn't fulfill these requirements."); return [=](TaskInterface &taskInterface) { Adapter &adapter = static_cast(taskInterface); if constexpr (isDynamic) return std::invoke(handler, *adapter.task()); std::invoke(handler, *adapter.task()); return SetupResult::Continue; }; }; static TaskEndHandler wrapEnd(const EndHandler &handler) { if (!handler) return {}; return [handler](const TaskInterface &taskInterface) { const Adapter &adapter = static_cast(taskInterface); handler(*adapter.task()); }; }; }; class TaskTreePrivate; class TASKING_EXPORT TaskTree final : public QObject { Q_OBJECT public: TaskTree(); TaskTree(const Group &recipe); ~TaskTree(); void setRecipe(const Group &recipe); void start(); void stop(); bool isRunning() const; // Helper methods. They execute a local event loop with ExcludeUserInputEvents. // The passed future is used for listening to the cancel event. // Don't use it in main thread. To be used in non-main threads or in auto tests. bool runBlocking(); bool runBlocking(const QFuture &future); static bool runBlocking(const Group &recipe, std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); static bool runBlocking(const Group &recipe, const QFuture &future, std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); int taskCount() const; int progressMaximum() const { return taskCount(); } int progressValue() const; // all finished / skipped / stopped tasks, groups itself excluded template void onStorageSetup(const TreeStorage &storage, StorageHandler &&handler) { constexpr bool isInvokable = std::is_invocable_v, StorageStruct &>; static_assert(isInvokable, "Storage setup handler needs to take (Storage &) as an argument. " "The passed handler doesn't fulfill these requirements."); setupStorageHandler(storage, wrapHandler(std::forward(handler)), {}); } template void onStorageDone(const TreeStorage &storage, StorageHandler &&handler) { constexpr bool isInvokable = std::is_invocable_v, const StorageStruct &>; static_assert(isInvokable, "Storage done handler needs to take (const Storage &) as an argument. " "The passed handler doesn't fulfill these requirements."); setupStorageHandler(storage, {}, wrapHandler(std::forward(handler))); } signals: void started(); void done(); void errorOccurred(); void progressValueChanged(int value); // updated whenever task finished / skipped / stopped private: using StorageVoidHandler = std::function; void setupStorageHandler(const TreeStorageBase &storage, StorageVoidHandler setupHandler, StorageVoidHandler doneHandler); template StorageVoidHandler wrapHandler(StorageHandler &&handler) { return [=](void *voidStruct) { StorageStruct *storageStruct = static_cast(voidStruct); std::invoke(handler, *storageStruct); }; } friend class TaskTreePrivate; TaskTreePrivate *d; }; class TASKING_EXPORT TaskTreeTaskAdapter : public TaskAdapter { public: TaskTreeTaskAdapter(); void start() final; }; class TASKING_EXPORT TimeoutTaskAdapter : public TaskAdapter { public: TimeoutTaskAdapter(); ~TimeoutTaskAdapter(); void start() final; private: std::optional m_timerId; }; using TaskTreeTask = CustomTask; using TimeoutTask = CustomTask; } // namespace Tasking