## Task Manager[br] ## ## A simple [Task] scheduler. This can take a list of functions and process ## them, either in the background or asynchronously. ## Multiple instances of these can be set at different scopes. [br] ## ## Note that due to the nature of processing tasks, if we are to destroy ## referenced objects within task closures, we should await [method TaskManager await_shutdown] ## before unloading said references (nodes, internal state objects, etc) ## otherwise a task may continue to process and use-after-free. class_name TaskManager extends RefCounted ## Called when a task starts signal task_started(task:Task) ## Called when a task finishes. signal task_complete(task:Task) ## Nice name for distinguishing task processors. var name:String = "Untitled Task Scheduler" ## Array of tasks that are either processing or paused. ## Halted tasks may get GC'd out at any point. var tasks:Array[Task] = [] ## Internal flag. If true, all tasks are to stop when possible. ## Use [method TaskManager.halt_all] if you want this behavior. var _halt := false ## Initialize. Give a nice name. func _init(_name:String)->void: name = _name ## Execute a list of commands. Returns the wrapped up [Task]. Try not to mutate ## the task or you will be killed by a clown under your bed in seven days.[br] ## [param task_name] Identifier for task.[br] ## [param commands] List of prepared functions.[br] ## [param exit_cond] Exit predicate, which can interrupt a process early.[br] ## [param block] If true, will await until the task is complete.[br] ## Otherwise, it'll just start running the task without waiting to return. [br] ## Returns the generated task instance should the origin want to observe it. [br] ## [codeblock] ## var task_manager := TaskManager.new("EXAMPLE") ## # Better if you have coroutines that take time to call. ie awaiting a timeout. ## var commands:Array[Callable] = [print("1")] ## var active_task := task_manager.execute_commands("prints", commands, false) ## print(task_manager.dump_text) ## await active_task.on_finished ## print("Done") ## [/codeblock] func execute_commands( task_name:String, commands:Array[Callable], exit_cond:Callable = Callable(), block:bool = true )->Task: var new_task := Task.new(task_name, commands, [p_halt_check.bind(exit_cond)]) tasks.append(new_task) new_task.on_finished.connect(remove_task, CONNECT_ONE_SHOT) new_task.execute() if block: await new_task.finished return new_task ## Find a given task in this task reference. ## [param task] Target task. func remove_task(task:Task)->void: var task_index := tasks.find(task) if task_index == -1: return tasks.remove_at(task_index) ## Flag all tasks to be stopped as soon as possible. func halt_all()->void: _halt = true ## Pause or unpause all tasks with [param to_pause]. func set_pause_on_all(to_pause:bool)->void: Task.set_pause_many(tasks, to_pause) ## If we want this task manager to tick itself, throw it onto a separate channel. ## This lets us be lazy and probably regret it later.[br] ## [param tick_rate] is how many seconds to wait until ticking again. ## We do GC-ing and such for finished tasks, which can be expensive every frame. func self_process(tick_rate:float = 0.5)->void: while not _halt: tick_status_check() await Utils.COMMANDS.wait_seconds(tick_rate) ## Effectively garbage-collect finished tasks. func tick_status_check()->void: #var _is_ongoing := func _is_ongoing(task:Task)->bool: return not task.finished var _is_finished := func _is_finished(task:Task)->bool: return task.finished for task:Task in tasks.map(_is_finished): remove_task(task) ## Sends a halt signal to all processes and awaits until all tasks are safely stopped. [br] ## Await this call to safely ensure you won't have stray processes ticking ## and possibly calling use-after-free references. func await_shutdown()->void: halt_all() # delay until tasks finish and remove themselves. while not tasks.is_empty(): await Engine.get_main_loop().root.get_tree().process_frame ## Get a string of this scheduler and all the tasks currently in process. func dump_text()->String: var scheduler_text := "[%s]" var reduce := func(accum:String, current:String)->String: return accum + current + "\n" var active_tasks := Task.dump_commands(tasks) var task_megastring:String = active_tasks.reduce(reduce, "") return scheduler_text + "\n" + task_megastring ## Recursive predicate for halt checking.[br] ## [param p_halt] if true, the task should stop.[br] ## If no [param p_halt] is set, we only will halt if the scheduler is told to halt. func p_halt_check(p_halt:=Callable())->bool: if not p_halt.is_valid(): return _halt return _halt or p_halt.call()