diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-59452-MultipleTasksInSchedulerCommand.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-59452-MultipleTasksInSchedulerCommand.rst new file mode 100644 index 0000000000000000000000000000000000000000..b11e4828f0db5c6a151e6ab9ed1ad4fef3de1e82 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-59452-MultipleTasksInSchedulerCommand.rst @@ -0,0 +1,33 @@ +.. include:: ../../Includes.txt + +===================================================================== +Feature: #87451 - scheduler:run command accepts multiple task options +===================================================================== + +See :issue:`87451` + +Description +=========== + +The `scheduler:run` command now accepts multiple `--task` options. + +The tasks will be executed in the order in which they are given: + +.. code-block:: bash + + ./typo3/sysext/core/bin/typo3 scheduler:run --task 1 --task 2 + + +It is now also possible to pass verbose flags to the command to get more information about what is +going on. + +A single `-v` flag will output errors only. Two `-vv` flags will also output additional information. + +Impact +====== + +The new feature allows the execution of tasks in a given order. + +This can be used to debug side effects between tasks that are executed within the same scheduler run. + +.. index:: CLI, ext:scheduler diff --git a/typo3/sysext/scheduler/Classes/Command/SchedulerCommand.php b/typo3/sysext/scheduler/Classes/Command/SchedulerCommand.php index 3368696e18de04454a83bae3913b6b0a10126836..a741ee2cabcff343ab6f481ff0873ae603ff29a7 100644 --- a/typo3/sysext/scheduler/Classes/Command/SchedulerCommand.php +++ b/typo3/sysext/scheduler/Classes/Command/SchedulerCommand.php @@ -18,6 +18,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; use TYPO3\CMS\Core\Core\Bootstrap; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Scheduler\Scheduler; @@ -38,6 +39,30 @@ class SchedulerCommand extends Command */ protected $scheduler; + /** + * @var SymfonyStyle + */ + protected $io; + + /** + * Array of tasks UIDs that should be executed. Null if task option is not provided. + * + * @var int[]|array|null + */ + protected $overwrittenTaskList; + + /** + * This is true when the tasks should be marked as stopped instead of beeing executed. + * + * @var bool + */ + protected $stopTasks = false; + + /** + * @var bool + */ + protected $forceExecution; + /** * Configure the command by defining the name, options and arguments */ @@ -50,20 +75,20 @@ Call it like this: typo3/sysext/core/bin/typo3 scheduler:run --task=13 -f') ->addOption( 'task', 'i', - InputOption::VALUE_REQUIRED, - 'UID of a specific task' + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + 'UID of a specific task. Can be provided multiple times to execute multiple tasks sequentially.' ) ->addOption( 'force', 'f', InputOption::VALUE_NONE, - 'Force execution of the task which is passed with -i option' + 'Force execution of the task which is passed with --task option' ) ->addOption( 'stop', 's', InputOption::VALUE_NONE, - 'Stop the task which is passed with -i option' + 'Stop the task which is passed with --task option' ); } @@ -76,41 +101,59 @@ Call it like this: typo3/sysext/core/bin/typo3 scheduler:run --task=13 -f') */ public function execute(InputInterface $input, OutputInterface $output) { + $this->io = new SymfonyStyle($input, $output); + // Make sure the _cli_ user is loaded Bootstrap::initializeBackendAuthentication(); $this->scheduler = GeneralUtility::makeInstance(Scheduler::class); - if ((int)$input->getOption('task') > 0) { - $taskUid = (int)$input->getOption('task'); - $stopTask = (bool)($input->hasOption('stop') && $input->getOption('stop')); - $force = (bool)($input->hasOption('force') && $input->getOption('force')); - $task = $this->getTask($taskUid, $stopTask || $force); - - if ($this->scheduler->isValidTaskObject($task)) { - if ($stopTask) { - $this->stopTask($task); - } else { - $this->scheduler->executeTask($task); - } - } - // Record the run in the system registry - $this->scheduler->recordLastRun('cli-by-id'); - } else { - $this->loopTasks(); + $overwrittenTaskList = $input->getOption('task'); + if ($overwrittenTaskList !== []) { + $this->overwrittenTaskList = $overwrittenTaskList; } + + $this->forceExecution = (bool)$input->getOption('force'); + $this->stopTasks = $this->shouldStopTasks((bool)$input->getOption('stop')); + $this->loopTasks(); return 0; } + /** + * Checks if the tasks should be stopped instead of beeing executed. + * + * Stopping is only performed when the --stop option is provided together with the --task option. + * + * @param bool $stopOption + * @return bool + */ + protected function shouldStopTasks(bool $stopOption): bool + { + if (!$stopOption) { + return false; + } + + if ($this->overwrittenTaskList !== []) { + return true; + } + + if ($this->io->isVerbose()) { + $this->io->warning('Stopping tasks is only possible when the --task option is provided.'); + } + return false; + } + /** * Stop task * * @param AbstractTask $task */ - protected function stopTask($task) + protected function stopTask(AbstractTask $task) { - if ($this->scheduler->isValidTaskObject($task)) { - $task->unmarkAllExecutions(); + $task->unmarkAllExecutions(); + + if ($this->io->isVeryVerbose()) { + $this->io->writeln(sprintf('Task #%d was stopped', $task->getTaskUid())); } } @@ -118,18 +161,17 @@ Call it like this: typo3/sysext/core/bin/typo3 scheduler:run --task=13 -f') * Return task a task for a given UID * * @param int $taskUid - * @param bool $force fetch the task regardless if it is queued for execution * @return AbstractTask */ - protected function getTask(int $taskUid, bool $force) + protected function getTask(int $taskUid) { + $force = $this->stopTasks || $this->forceExecution; if ($force) { - $task = $this->scheduler->fetchTask($taskUid); - } else { - $whereClause = 'uid = ' . (int)$taskUid . ' AND nextexecution != 0 AND nextexecution <= ' . $GLOBALS['EXEC_TIME']; - [$task] = $this->scheduler->fetchTasksWithCondition($whereClause); + return $this->scheduler->fetchTask($taskUid); } + $whereClause = 'uid = ' . (int)$taskUid . ' AND nextexecution != 0 AND nextexecution <= ' . $GLOBALS['EXEC_TIME']; + [$task] = $this->scheduler->fetchTasksWithCondition($whereClause); return $task; } @@ -142,22 +184,80 @@ Call it like this: typo3/sysext/core/bin/typo3 scheduler:run --task=13 -f') // Try getting the next task and execute it // If there are no more tasks to execute, an exception is thrown by \TYPO3\CMS\Scheduler\Scheduler::fetchTask() try { - $task = $this->scheduler->fetchTask(); + $task = $this->fetchNextTask(); try { - $this->scheduler->executeTask($task); + $this->executeOrStopTask($task); } catch (\Exception $e) { + if ($this->io->isVerbose()) { + $this->io->warning($e->getMessage()); + } // We ignore any exception that may have been thrown during execution, // as this is a background process. // The exception message has been recorded to the database anyway continue; } } catch (\OutOfBoundsException $e) { - $this->hasTask = false; + if ($this->io->isVeryVerbose()) { + $this->io->writeln($e->getMessage()); + } + $this->hasTask = !empty($this->overwrittenTaskList); } catch (\UnexpectedValueException $e) { + if ($this->io->isVerbose()) { + $this->io->warning($e->getMessage()); + } continue; } } while ($this->hasTask); // Record the run in the system registry $this->scheduler->recordLastRun(); } + + /** + * When the --task option is provided, the next task is fetched from the provided task UIDs. Depending + * on the --force option the task is fechted even if it is not marked for execution. + * + * Without the --task option we ask the scheduler for the next task with pending execution. + * + * @return AbstractTask + * @throws \OutOfBoundsException When there are no more tasks to execute. + * @throws \UnexpectedValueException When no task is found by the provided UID or the task is not marked for execution. + */ + protected function fetchNextTask(): AbstractTask + { + if ($this->overwrittenTaskList === null) { + return $this->scheduler->fetchTask(); + } + + if (count($this->overwrittenTaskList) === 0) { + throw new \OutOfBoundsException('No more tasks to execute', 1547675594); + } + + $taskUid = (int)array_shift($this->overwrittenTaskList); + $task = $this->getTask($taskUid); + if (!$this->scheduler->isValidTaskObject($task)) { + throw new \UnexpectedValueException( + sprintf('The task #%d is not scheduled for execution or does not exist.', $taskUid), + 1547675557 + ); + } + return $task; + } + + /** + * When in stop mode the given task is stopped. Otherwise the task is executed. + * + * @param AbstractTask $task + */ + protected function executeOrStopTask(AbstractTask $task): void + { + if ($this->stopTasks) { + $this->stopTask($task); + return; + } + + $this->scheduler->executeTask($task); + if ($this->io->isVeryVerbose()) { + $this->io->writeln(sprintf('Task #%d was executed', $task->getTaskUid())); + } + } } diff --git a/typo3/sysext/scheduler/Classes/Scheduler.php b/typo3/sysext/scheduler/Classes/Scheduler.php index a788ce190a392bda07a24e785e0290afe66f85a5..6046f73cb6098f2b46887e2619199e3e08399940 100644 --- a/typo3/sysext/scheduler/Classes/Scheduler.php +++ b/typo3/sysext/scheduler/Classes/Scheduler.php @@ -336,7 +336,7 @@ class Scheduler implements SingletonInterface, LoggerAwareInterface if (empty($row)) { if (empty($uid)) { // No uid was passed and no overdue task was found - throw new \OutOfBoundsException('No tasks available for execution', 1247827244); + throw new \OutOfBoundsException('No (more) tasks available for execution', 1247827244); } // Although a uid was passed, no task with given was found throw new \OutOfBoundsException('No task with id ' . $uid . ' found', 1422044826);