TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,682 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Scheduler\Domain\Repository;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\CommandLineUserAuthentication;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Core\Bootstrap;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
|
||||
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\Field\StaticSelectFieldType;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Scheduler\Exception\InvalidTaskException;
|
||||
use TYPO3\CMS\Scheduler\ProgressProviderInterface;
|
||||
use TYPO3\CMS\Scheduler\Service\TaskService;
|
||||
use TYPO3\CMS\Scheduler\Task\AbstractTask;
|
||||
use TYPO3\CMS\Scheduler\Task\TaskSerializer;
|
||||
use TYPO3\CMS\Scheduler\Task\TaskStatus;
|
||||
use TYPO3\CMS\Scheduler\Validation\Validator\TaskValidator;
|
||||
|
||||
/**
|
||||
* Repository class to fetch tasks available in the systems ready to be executed
|
||||
*
|
||||
* @internal not part of public TYPO3 Core API
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class SchedulerTaskRepository
|
||||
{
|
||||
protected const TABLE_NAME = 'tx_scheduler_task';
|
||||
|
||||
public function __construct(
|
||||
protected TaskSerializer $taskSerializer,
|
||||
protected TaskService $taskService,
|
||||
protected TcaSchemaFactory $tcaSchemaFactory,
|
||||
protected Context $context,
|
||||
protected ConnectionPool $connectionPool,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Adds a task to the pool.
|
||||
*
|
||||
* @param AbstractTask $task The object representing the task to add
|
||||
* @return bool TRUE if the task was successfully added, FALSE otherwise
|
||||
*/
|
||||
public function add(AbstractTask $task): bool
|
||||
{
|
||||
$taskUid = $task->getTaskUid();
|
||||
if (!empty($taskUid)) {
|
||||
return false;
|
||||
}
|
||||
$fields = $this->taskService->getFieldsForRecord($task);
|
||||
$fields['pid'] = 0;
|
||||
$newId = uniqid('NEW');
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dataHandler->start([
|
||||
self::TABLE_NAME => [
|
||||
$newId => $fields,
|
||||
],
|
||||
], []);
|
||||
$dataHandler->process_datamap();
|
||||
$taskUid = (int)$dataHandler->substNEWwithIDs[$newId];
|
||||
if ($taskUid) {
|
||||
$task->setTaskUid($taskUid);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a task completely from the system.
|
||||
*
|
||||
* @param int|AbstractTask $task The object representing the task to delete
|
||||
* @return bool TRUE if the task was successfully deleted, FALSE otherwise
|
||||
*/
|
||||
public function remove(int|AbstractTask $task): bool
|
||||
{
|
||||
$taskUid = is_int($task) ? $task : $task->getTaskUid();
|
||||
if (empty($taskUid)) {
|
||||
return false;
|
||||
}
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dataHandler->start([], [
|
||||
self::TABLE_NAME => [
|
||||
$taskUid => [
|
||||
'delete' => 1,
|
||||
],
|
||||
],
|
||||
]);
|
||||
$dataHandler->process_cmdmap();
|
||||
return $dataHandler->errorLog === [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a task in the pool.
|
||||
*/
|
||||
public function update(AbstractTask $task, ?array $fields = null): bool
|
||||
{
|
||||
$taskUid = $task->getTaskUid();
|
||||
if (empty($taskUid)) {
|
||||
return false;
|
||||
}
|
||||
$fields = $fields ?? $this->taskService->getFieldsForRecord($task);
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dataHandler->start([
|
||||
self::TABLE_NAME => [
|
||||
$taskUid => $fields,
|
||||
],
|
||||
], []);
|
||||
$dataHandler->process_datamap();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a task in the pool but only the execution information.
|
||||
*/
|
||||
public function updateExecution(AbstractTask $task, bool $forceDisablingTask = false): void
|
||||
{
|
||||
$taskUid = $task->getTaskUid();
|
||||
if (empty($taskUid)) {
|
||||
return;
|
||||
}
|
||||
$fields = $this->taskService->getFieldsForRecord($task);
|
||||
$fields = [
|
||||
'nextexecution' => $fields['nextexecution'],
|
||||
'disable' => $forceDisablingTask ? true : $fields['disable'],
|
||||
'execution_details' => $fields['execution_details'],
|
||||
];
|
||||
$backendUser = $GLOBALS['BE_USER'] ?? null;
|
||||
if ($backendUser === null && Environment::isCli()) {
|
||||
/** @var CommandLineUserAuthentication $backendUser */
|
||||
$backendUser = Bootstrap::initializeBackendUser(CommandLineUserAuthentication::class);
|
||||
$backendUser->authenticate();
|
||||
}
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
// We don't want every execution information logged
|
||||
$dataHandler->enableLogging = false;
|
||||
$dataHandler->start([
|
||||
self::TABLE_NAME => [
|
||||
$taskUid => $fields,
|
||||
],
|
||||
], [], $backendUser);
|
||||
$dataHandler->process_datamap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a task object from the db with the given $uid. The object representing
|
||||
* the next due task is returned.
|
||||
* If there are no tasks due, the method throws an exception.
|
||||
*
|
||||
* @param int $uid Primary key of a task
|
||||
* @throws \OutOfBoundsException
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
public function findByUid(int $uid): AbstractTask
|
||||
{
|
||||
$row = BackendUtility::getRecord(self::TABLE_NAME, $uid);
|
||||
if (empty($row)) {
|
||||
// Although an uid was passed, no task with given was found
|
||||
throw new \OutOfBoundsException('No task with id ' . $uid . ' found', 1422044826);
|
||||
}
|
||||
|
||||
return $this->createValidTaskObjectOrDisableTask($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the DB record for a given task UID.
|
||||
*
|
||||
* @param int $uid Primary key of the task to get
|
||||
* @return array|null Database record for the task
|
||||
* @see findByUid()
|
||||
*/
|
||||
public function findRecordByUid(int $uid): ?array
|
||||
{
|
||||
$row = BackendUtility::getRecord(self::TABLE_NAME, $uid);
|
||||
if (empty($row)) {
|
||||
return null;
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and unserialize a task object from the db. Returns the object representing the
|
||||
* next due task is returned. If there are no due tasks, the method throws an exception.
|
||||
*
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
public function findNextExecutableTask(): ?AbstractTask
|
||||
{
|
||||
// If no uid is given, take any non-disabled task that has a next execution time in the past
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
$queryBuilder->select(
|
||||
't.*'
|
||||
)
|
||||
->from(self::TABLE_NAME, 't')
|
||||
->setMaxResults(1);
|
||||
// Define where clause
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$queryBuilder->leftJoin(
|
||||
't',
|
||||
'tx_scheduler_task_group',
|
||||
'g',
|
||||
$queryBuilder->expr()->eq('g.uid', $queryBuilder->expr()->castInt($queryBuilder->quoteIdentifier('t.task_group')))
|
||||
);
|
||||
$queryBuilder->where(
|
||||
$queryBuilder->expr()->eq('t.disable', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
|
||||
$queryBuilder->expr()->neq(
|
||||
't.nextexecution',
|
||||
$queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->lte(
|
||||
't.nextexecution',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], Connection::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->or(
|
||||
$queryBuilder->expr()->eq('g.hidden', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
|
||||
$queryBuilder->expr()->isNull('g.hidden')
|
||||
),
|
||||
$queryBuilder->expr()->eq('t.deleted', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT))
|
||||
);
|
||||
$queryBuilder->orderBy('t.priority', 'DESC')->addOrderBy('t.nextexecution', 'ASC');
|
||||
|
||||
$row = $queryBuilder->executeQuery()->fetchAssociative();
|
||||
if (empty($row)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->createValidTaskObjectOrDisableTask($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo This will get split up into errored classes
|
||||
*/
|
||||
public function getGroupedTasks(): array
|
||||
{
|
||||
// Get all registered tasks
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$result = $queryBuilder->select('t.*')
|
||||
->addSelect(
|
||||
'g.groupName AS taskGroupName',
|
||||
'g.description AS taskGroupDescription',
|
||||
'g.uid AS taskGroupId',
|
||||
'g.color AS taskGroupColor',
|
||||
'g.deleted AS isTaskGroupDeleted',
|
||||
'g.hidden AS isTaskGroupHidden',
|
||||
)
|
||||
->from(self::TABLE_NAME, 't')
|
||||
->leftJoin(
|
||||
't',
|
||||
'tx_scheduler_task_group',
|
||||
'g',
|
||||
$queryBuilder->expr()->eq('g.uid', $queryBuilder->expr()->castInt($queryBuilder->quoteIdentifier('t.task_group')))
|
||||
)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('t.deleted', 0)
|
||||
)
|
||||
->orderBy('g.sorting')
|
||||
->executeQuery();
|
||||
|
||||
$taskGroupsWithTasks = [];
|
||||
$errorClasses = [];
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
$taskData = [
|
||||
'uid' => (int)$row['uid'],
|
||||
'lastExecutionTime' => (int)$row['lastexecution_time'],
|
||||
'lastExecutionContext' => $row['lastexecution_context'],
|
||||
'errorMessage' => '',
|
||||
'description' => $row['description'],
|
||||
];
|
||||
|
||||
try {
|
||||
$taskObject = $this->taskSerializer->deserialize($row);
|
||||
} catch (InvalidTaskException $e) {
|
||||
$taskData['errorMessage'] = $e->getMessage();
|
||||
$taskData['taskType'] = $row['tasktype'] ?: $this->taskSerializer->extractClassName($row['serialized_task_object']);
|
||||
$errorClasses[] = $taskData;
|
||||
continue;
|
||||
}
|
||||
|
||||
$taskData['taskType'] = $taskObject->getTaskType();
|
||||
|
||||
if (!$this->isValidTaskObject($taskObject)) {
|
||||
$taskData['errorMessage'] = 'The task "' . $taskObject->getTaskType() . ' is not a valid task';
|
||||
$errorClasses[] = $taskData;
|
||||
continue;
|
||||
}
|
||||
|
||||
$taskInformation = $this->taskService->getTaskDetailsFromTask($taskObject);
|
||||
if ($taskInformation === null) {
|
||||
$taskData['errorMessage'] = 'The task ' . $taskObject->getTaskType() . ' is not a registered task';
|
||||
$errorClasses[] = $taskData;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($taskObject instanceof ProgressProviderInterface) {
|
||||
$taskData['progress'] = round((float)$taskObject->getProgress(), 2);
|
||||
}
|
||||
$taskData['fullTitle'] = $taskInformation['fullTitle'];
|
||||
$taskData['additionalInformation'] = $taskObject->getAdditionalInformation();
|
||||
$taskData['disabled'] = (bool)$row['disable'];
|
||||
$taskData['isRunning'] = !empty($row['serialized_executions']);
|
||||
$taskData['nextExecution'] = (int)$row['nextexecution'];
|
||||
$taskData['runningType'] = 'single';
|
||||
$taskData['frequency'] = '';
|
||||
if ($taskObject->getExecution()->isRecurring()) {
|
||||
$taskData['runningType'] = 'recurring';
|
||||
$taskData['frequency'] = $taskObject->getExecution()->getCronCmd() ?: $taskObject->getExecution()->getInterval();
|
||||
}
|
||||
$taskData['multiple'] = (bool)$taskObject->getExecution()->isParallelExecutionAllowed();
|
||||
$taskData['priority'] = (int)$row['priority'];
|
||||
$taskData['priorityLabel'] = $this->resolvePriorityLabel((int)$row['priority']);
|
||||
$taskData['lastExecutionFailure'] = false;
|
||||
if (!empty($row['lastexecution_failure'])) {
|
||||
$taskData['lastExecutionFailure'] = true;
|
||||
// only scalars are serialized in \TYPO3\CMS\Scheduler\Scheduler::executeTask
|
||||
$exceptionArray = @unserialize($row['lastexecution_failure'], ['allowed_classes' => false]);
|
||||
$taskData['lastExecutionFailureCode'] = '';
|
||||
$taskData['lastExecutionFailureMessage'] = '';
|
||||
if (is_array($exceptionArray)) {
|
||||
$taskData['lastExecutionFailureCode'] = $exceptionArray['code'];
|
||||
$taskData['lastExecutionFailureMessage'] = $exceptionArray['message'];
|
||||
}
|
||||
}
|
||||
|
||||
$taskData['statuses'] = $this->buildTaskStatuses($taskData, (bool)$row['isTaskGroupHidden']);
|
||||
|
||||
// If a group is deleted or no group is set it needs to go into "not assigned groups"
|
||||
$groupIndex = $row['isTaskGroupDeleted'] === 1 || $row['isTaskGroupDeleted'] === null ? 0 : (int)$row['task_group'];
|
||||
if (!isset($taskGroupsWithTasks[$groupIndex])) {
|
||||
$taskGroupsWithTasks[$groupIndex] = [
|
||||
'uid' => $row['taskGroupId'],
|
||||
'groupName' => $row['taskGroupName'],
|
||||
'description' => $row['taskGroupDescription'],
|
||||
'color' => $row['taskGroupColor'],
|
||||
'hidden' => $row['isTaskGroupHidden'],
|
||||
'tasks' => [],
|
||||
];
|
||||
}
|
||||
$taskGroupsWithTasks[$groupIndex]['tasks'][] = $taskData;
|
||||
}
|
||||
|
||||
return [
|
||||
'taskGroupsWithTasks' => $taskGroupsWithTasks,
|
||||
'errorClasses' => $errorClasses,
|
||||
];
|
||||
}
|
||||
|
||||
protected function createValidTaskObjectOrDisableTask(array $row): AbstractTask
|
||||
{
|
||||
$isInvalidTask = false;
|
||||
$task = null;
|
||||
try {
|
||||
$task = $this->taskSerializer->deserialize($row);
|
||||
} catch (InvalidTaskException) {
|
||||
$isInvalidTask = true;
|
||||
}
|
||||
if ($isInvalidTask || !$this->isValidTaskObject($task)) {
|
||||
$fieldName = $this->tcaSchemaFactory
|
||||
->get(self::TABLE_NAME)
|
||||
->getCapability(TcaSchemaCapability::RestrictionDisabledField)
|
||||
->getFieldName();
|
||||
if ((bool)$row[$fieldName] !== true) {
|
||||
// Forcibly set the disabled flag to 1 in the database (if not already set), so that the
|
||||
// task does not come up again and again for execution. Execute a simple update statement
|
||||
// to avoid triggering any DH hook again, which would lead to an infinity loop.
|
||||
$this->connectionPool
|
||||
->getConnectionForTable(self::TABLE_NAME)
|
||||
->update(self::TABLE_NAME, [$fieldName => 1], ['uid' => (int)$row['uid']]);
|
||||
}
|
||||
// Throw an exception to raise the problem
|
||||
// @todo: This should most likely be changed to a specific exception.
|
||||
throw new \UnexpectedValueException('Could not unserialize task', 1255083671);
|
||||
}
|
||||
|
||||
// The task is valid, return it
|
||||
if ($task->getTaskGroup() === null) {
|
||||
// Fix invalid task_group=NULL settings in order to avoid exceptions when saving on PostgreSQL
|
||||
$task->setTaskGroup(0);
|
||||
}
|
||||
return $task;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and unserialize task objects selected with some (SQL) condition
|
||||
*/
|
||||
public function findNextExecutableTaskForUid(int $uid): ?AbstractTask
|
||||
{
|
||||
$queryBuilder = $this->connectionPool
|
||||
->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
||||
->add(GeneralUtility::makeInstance(HiddenRestriction::class));
|
||||
|
||||
$queryBuilder
|
||||
->select('*')
|
||||
->from(self::TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)),
|
||||
$queryBuilder->expr()->neq('nextexecution', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
|
||||
$queryBuilder->expr()->lte('nextexecution', $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], Connection::PARAM_INT)),
|
||||
);
|
||||
|
||||
$result = $queryBuilder->executeQuery();
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
try {
|
||||
$task = $this->taskSerializer->deserialize($row);
|
||||
} catch (InvalidTaskException) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add the task to the list only if it is valid
|
||||
if ($this->isValidTaskObject($task)) {
|
||||
return $task;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isTaskMarkedAsRunning(AbstractTask $task): bool
|
||||
{
|
||||
$row = BackendUtility::getRecord(self::TABLE_NAME, $task->getTaskUid());
|
||||
return !empty($row['serialized_executions'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method adds current execution to the execution list.
|
||||
* It also logs the execution time and mode
|
||||
*
|
||||
* The execution id is guaranteed to start from zero if the task has no
|
||||
* currently running execution at the time of id allocation.
|
||||
*
|
||||
* @return int Execution id
|
||||
*/
|
||||
public function addExecutionToTask(AbstractTask $task): int
|
||||
{
|
||||
while (true) {
|
||||
$row = BackendUtility::getRecord(self::TABLE_NAME, $task->getTaskUid());
|
||||
|
||||
if ($row === null) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Given task must have a persistence record associated with it',
|
||||
1741257045
|
||||
);
|
||||
}
|
||||
|
||||
$previousExecutions = isset($row['serialized_executions'])
|
||||
? (string)$row['serialized_executions']
|
||||
: null;
|
||||
|
||||
$runningExecutions = $previousExecutions !== null
|
||||
&& $previousExecutions !== ''
|
||||
// serialized in \TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository::addExecutionToTask as `array<int, int>`
|
||||
? unserialize($previousExecutions, ['allowed_classes' => false])
|
||||
: [];
|
||||
|
||||
// Count the number of existing executions and use that number as a key
|
||||
// (we need to know that number, because it is returned at the end of the method)
|
||||
$numExecutions = count($runningExecutions);
|
||||
$runningExecutions[$numExecutions] = time();
|
||||
$updateCount = $this->connectionPool
|
||||
->getConnectionForTable(self::TABLE_NAME)
|
||||
->update(
|
||||
self::TABLE_NAME,
|
||||
[
|
||||
'serialized_executions' => serialize($runningExecutions),
|
||||
'lastexecution_time' => time(),
|
||||
// Define the context in which the script is running
|
||||
'lastexecution_context' => Environment::isCli() ? 'CLI' : 'BE',
|
||||
],
|
||||
[
|
||||
'uid' => $task->getTaskUid(),
|
||||
'serialized_executions' => $previousExecutions,
|
||||
],
|
||||
[
|
||||
'serialized_executions' => Connection::PARAM_LOB,
|
||||
]
|
||||
);
|
||||
|
||||
if ($updateCount === 1) {
|
||||
return $numExecutions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a given execution from the list
|
||||
*
|
||||
* @param int $executionID Id of the execution to remove.
|
||||
* @param string|array|null $failureReason Details of an exception to signal a failed execution.
|
||||
*/
|
||||
public function removeExecutionOfTask(AbstractTask $task, int $executionID, array|string|null $failureReason = null): void
|
||||
{
|
||||
while ($row = BackendUtility::getRecord(self::TABLE_NAME, $task->getTaskUid())) {
|
||||
$previousExecutions = (string)($row['serialized_executions'] ?? '');
|
||||
if ($previousExecutions === '') {
|
||||
break;
|
||||
}
|
||||
// serialized in \TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository::addExecutionToTask as `array<int, int>`
|
||||
$runningExecutions = unserialize($previousExecutions, ['allowed_classes' => false]);
|
||||
// Remove the selected execution
|
||||
unset($runningExecutions[$executionID]);
|
||||
if (!empty($runningExecutions)) {
|
||||
// Re-serialize the updated executions list (if necessary)
|
||||
$runningExecutionsSerialized = serialize($runningExecutions);
|
||||
} else {
|
||||
$runningExecutionsSerialized = '';
|
||||
}
|
||||
if (is_array($failureReason)) {
|
||||
$failureReason = json_encode($failureReason);
|
||||
}
|
||||
// Save the updated executions list
|
||||
$fieldUpdates = [
|
||||
'serialized_executions' => $runningExecutionsSerialized,
|
||||
];
|
||||
if ($failureReason !== null) {
|
||||
$fieldUpdates['lastexecution_failure'] = (string)$failureReason;
|
||||
}
|
||||
$updateCount = $this->connectionPool
|
||||
->getConnectionForTable(self::TABLE_NAME)
|
||||
->update(
|
||||
self::TABLE_NAME,
|
||||
$fieldUpdates,
|
||||
[
|
||||
'uid' => $task->getTaskUid(),
|
||||
'serialized_executions' => $previousExecutions,
|
||||
],
|
||||
[
|
||||
'serialized_executions' => Connection::PARAM_LOB,
|
||||
]
|
||||
);
|
||||
if ($updateCount === 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all marked executions
|
||||
*
|
||||
* @return bool TRUE if the clearing succeeded, FALSE otherwise
|
||||
*/
|
||||
public function removeAllRegisteredExecutionsForTask(AbstractTask $task): bool
|
||||
{
|
||||
// Set the serialized executions field to empty
|
||||
$result = $this->connectionPool
|
||||
->getConnectionForTable(self::TABLE_NAME)
|
||||
->update(
|
||||
self::TABLE_NAME,
|
||||
['serialized_executions' => ''],
|
||||
['uid' => $task->getTaskUid()],
|
||||
['serialized_executions' => Connection::PARAM_LOB]
|
||||
);
|
||||
return (bool)$result;
|
||||
}
|
||||
|
||||
/**
|
||||
* See if there are any tasks configured at all.
|
||||
*/
|
||||
public function hasTasks(): bool
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
$queryBuilder
|
||||
->count('*')
|
||||
->from(self::TABLE_NAME);
|
||||
return $queryBuilder->executeQuery()->fetchOne() > 0;
|
||||
}
|
||||
|
||||
protected function isValidTaskObject($task): bool
|
||||
{
|
||||
return (new TaskValidator())->isValid($task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the status flags of a single task into an ordered list, shared as a single
|
||||
* source of truth by the backend module listing and the "scheduler:list" CLI command.
|
||||
*
|
||||
* @return list<TaskStatus>
|
||||
*/
|
||||
private function buildTaskStatuses(array $task, bool $groupHidden): array
|
||||
{
|
||||
$now = $this->context->getAspect('date')->get('timestamp');
|
||||
$statuses = [];
|
||||
|
||||
if ($task['isRunning']) {
|
||||
$statuses[] = new TaskStatus(
|
||||
type: 'running',
|
||||
severity: ContextualFeedbackSeverity::INFO,
|
||||
state: 'running',
|
||||
label: 'scheduler.messages:status.running',
|
||||
);
|
||||
}
|
||||
if ($task['nextExecution'] && $task['nextExecution'] < $now && !$groupHidden && !$task['disabled']) {
|
||||
$statuses[] = new TaskStatus(
|
||||
type: 'late',
|
||||
severity: ContextualFeedbackSeverity::WARNING,
|
||||
state: 'warning',
|
||||
label: 'scheduler.messages:status.late',
|
||||
);
|
||||
}
|
||||
if ($task['disabled'] && !$task['isRunning']) {
|
||||
$statuses[] = new TaskStatus(
|
||||
type: 'disabled',
|
||||
severity: ContextualFeedbackSeverity::NOTICE,
|
||||
state: 'disabled',
|
||||
label: 'scheduler.messages:status.disabled',
|
||||
);
|
||||
}
|
||||
if ($groupHidden && !$task['isRunning']) {
|
||||
$statuses[] = new TaskStatus(
|
||||
type: 'disabledByGroup',
|
||||
severity: ContextualFeedbackSeverity::NOTICE,
|
||||
state: 'disabled',
|
||||
label: 'scheduler.messages:status.disabledByGroup',
|
||||
);
|
||||
}
|
||||
if ($task['lastExecutionFailure'] ?? false) {
|
||||
if (($task['lastExecutionFailureMessage'] ?? '') !== '') {
|
||||
$statuses[] = new TaskStatus(
|
||||
type: 'failure',
|
||||
severity: ContextualFeedbackSeverity::ERROR,
|
||||
state: 'danger',
|
||||
label: 'scheduler.messages:status.failure',
|
||||
message: 'scheduler.messages:msg.executionFailureReport',
|
||||
messageArguments: [
|
||||
$task['lastExecutionFailureCode'],
|
||||
$task['lastExecutionFailureMessage'],
|
||||
],
|
||||
);
|
||||
} else {
|
||||
$statuses[] = new TaskStatus(
|
||||
type: 'failure',
|
||||
severity: ContextualFeedbackSeverity::ERROR,
|
||||
state: 'default',
|
||||
label: 'scheduler.messages:status.failure',
|
||||
message: 'scheduler.messages:msg.executionFailureDefault',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
private function resolvePriorityLabel(int $priority): string
|
||||
{
|
||||
$field = $this->tcaSchemaFactory->get(self::TABLE_NAME)->getField('priority');
|
||||
if ($field instanceof StaticSelectFieldType) {
|
||||
foreach ($field->getItems() as $item) {
|
||||
if ((int)$item->getValue() === $priority) {
|
||||
return $item->getLabel();
|
||||
}
|
||||
}
|
||||
}
|
||||
return (string)$priority;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user