‪TYPO3CMS  10.4
Scheduler.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
16 namespace ‪TYPO3\CMS\Scheduler;
17 
18 use Doctrine\DBAL\DBALException;
19 use Psr\Log\LoggerAwareInterface;
20 use Psr\Log\LoggerAwareTrait;
21 use Psr\Log\LoggerInterface;
31 
35 class ‪Scheduler implements ‪SingletonInterface, LoggerAwareInterface
36 {
37  use LoggerAwareTrait;
38 
42  public ‪$extConf = [];
43 
47  public function ‪__construct()
48  {
49  // Get configuration from the extension manager
50  $this->extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('scheduler');
51  if (empty($this->extConf['maxLifetime'])) {
52  $this->extConf['maxLifetime'] = 1440;
53  }
54  // Clean up the serialized execution arrays
55  $this->‪cleanExecutionArrays();
56  }
57 
64  public function ‪addTask(‪AbstractTask $task)
65  {
66  $taskUid = $task->‪getTaskUid();
67  if (empty($taskUid)) {
68  ‪$fields = [
69  'crdate' => ‪$GLOBALS['EXEC_TIME'],
70  'disable' => (int)$task->‪isDisabled(),
71  'description' => $task->‪getDescription(),
72  'task_group' => $task->‪getTaskGroup(),
73  'serialized_task_object' => 'RESERVED'
74  ];
75  $connection = GeneralUtility::makeInstance(ConnectionPool::class)
76  ->getConnectionForTable('tx_scheduler_task');
77  $result = $connection->insert(
78  'tx_scheduler_task',
79  ‪$fields,
80  ['serialized_task_object' => ‪Connection::PARAM_LOB]
81  );
82 
83  if ($result) {
84  $task->‪setTaskUid($connection->lastInsertId('tx_scheduler_task'));
85  $task->‪save();
86  $result = true;
87  } else {
88  $result = false;
89  }
90  } else {
91  $result = false;
92  }
93  return $result;
94  }
95 
100  protected function ‪cleanExecutionArrays()
101  {
102  $tstamp = ‪$GLOBALS['EXEC_TIME'];
103  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
104  $queryBuilder = $connectionPool->getQueryBuilderForTable('tx_scheduler_task');
105 
106  // Select all tasks with executions
107  // NOTE: this cleanup is done for disabled tasks too,
108  // to avoid leaving old executions lying around
109  $result = $queryBuilder->select('uid', 'serialized_executions', 'serialized_task_object')
110  ->from('tx_scheduler_task')
111  ->where(
112  $queryBuilder->expr()->neq(
113  'serialized_executions',
114  $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
115  ),
116  $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
117  )
118  ->execute();
119  $maxDuration = $this->extConf['maxLifetime'] * 60;
120  while ($row = $result->fetch()) {
121  $executions = [];
122  if ($serialized_executions = unserialize($row['serialized_executions'])) {
123  foreach ($serialized_executions as $task) {
124  if ($tstamp - $task < $maxDuration) {
125  $executions[] = $task;
126  } else {
127  $task = unserialize($row['serialized_task_object']);
128  $this->‪log('Removing logged execution, assuming that the process is dead. Execution of \'' . get_class($task) . '\' (UID: ' . $row['uid'] . ') was started at ' . date('Y-m-d H:i:s', $task->getExecutionTime()));
129  }
130  }
131  }
132  $executionCount = count($executions);
133  if (!is_array($serialized_executions) || count($serialized_executions) !== $executionCount) {
134  if ($executionCount === 0) {
135  $value = '';
136  } else {
137  $value = serialize($executions);
138  }
139  $connectionPool->getConnectionForTable('tx_scheduler_task')->update(
140  'tx_scheduler_task',
141  ['serialized_executions' => $value],
142  ['uid' => (int)$row['uid']],
143  ['serialized_executions' => Connection::PARAM_LOB]
144  );
145  }
146  }
147  }
148 
157  public function executeTask(AbstractTask $task)
158  {
159  $task->setRunOnNextCronJob(false);
160  // Trigger the saving of the task, as this will calculate its next execution time
161  // This should be calculated all the time, even if the execution is skipped
162  // (in case it is skipped, this pushes back execution to the next possible date)
163  $task->save();
164  // Set a scheduler object for the task again,
165  // as it was removed during the save operation
166  $task->setScheduler();
167  $result = true;
168  // Task is already running and multiple executions are not allowed
169  if (!$task->areMultipleExecutionsAllowed() && $task->isExecutionRunning()) {
170  // Log multiple execution error
171  $logMessage = 'Task is already running and multiple executions are not allowed, skipping! Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
172  $this->logger->info($logMessage);
173  $result = false;
174  } else {
175  // Log scheduler invocation
176  $this->logger->info('Start execution. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid());
177  // Register execution
178  $executionID = $task->markExecution();
179  $failure = null;
180  try {
181  // Execute task
182  $successfullyExecuted = $task->execute();
183  if (!$successfullyExecuted) {
184  throw new FailedExecutionException('Task failed to execute successfully. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid(), 1250596541);
185  }
186  } catch (\Throwable $e) {
187  // Store exception, so that it can be saved to database
188  $failure = $e;
189  }
190  // Un-register execution
191  $task->unmarkExecution($executionID, $failure);
192  // Log completion of execution
193  $this->logger->info('Task executed. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid());
194  // Now that the result of the task execution has been handled,
195  // throw the exception again, if any
196  if ($failure instanceof \Throwable) {
197  throw $failure;
198  }
199  }
200  return $result;
201  }
202 
208  public function recordLastRun($type = 'cron')
209  {
210  // Validate input value
211  if ($type !== 'manual' && $type !== 'cli-by-id') {
212  $type = 'cron';
213  }
215  $registry = GeneralUtility::makeInstance(Registry::class);
216  $runInformation = ['start' => $GLOBALS['EXEC_TIME'], 'end' => time(), 'type' => $type];
217  $registry->set('tx_scheduler', 'lastRun', $runInformation);
218  }
219 
228  public function removeTask(AbstractTask $task)
229  {
230  $taskUid = $task->getTaskUid();
231  if (!empty($taskUid)) {
232  $result = GeneralUtility::makeInstance(ConnectionPool::class)
233  ->getConnectionForTable('tx_scheduler_task')
234  ->update('tx_scheduler_task', ['deleted' => 1], ['uid' => $taskUid]);
235  } else {
236  $result = false;
237  }
238  return $result;
239  }
240 
247  public function saveTask(AbstractTask $task)
248  {
249  $result = true;
250  $taskUid = $task->getTaskUid();
251  if (!empty($taskUid)) {
252  try {
253  if ($task->getRunOnNextCronJob()) {
254  $executionTime = time();
255  } else {
256  $executionTime = $task->getNextDueExecution();
257  }
258  $task->setExecutionTime($executionTime);
259  } catch (\Exception $e) {
260  $task->setDisabled(true);
261  $executionTime = 0;
262  }
263  $task->unsetScheduler();
264  $fields = [
265  'nextexecution' => $executionTime,
266  'disable' => (int)$task->isDisabled(),
267  'description' => $task->getDescription(),
268  'task_group' => $task->getTaskGroup(),
269  'serialized_task_object' => serialize($task)
270  ];
271  try {
272  GeneralUtility::makeInstance(ConnectionPool::class)
273  ->getConnectionForTable('tx_scheduler_task')
274  ->update(
275  'tx_scheduler_task',
276  $fields,
277  ['uid' => $taskUid],
278  ['serialized_task_object' => Connection::PARAM_LOB]
279  );
280  } catch (DBALException $e) {
281  $result = false;
282  }
283  } else {
284  $result = false;
285  }
286  return $result;
287  }
288 
299  public function fetchTask($uid = 0)
300  {
301  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
302  $queryBuilder = $connectionPool->getQueryBuilderForTable('tx_scheduler_task');
303 
304  $queryBuilder->select('t.uid', 't.serialized_task_object')
305  ->from('tx_scheduler_task', 't')
306  ->setMaxResults(1);
307  // Define where clause
308  // If no uid is given, take any non-disabled task which has a next execution time in the past
309  if (empty($uid)) {
310  $queryBuilder->getRestrictions()->removeAll();
311  $queryBuilder->leftJoin(
312  't',
313  'tx_scheduler_task_group',
314  'g',
315  $queryBuilder->expr()->eq('t.task_group', $queryBuilder->quoteIdentifier('g.uid'))
316  );
317  $queryBuilder->where(
318  $queryBuilder->expr()->eq('t.disable', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
319  $queryBuilder->expr()->neq('t.nextexecution', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
320  $queryBuilder->expr()->lte(
321  't.nextexecution',
322  $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
323  ),
324  $queryBuilder->expr()->orX(
325  $queryBuilder->expr()->eq('g.hidden', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
326  $queryBuilder->expr()->isNull('g.hidden')
327  ),
328  $queryBuilder->expr()->eq('t.deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
329  );
330  $queryBuilder->orderBy('t.nextexecution', 'ASC');
331  } else {
332  $queryBuilder->where(
333  $queryBuilder->expr()->eq('t.uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)),
334  $queryBuilder->expr()->eq('t.deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
335  );
336  }
337 
338  $row = $queryBuilder->execute()->fetch();
339  if (empty($row)) {
340  if (empty($uid)) {
341  // No uid was passed and no overdue task was found
342  throw new \OutOfBoundsException('No (more) tasks available for execution', 1247827244);
343  }
344  // Although a uid was passed, no task with given was found
345  throw new \OutOfBoundsException('No task with id ' . $uid . ' found', 1422044826);
346  }
348  $task = unserialize($row['serialized_task_object']);
349  if ($this->isValidTaskObject($task)) {
350  // The task is valid, return it
351  $task->setScheduler();
352  if ($task->getTaskGroup() === null) {
353  // Fix invalid task_group=NULL settings in order to avoid exceptions when saving on PostgreSQL
354  $task->setTaskGroup(0);
355  }
356  } else {
357  // Forcibly set the disable flag to 1 in the database,
358  // so that the task does not come up again and again for execution
359  $connectionPool->getConnectionForTable('tx_scheduler_task')->update(
360  'tx_scheduler_task',
361  ['disable' => 1],
362  ['uid' => (int)$row['uid']]
363  );
364  // Throw an exception to raise the problem
365  throw new \UnexpectedValueException('Could not unserialize task', 1255083671);
366  }
367 
368  return $task;
369  }
370 
380  public function fetchTaskRecord($uid)
381  {
382  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
383  ->getQueryBuilderForTable('tx_scheduler_task');
384  $row = $queryBuilder->select('*')
385  ->from('tx_scheduler_task')
386  ->where(
387  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)$uid, \PDO::PARAM_INT)),
388  $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
389  )
390  ->execute()
391  ->fetch();
392 
393  // If the task is not found, throw an exception
394  if (empty($row)) {
395  throw new \OutOfBoundsException('No task', 1247827245);
396  }
397 
398  return $row;
399  }
400 
409  public function fetchTasksWithCondition($where, $includeDisabledTasks = false)
410  {
411  $tasks = [];
412  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
413  ->getQueryBuilderForTable('tx_scheduler_task');
414 
415  $queryBuilder
416  ->select('serialized_task_object')
417  ->from('tx_scheduler_task')
418  ->where(
419  $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
420  );
421 
422  if (!$includeDisabledTasks) {
423  $queryBuilder->andWhere(
424  $queryBuilder->expr()->eq('disable', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
425  );
426  }
427 
428  if (!empty($where)) {
429  $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($where));
430  }
431 
432  $result = $queryBuilder->execute();
433  while ($row = $result->fetch()) {
435  $task = unserialize($row['serialized_task_object']);
436  // Add the task to the list only if it is valid
437  if ($this->isValidTaskObject($task)) {
438  $task->setScheduler();
439  $tasks[] = $task;
440  }
441  }
442 
443  return $tasks;
444  }
445 
458  public function isValidTaskObject($task)
459  {
460  return $task instanceof AbstractTask && get_class($task->getExecution()) !== \__PHP_Incomplete_Class::class;
461  }
462 
471  public function log($message, $status = 0, $code = '')
472  {
473  // this method could be called from the constructor (via "cleanExecutionArrays") and no logger is instantiated
474  // by then, that's why check if the logger is available
475  if (!($this->logger instanceof LoggerInterface)) {
476  $this->setLogger(GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__));
477  }
478  $message = trim('[scheduler]: ' . $code) . ' - ' . $message;
479  switch ((int)$status) {
480  // error (user problem)
481  case 1:
482  $this->logger->alert($message);
483  break;
484  // System Error (which should not happen)
485  case 2:
486  $this->logger->error($message);
487  break;
488  // security notice (admin)
489  case 3:
490  $this->logger->emergency($message);
491  break;
492  // regular message (= 0)
493  default:
494  $this->logger->info($message);
495  }
496  }
497 }
‪TYPO3\CMS\Scheduler
Definition: AbstractAdditionalFieldProvider.php:18
‪TYPO3\CMS\Scheduler\Scheduler\__construct
‪__construct()
Definition: Scheduler.php:46
‪TYPO3\CMS\Scheduler\Task\AbstractTask\save
‪bool save()
Definition: AbstractTask.php:558
‪TYPO3\CMS\Scheduler\Task\AbstractTask\getTaskUid
‪int getTaskUid()
Definition: AbstractTask.php:138
‪TYPO3\CMS\Core\Configuration\ExtensionConfiguration
Definition: ExtensionConfiguration.php:45
‪TYPO3\CMS\Scheduler\Task\AbstractTask\setTaskUid
‪setTaskUid($id)
Definition: AbstractTask.php:128
‪TYPO3\CMS\Core\Registry
Definition: Registry.php:33
‪TYPO3\CMS\Scheduler\Scheduler\$extConf
‪array $extConf
Definition: Scheduler.php:41
‪TYPO3\CMS\Scheduler\Scheduler\log
‪log($message, $status=0, $code='')
Definition: Scheduler.php:470
‪$fields
‪$fields
Definition: pages.php:5
‪TYPO3\CMS\Scheduler\Scheduler\addTask
‪bool addTask(AbstractTask $task)
Definition: Scheduler.php:63
‪TYPO3\CMS\Scheduler\Task\AbstractTask
Definition: AbstractTask.php:35
‪TYPO3\CMS\Core\Database\Query\QueryHelper
Definition: QueryHelper.php:32
‪TYPO3\CMS\Scheduler\Scheduler
Definition: Scheduler.php:36
‪TYPO3\CMS\Scheduler\Task\AbstractTask\isDisabled
‪bool isDisabled()
Definition: AbstractTask.php:178
‪TYPO3\CMS\Scheduler\Scheduler\cleanExecutionArrays
‪cleanExecutionArrays()
Definition: Scheduler.php:99
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:36
‪TYPO3\CMS\Core\SingletonInterface
Definition: SingletonInterface.php:23
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Log\LogManager
Definition: LogManager.php:30
‪TYPO3\CMS\Scheduler\Task\AbstractTask\getTaskGroup
‪int getTaskGroup()
Definition: AbstractTask.php:232
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Scheduler\Task\AbstractTask\getDescription
‪string getDescription()
Definition: AbstractTask.php:272
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪TYPO3\CMS\Core\Database\Connection\PARAM_LOB
‪const PARAM_LOB
Definition: Connection.php:57