TYPO3 CMS  TYPO3_8-7
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 
24 
30 {
34  public $extConf = [];
35 
41  public function __construct()
42  {
43  // Get configuration from the extension manager
44  $this->extConf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['scheduler'], ['allowed_classes' => false]);
45  if (empty($this->extConf['maxLifetime'])) {
46  $this->extConf['maxLifetime'] = 1440;
47  }
48  if (empty($this->extConf['useAtdaemon'])) {
49  $this->extConf['useAtdaemon'] = 0;
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  )
114  ->execute();
115  $maxDuration = $this->extConf['maxLifetime'] * 60;
116  while ($row = $result->fetch()) {
117  $executions = [];
118  if ($serialized_executions = unserialize($row['serialized_executions'])) {
119  foreach ($serialized_executions as $task) {
120  if ($tstamp - $task < $maxDuration) {
121  $executions[] = $task;
122  } else {
123  $task = unserialize($row['serialized_task_object']);
124  $logMessage = '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());
125  $this->log($logMessage);
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->log($logMessage);
170  $result = false;
171  } else {
172  // Log scheduler invocation
173  $logMessage = 'Start execution. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
174  $this->log($logMessage);
175  // Register execution
176  $executionID = $task->markExecution();
177  $failure = null;
178  try {
179  // Execute task
180  $successfullyExecuted = $task->execute();
181  if (!$successfullyExecuted) {
182  throw new FailedExecutionException('Task failed to execute successfully. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid(), 1250596541);
183  }
184  } catch (\Throwable $e) {
185  // Store exception, so that it can be saved to database
186  $failure = $e;
187  }
188  // Un-register execution
189  $task->unmarkExecution($executionID, $failure);
190  // Log completion of execution
191  $logMessage = 'Task executed. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
192  $this->log($logMessage);
193  // Now that the result of the task execution has been handled,
194  // throw the exception again, if any
195  if ($failure instanceof \Throwable) {
196  throw $failure;
197  }
198  }
199  return $result;
200  }
201 
207  public function recordLastRun($type = 'cron')
208  {
209  // Validate input value
210  if ($type !== 'manual' && $type !== 'cli-by-id') {
211  $type = 'cron';
212  }
214  $registry = GeneralUtility::makeInstance(Registry::class);
215  $runInformation = ['start' => $GLOBALS['EXEC_TIME'], 'end' => time(), 'type' => $type];
216  $registry->set('tx_scheduler', 'lastRun', $runInformation);
217  }
218 
227  public function removeTask(Task\AbstractTask $task)
228  {
229  $taskUid = $task->getTaskUid();
230  if (!empty($taskUid)) {
231  $result = GeneralUtility::makeInstance(ConnectionPool::class)
232  ->getConnectionForTable('tx_scheduler_task')
233  ->delete('tx_scheduler_task', ['uid' => $taskUid]);
234  } else {
235  $result = false;
236  }
237  if ($result) {
238  $this->scheduleNextSchedulerRunUsingAtDaemon();
239  }
240  return $result;
241  }
242 
249  public function saveTask(Task\AbstractTask $task)
250  {
251  $result = true;
252  $taskUid = $task->getTaskUid();
253  if (!empty($taskUid)) {
254  try {
255  if ($task->getRunOnNextCronJob()) {
256  $executionTime = time();
257  } else {
258  $executionTime = $task->getNextDueExecution();
259  }
260  $task->setExecutionTime($executionTime);
261  } catch (\Exception $e) {
262  $task->setDisabled(true);
263  $executionTime = 0;
264  }
265  $task->unsetScheduler();
266  $fields = [
267  'nextexecution' => $executionTime,
268  'disable' => (int)$task->isDisabled(),
269  'description' => $task->getDescription(),
270  'task_group' => $task->getTaskGroup(),
271  'serialized_task_object' => serialize($task)
272  ];
273  try {
274  GeneralUtility::makeInstance(ConnectionPool::class)
275  ->getConnectionForTable('tx_scheduler_task')
276  ->update(
277  'tx_scheduler_task',
278  $fields,
279  ['uid' => $taskUid],
280  ['serialized_task_object' => Connection::PARAM_LOB]
281  );
282  } catch (\Doctrine\DBAL\DBALException $e) {
283  $result = false;
284  }
285  } else {
286  $result = false;
287  }
288  if ($result) {
289  $this->scheduleNextSchedulerRunUsingAtDaemon();
290  }
291  return $result;
292  }
293 
304  public function fetchTask($uid = 0)
305  {
306  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
307  $queryBuilder = $connectionPool->getQueryBuilderForTable('tx_scheduler_task');
308 
309  $queryBuilder->select('t.uid', 't.serialized_task_object')
310  ->from('tx_scheduler_task', 't')
311  ->setMaxResults(1);
312  // Define where clause
313  // If no uid is given, take any non-disabled task which has a next execution time in the past
314  if (empty($uid)) {
315  $queryBuilder->getRestrictions()->removeAll();
316  $queryBuilder->leftJoin(
317  't',
318  'tx_scheduler_task_group',
319  'g',
320  $queryBuilder->expr()->eq('t.task_group', $queryBuilder->quoteIdentifier('g.uid'))
321  );
322  $queryBuilder->where(
323  $queryBuilder->expr()->eq('t.disable', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
324  $queryBuilder->expr()->neq('t.nextexecution', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
325  $queryBuilder->expr()->lte(
326  't.nextexecution',
327  $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
328  ),
329  $queryBuilder->expr()->orX(
330  $queryBuilder->expr()->eq('g.hidden', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
331  $queryBuilder->expr()->isNull('g.hidden')
332  )
333  );
334  $queryBuilder->orderBy('t.nextexecution', 'ASC');
335  } else {
336  $queryBuilder->where(
337  $queryBuilder->expr()->eq('t.uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
338  );
339  }
340 
341  $row = $queryBuilder->execute()->fetch();
342  if (empty($row)) {
343  if (empty($uid)) {
344  // No uid was passed and no overdue task was found
345  throw new \OutOfBoundsException('No tasks available for execution', 1247827244);
346  }
347  // Although a uid was passed, no task with given was found
348  throw new \OutOfBoundsException('No task with id ' . $uid . ' found', 1422044826);
349  }
351  $task = unserialize($row['serialized_task_object']);
352  if ($this->isValidTaskObject($task)) {
353  // The task is valid, return it
354  $task->setScheduler();
355  } else {
356  // Forcibly set the disable flag to 1 in the database,
357  // so that the task does not come up again and again for execution
358  $connectionPool->getConnectionForTable('tx_scheduler_task')->update(
359  'tx_scheduler_task',
360  ['disable' => 1],
361  ['uid' => (int)$row['uid']]
362  );
363  // Throw an exception to raise the problem
364  throw new \UnexpectedValueException('Could not unserialize task', 1255083671);
365  }
366 
367  return $task;
368  }
369 
379  public function fetchTaskRecord($uid)
380  {
381  $row = GeneralUtility::makeInstance(ConnectionPool::class)
382  ->getConnectionForTable('tx_scheduler_task')
383  ->select(['*'], 'tx_scheduler_task', ['uid' => (int)$uid])
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  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
405  ->getQueryBuilderForTable('tx_scheduler_task');
406 
407  $constraints = [];
408  $tasks = [];
409 
410  if (!$includeDisabledTasks) {
411  $constraints[] = $queryBuilder->expr()->eq(
412  'disable',
413  $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
414  );
415  } else {
416  $constraints[] = '1=1';
417  }
418 
419  if (!empty($where)) {
420  $constraints[] = QueryHelper::stripLogicalOperatorPrefix($where);
421  }
422 
423  $result = $queryBuilder->select('serialized_task_object')
424  ->from('tx_scheduler_task')
425  ->where(...$constraints)
426  ->execute();
427 
428  while ($row = $result->fetch()) {
430  $task = unserialize($row['serialized_task_object']);
431  // Add the task to the list only if it is valid
432  if ($this->isValidTaskObject($task)) {
433  $task->setScheduler();
434  $tasks[] = $task;
435  }
436  }
437 
438  return $tasks;
439  }
440 
453  public function isValidTaskObject($task)
454  {
455  return $task instanceof Task\AbstractTask && get_class($task->getExecution()) !== '__PHP_Incomplete_Class';
456  }
457 
466  public function log($message, $status = 0, $code = 'scheduler')
467  {
468  // Log only if enabled
469  if (!empty($this->extConf['enableBELog'])) {
470  $GLOBALS['BE_USER']->writelog(4, 0, $status, 0, '[scheduler]: ' . $code . ' - ' . $message, []);
471  }
472  }
473 
481  public function scheduleNextSchedulerRunUsingAtDaemon()
482  {
483  if ((int)$this->extConf['useAtdaemon'] !== 1) {
484  return false;
485  }
487  $registry = GeneralUtility::makeInstance(Registry::class);
488  // Get at job id from registry and remove at job
489  $atJobId = $registry->get('tx_scheduler', 'atJobId');
490  if (MathUtility::canBeInterpretedAsInteger($atJobId)) {
491  shell_exec('atrm ' . (int)$atJobId . ' 2>&1');
492  }
493  // Can not use fetchTask() here because if tasks have just executed
494  // they are not in the list of next executions
495  $tasks = $this->fetchTasksWithCondition('');
496  $nextExecution = false;
497  foreach ($tasks as $task) {
498  try {
500  $tempNextExecution = $task->getNextDueExecution();
501  if ($nextExecution === false || $tempNextExecution < $nextExecution) {
502  $nextExecution = $tempNextExecution;
503  }
504  } catch (\OutOfBoundsException $e) {
505  // The event will not be executed again or has already ended - we don't have to consider it for
506  // scheduling the next "at" run
507  }
508  }
509  if ($nextExecution !== false) {
510  if ($nextExecution > $GLOBALS['EXEC_TIME']) {
511  $startTime = strftime('%H:%M %F', $nextExecution);
512  } else {
513  $startTime = 'now+1minute';
514  }
515  $cliDispatchPath = PATH_site . 'typo3/sysext/core/bin/typo3';
516  list($cliDispatchPathEscaped, $startTimeEscaped) =
517  CommandUtility::escapeShellArguments([$cliDispatchPath, $startTime]);
518  $cmd = 'echo ' . $cliDispatchPathEscaped . ' scheduler:run | at ' . $startTimeEscaped . ' 2>&1';
519  $output = shell_exec($cmd);
520  $outputParts = '';
521  foreach (explode(LF, $output) as $outputLine) {
522  if (GeneralUtility::isFirstPartOfStr($outputLine, 'job')) {
523  $outputParts = explode(' ', $outputLine, 3);
524  break;
525  }
526  }
527  if ($outputParts[0] === 'job' && MathUtility::canBeInterpretedAsInteger($outputParts[1])) {
528  $atJobId = (int)$outputParts[1];
529  $registry->set('tx_scheduler', 'atJobId', $atJobId);
530  }
531  }
532  return true;
533  }
534 }
static isFirstPartOfStr($str, $partStr)
static makeInstance($className,... $constructorArguments)
$fields
Definition: pages.php:4
addTask(Task\AbstractTask $task)
Definition: Scheduler.php:61
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']