TYPO3CMS  8
 All Classes Namespaces Files Functions Variables Pages
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 
23 
29 {
33  public $extConf = [];
34 
40  public function __construct()
41  {
42  // Get configuration from the extension manager
43  $this->extConf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['scheduler'], ['allowed_classes' => false]);
44  if (empty($this->extConf['maxLifetime'])) {
45  $this->extConf['maxLifetime'] = 1440;
46  }
47  if (empty($this->extConf['useAtdaemon'])) {
48  $this->extConf['useAtdaemon'] = 0;
49  }
50  // Clean up the serialized execution arrays
51  $this->cleanExecutionArrays();
52  }
53 
60  public function addTask(Task\AbstractTask $task)
61  {
62  $taskUid = $task->getTaskUid();
63  if (empty($taskUid)) {
64  $fields = [
65  'crdate' => $GLOBALS['EXEC_TIME'],
66  'disable' => (int)$task->isDisabled(),
67  'description' => $task->getDescription(),
68  'task_group' => $task->getTaskGroup(),
69  'serialized_task_object' => 'RESERVED'
70  ];
71  $connection = GeneralUtility::makeInstance(ConnectionPool::class)
72  ->getConnectionForTable('tx_scheduler_task');
73  $result = $connection->insert('tx_scheduler_task', $fields);
74 
75  if ($result) {
76  $task->setTaskUid($connection->lastInsertId('tx_scheduler_task'));
77  $task->save();
78  $result = true;
79  } else {
80  $result = false;
81  }
82  } else {
83  $result = false;
84  }
85  return $result;
86  }
87 
94  protected function cleanExecutionArrays()
95  {
96  $tstamp = $GLOBALS['EXEC_TIME'];
97  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
98  $queryBuilder = $connectionPool->getQueryBuilderForTable('tx_scheduler_task');
99 
100  // Select all tasks with executions
101  // NOTE: this cleanup is done for disabled tasks too,
102  // to avoid leaving old executions lying around
103  $result = $queryBuilder->select('uid', 'serialized_executions', 'serialized_task_object')
104  ->from('tx_scheduler_task')
105  ->where(
106  $queryBuilder->expr()->neq(
107  'serialized_executions',
108  $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
109  )
110  )
111  ->execute();
112  $maxDuration = $this->extConf['maxLifetime'] * 60;
113  while ($row = $result->fetch()) {
114  $executions = [];
115  if ($serialized_executions = unserialize($row['serialized_executions'])) {
116  foreach ($serialized_executions as $task) {
117  if ($tstamp - $task < $maxDuration) {
118  $executions[] = $task;
119  } else {
120  $task = unserialize($row['serialized_task_object']);
121  $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());
122  $this->log($logMessage);
123  }
124  }
125  }
126  $executionCount = count($executions);
127  if (count($serialized_executions) !== $executionCount) {
128  if ($executionCount === 0) {
129  $value = '';
130  } else {
131  $value = serialize($executions);
132  }
133  $connectionPool->getConnectionForTable('tx_scheduler_task')->update(
134  'tx_scheduler_task',
135  ['serialized_executions' => $value],
136  ['uid' => (int)$row['uid']]
137  );
138  }
139  }
140  }
141 
151  public function executeTask(Task\AbstractTask $task)
152  {
153  // Trigger the saving of the task, as this will calculate its next execution time
154  // This should be calculated all the time, even if the execution is skipped
155  // (in case it is skipped, this pushes back execution to the next possible date)
156  $task->save();
157  // Set a scheduler object for the task again,
158  // as it was removed during the save operation
159  $task->setScheduler();
160  $result = true;
161  // Task is already running and multiple executions are not allowed
162  if (!$task->areMultipleExecutionsAllowed() && $task->isExecutionRunning()) {
163  // Log multiple execution error
164  $logMessage = 'Task is already running and multiple executions are not allowed, skipping! Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
165  $this->log($logMessage);
166  $result = false;
167  } else {
168  // Log scheduler invocation
169  $logMessage = 'Start execution. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
170  $this->log($logMessage);
171  // Register execution
172  $executionID = $task->markExecution();
173  $failure = null;
174  try {
175  // Execute task
176  $successfullyExecuted = $task->execute();
177  if (!$successfullyExecuted) {
178  throw new FailedExecutionException('Task failed to execute successfully. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid(), 1250596541);
179  }
180  } catch (\Exception $e) {
181  // Store exception, so that it can be saved to database
182  $failure = $e;
183  }
184  // Un-register execution
185  $task->unmarkExecution($executionID, $failure);
186  // Log completion of execution
187  $logMessage = 'Task executed. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
188  $this->log($logMessage);
189  // Now that the result of the task execution has been handled,
190  // throw the exception again, if any
191  if ($failure instanceof \Exception) {
192  throw $failure;
193  }
194  }
195  return $result;
196  }
197 
204  public function recordLastRun($type = 'cron')
205  {
206  // Validate input value
207  if ($type !== 'manual' && $type !== 'cli-by-id') {
208  $type = 'cron';
209  }
211  $registry = GeneralUtility::makeInstance(Registry::class);
212  $runInformation = ['start' => $GLOBALS['EXEC_TIME'], 'end' => time(), 'type' => $type];
213  $registry->set('tx_scheduler', 'lastRun', $runInformation);
214  }
215 
224  public function removeTask(Task\AbstractTask $task)
225  {
226  $taskUid = $task->getTaskUid();
227  if (!empty($taskUid)) {
228  $result = GeneralUtility::makeInstance(ConnectionPool::class)
229  ->getConnectionForTable('tx_scheduler_task')
230  ->delete('tx_scheduler_task', ['uid' => $taskUid]);
231  } else {
232  $result = false;
233  }
234  if ($result) {
235  $this->scheduleNextSchedulerRunUsingAtDaemon();
236  }
237  return $result;
238  }
239 
246  public function saveTask(Task\AbstractTask $task)
247  {
248  $taskUid = $task->getTaskUid();
249  if (!empty($taskUid)) {
250  try {
251  $executionTime = $task->getNextDueExecution();
252  $task->setExecutionTime($executionTime);
253  } catch (\Exception $e) {
254  $task->setDisabled(true);
255  $executionTime = 0;
256  }
257  $task->unsetScheduler();
258  $fields = [
259  'nextexecution' => $executionTime,
260  'disable' => (int)$task->isDisabled(),
261  'description' => $task->getDescription(),
262  'task_group' => $task->getTaskGroup(),
263  'serialized_task_object' => serialize($task)
264  ];
265  $result = GeneralUtility::makeInstance(ConnectionPool::class)
266  ->getConnectionForTable('tx_scheduler_task')
267  ->update('tx_scheduler_task', $fields, ['uid' => $taskUid]);
268  } else {
269  $result = false;
270  }
271  if ($result) {
272  $this->scheduleNextSchedulerRunUsingAtDaemon();
273  }
274  return $result;
275  }
276 
287  public function fetchTask($uid = 0)
288  {
289  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
290  $queryBuilder = $connectionPool->getQueryBuilderForTable('tx_scheduler_task');
291 
292  $queryBuilder->select('t.uid', 't.serialized_task_object')
293  ->from('tx_scheduler_task', 't')
294  ->setMaxResults(1);
295  // Define where clause
296  // If no uid is given, take any non-disabled task which has a next execution time in the past
297  if (empty($uid)) {
298  $queryBuilder->getRestrictions()->removeAll();
299  $queryBuilder->leftJoin(
300  't',
301  'tx_scheduler_task_group',
302  'g',
303  $queryBuilder->expr()->eq('t.task_group', $queryBuilder->quoteIdentifier('g.uid'))
304  );
305  $queryBuilder->where(
306  $queryBuilder->expr()->eq('t.disable', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
307  $queryBuilder->expr()->neq('t.nextexecution', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
308  $queryBuilder->expr()->lte(
309  't.nextexecution',
310  $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
311  ),
312  $queryBuilder->expr()->orX(
313  $queryBuilder->expr()->eq('g.hidden', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
314  $queryBuilder->expr()->isNull('g.hidden')
315  )
316  );
317  } else {
318  $queryBuilder->where(
319  $queryBuilder->expr()->eq('t.uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
320  );
321  }
322 
323  $row = $queryBuilder->execute()->fetch();
324  if ($row === false) {
325  throw new \OutOfBoundsException('Query could not be executed. Possible defect in tables tx_scheduler_task or tx_scheduler_task_group or DB server problems', 1422044826);
326  } elseif (empty($row)) {
327  // If there are no available tasks, thrown an exception
328  throw new \OutOfBoundsException('No task', 1247827244);
329  } else {
331  $task = unserialize($row['serialized_task_object']);
332  if ($this->isValidTaskObject($task)) {
333  // The task is valid, return it
334  $task->setScheduler();
335  } else {
336  // Forcibly set the disable flag to 1 in the database,
337  // so that the task does not come up again and again for execution
338  $connectionPool->getConnectionForTable('tx_scheduler_task')->update(
339  'tx_scheduler_task',
340  ['disable' => 1],
341  ['uid' => (int)$row['uid']]
342  );
343  // Throw an exception to raise the problem
344  throw new \UnexpectedValueException('Could not unserialize task', 1255083671);
345  }
346  }
347  return $task;
348  }
349 
359  public function fetchTaskRecord($uid)
360  {
361  $row = GeneralUtility::makeInstance(ConnectionPool::class)
362  ->getConnectionForTable('tx_scheduler_task')
363  ->select(['*'], 'tx_scheduler_task', ['uid' => (int)$uid])
364  ->fetch();
365 
366  // If the task is not found, throw an exception
367  if (empty($row)) {
368  throw new \OutOfBoundsException('No task', 1247827245);
369  }
370 
371  return $row;
372  }
373 
382  public function fetchTasksWithCondition($where, $includeDisabledTasks = false)
383  {
384  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
385  ->getQueryBuilderForTable('tx_scheduler_task');
386 
387  $constraints = [];
388  $tasks = [];
389 
390  if (!$includeDisabledTasks) {
391  $constraints[] = $queryBuilder->expr()->eq(
392  'disable',
393  $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
394  );
395  } else {
396  $constraints[] = '1=1';
397  }
398 
399  if (!empty($where)) {
400  $constraints[] = QueryHelper::stripLogicalOperatorPrefix($where);
401  }
402 
403  $result = $queryBuilder->select('serialized_task_object')
404  ->from('tx_scheduler_task')
405  ->where(...$constraints)
406  ->execute();
407 
408  while ($row = $result->fetch()) {
410  $task = unserialize($row['serialized_task_object']);
411  // Add the task to the list only if it is valid
412  if ($this->isValidTaskObject($task)) {
413  $task->setScheduler();
414  $tasks[] = $task;
415  }
416  }
417 
418  return $tasks;
419  }
420 
433  public function isValidTaskObject($task)
434  {
435  return $task instanceof Task\AbstractTask;
436  }
437 
447  public function log($message, $status = 0, $code = 'scheduler')
448  {
449  // Log only if enabled
450  if (!empty($this->extConf['enableBELog'])) {
451  $GLOBALS['BE_USER']->writelog(4, 0, $status, $code, '[scheduler]: ' . $message, []);
452  }
453  }
454 
462  public function scheduleNextSchedulerRunUsingAtDaemon()
463  {
464  if ((int)$this->extConf['useAtdaemon'] !== 1) {
465  return false;
466  }
468  $registry = GeneralUtility::makeInstance(Registry::class);
469  // Get at job id from registry and remove at job
470  $atJobId = $registry->get('tx_scheduler', 'atJobId');
471  if (MathUtility::canBeInterpretedAsInteger($atJobId)) {
472  shell_exec('atrm ' . (int)$atJobId . ' 2>&1');
473  }
474  // Can not use fetchTask() here because if tasks have just executed
475  // they are not in the list of next executions
476  $tasks = $this->fetchTasksWithCondition('');
477  $nextExecution = false;
478  foreach ($tasks as $task) {
479  try {
481  $tempNextExecution = $task->getNextDueExecution();
482  if ($nextExecution === false || $tempNextExecution < $nextExecution) {
483  $nextExecution = $tempNextExecution;
484  }
485  } catch (\OutOfBoundsException $e) {
486  // The event will not be executed again or has already ended - we don't have to consider it for
487  // scheduling the next "at" run
488  }
489  }
490  if ($nextExecution !== false) {
491  if ($nextExecution > $GLOBALS['EXEC_TIME']) {
492  $startTime = strftime('%H:%M %F', $nextExecution);
493  } else {
494  $startTime = 'now+1minute';
495  }
496  $cliDispatchPath = PATH_site . 'typo3/cli_dispatch.phpsh';
497  list($cliDispatchPathEscaped, $startTimeEscaped) =
498  CommandUtility::escapeShellArguments([$cliDispatchPath, $startTime]);
499  $cmd = 'echo ' . $cliDispatchPathEscaped . ' scheduler | at ' . $startTimeEscaped . ' 2>&1';
500  $output = shell_exec($cmd);
501  $outputParts = '';
502  foreach (explode(LF, $output) as $outputLine) {
503  if (GeneralUtility::isFirstPartOfStr($outputLine, 'job')) {
504  $outputParts = explode(' ', $outputLine, 3);
505  break;
506  }
507  }
508  if ($outputParts[0] === 'job' && MathUtility::canBeInterpretedAsInteger($outputParts[1])) {
509  $atJobId = (int)$outputParts[1];
510  $registry->set('tx_scheduler', 'atJobId', $atJobId);
511  }
512  }
513  return true;
514  }
515 }
static isFirstPartOfStr($str, $partStr)
addTask(Task\AbstractTask $task)
Definition: Scheduler.php:60
if(TYPO3_MODE=== 'BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
static makeInstance($className,...$constructorArguments)