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