TYPO3 CMS  TYPO3_7-6
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 
21 
27 {
31  public $extConf = [];
32 
38  public function __construct()
39  {
40  // Get configuration from the extension manager
41  $this->extConf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['scheduler']);
42  if (empty($this->extConf['maxLifetime'])) {
43  $this->extConf['maxLifetime'] = 1440;
44  }
45  if (empty($this->extConf['useAtdaemon'])) {
46  $this->extConf['useAtdaemon'] = 0;
47  }
48  // Clean up the serialized execution arrays
49  $this->cleanExecutionArrays();
50  }
51 
58  public function addTask(Task\AbstractTask $task)
59  {
60  $taskUid = $task->getTaskUid();
61  if (empty($taskUid)) {
62  $fields = [
63  'crdate' => $GLOBALS['EXEC_TIME'],
64  'disable' => $task->isDisabled(),
65  'description' => $task->getDescription(),
66  'task_group' => $task->getTaskGroup(),
67  'serialized_task_object' => 'RESERVED'
68  ];
69  $result = $this->getDatabaseConnection()->exec_INSERTquery('tx_scheduler_task', $fields);
70  if ($result) {
71  $task->setTaskUid($this->getDatabaseConnection()->sql_insert_id());
72  $task->save();
73  $result = true;
74  } else {
75  $result = false;
76  }
77  } else {
78  $result = false;
79  }
80  return $result;
81  }
82 
89  protected function cleanExecutionArrays()
90  {
91  $tstamp = $GLOBALS['EXEC_TIME'];
92  $db = $this->getDatabaseConnection();
93  // Select all tasks with executions
94  // NOTE: this cleanup is done for disabled tasks too,
95  // to avoid leaving old executions lying around
96  $res = $db->exec_SELECTquery('uid, serialized_executions, serialized_task_object', 'tx_scheduler_task', 'serialized_executions <> \'\'');
97  $maxDuration = $this->extConf['maxLifetime'] * 60;
98  while ($row = $db->sql_fetch_assoc($res)) {
99  $executions = [];
100  if ($serialized_executions = unserialize($row['serialized_executions'])) {
101  foreach ($serialized_executions as $task) {
102  if ($tstamp - $task < $maxDuration) {
103  $executions[] = $task;
104  } else {
105  $task = unserialize($row['serialized_task_object']);
106  $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());
107  $this->log($logMessage);
108  }
109  }
110  }
111  $executionCount = count($executions);
112  if (count($serialized_executions) !== $executionCount) {
113  if ($executionCount === 0) {
114  $value = '';
115  } else {
116  $value = serialize($executions);
117  }
118  $db->exec_UPDATEquery('tx_scheduler_task', 'uid = ' . (int)$row['uid'], ['serialized_executions' => $value]);
119  }
120  }
121  $db->sql_free_result($res);
122  }
123 
133  public function executeTask(Task\AbstractTask $task)
134  {
135  // Trigger the saving of the task, as this will calculate its next execution time
136  // This should be calculated all the time, even if the execution is skipped
137  // (in case it is skipped, this pushes back execution to the next possible date)
138  $task->save();
139  // Set a scheduler object for the task again,
140  // as it was removed during the save operation
141  $task->setScheduler();
142  $result = true;
143  // Task is already running and multiple executions are not allowed
144  if (!$task->areMultipleExecutionsAllowed() && $task->isExecutionRunning()) {
145  // Log multiple execution error
146  $logMessage = 'Task is already running and multiple executions are not allowed, skipping! Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
147  $this->log($logMessage);
148  $result = false;
149  } else {
150  // Log scheduler invocation
151  $logMessage = 'Start execution. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
152  $this->log($logMessage);
153  // Register execution
154  $executionID = $task->markExecution();
155  $failure = null;
156  try {
157  // Execute task
158  $successfullyExecuted = $task->execute();
159  if (!$successfullyExecuted) {
160  throw new FailedExecutionException('Task failed to execute successfully. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid(), 1250596541);
161  }
162  } catch (\Exception $e) {
163  // Store exception, so that it can be saved to database
164  $failure = $e;
165  }
166  // make sure database-connection is fine
167  // for long-running tasks the database might meanwhile have disconnected
168  $this->getDatabaseConnection()->isConnected();
169  // Un-register execution
170  $task->unmarkExecution($executionID, $failure);
171  // Log completion of execution
172  $logMessage = 'Task executed. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
173  $this->log($logMessage);
174  // Now that the result of the task execution has been handled,
175  // throw the exception again, if any
176  if ($failure instanceof \Exception) {
177  throw $failure;
178  }
179  }
180  return $result;
181  }
182 
189  public function recordLastRun($type = 'cron')
190  {
191  // Validate input value
192  if ($type !== 'manual' && $type !== 'cli-by-id') {
193  $type = 'cron';
194  }
196  $registry = GeneralUtility::makeInstance(Registry::class);
197  $runInformation = ['start' => $GLOBALS['EXEC_TIME'], 'end' => time(), 'type' => $type];
198  $registry->set('tx_scheduler', 'lastRun', $runInformation);
199  }
200 
209  public function removeTask(Task\AbstractTask $task)
210  {
211  $taskUid = $task->getTaskUid();
212  if (!empty($taskUid)) {
213  $result = $this->getDatabaseConnection()->exec_DELETEquery('tx_scheduler_task', 'uid = ' . $taskUid);
214  } else {
215  $result = false;
216  }
217  if ($result) {
218  $this->scheduleNextSchedulerRunUsingAtDaemon();
219  }
220  return $result;
221  }
222 
229  public function saveTask(Task\AbstractTask $task)
230  {
231  $taskUid = $task->getTaskUid();
232  if (!empty($taskUid)) {
233  try {
234  $executionTime = $task->getNextDueExecution();
235  $task->setExecutionTime($executionTime);
236  } catch (\Exception $e) {
237  $task->setDisabled(true);
238  $executionTime = 0;
239  }
240  $task->unsetScheduler();
241  $fields = [
242  'nextexecution' => $executionTime,
243  'disable' => $task->isDisabled(),
244  'description' => $task->getDescription(),
245  'task_group' => $task->getTaskGroup(),
246  'serialized_task_object' => serialize($task)
247  ];
248  $result = $this->getDatabaseConnection()->exec_UPDATEquery('tx_scheduler_task', 'uid = ' . $taskUid, $fields);
249  } else {
250  $result = false;
251  }
252  if ($result) {
253  $this->scheduleNextSchedulerRunUsingAtDaemon();
254  }
255  return $result;
256  }
257 
268  public function fetchTask($uid = 0)
269  {
270  // Define where clause
271  // If no uid is given, take any non-disabled task which has a next execution time in the past
272  if (empty($uid)) {
273  $queryArray = [
274  'SELECT' => 'tx_scheduler_task.uid AS uid, serialized_task_object',
275  'FROM' => 'tx_scheduler_task LEFT JOIN tx_scheduler_task_group ON tx_scheduler_task.task_group = tx_scheduler_task_group.uid',
276  'WHERE' => 'disable = 0 AND nextexecution != 0 AND nextexecution <= ' . $GLOBALS['EXEC_TIME'] . ' AND (tx_scheduler_task_group.hidden = 0 OR tx_scheduler_task_group.hidden IS NULL)',
277  'LIMIT' => 1
278  ];
279  } else {
280  $queryArray = [
281  'SELECT' => 'uid, serialized_task_object',
282  'FROM' => 'tx_scheduler_task',
283  'WHERE' => 'uid = ' . (int)$uid,
284  'LIMIT' => 1
285  ];
286  }
287 
288  $db = $this->getDatabaseConnection();
289  $res = $db->exec_SELECT_queryArray($queryArray);
290  if ($res === false) {
291  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);
292  }
293  // If there are no available tasks, thrown an exception
294  if ($db->sql_num_rows($res) == 0) {
295  throw new \OutOfBoundsException('No task', 1247827244);
296  } else {
297  $row = $db->sql_fetch_assoc($res);
299  $task = unserialize($row['serialized_task_object']);
300  if ($this->isValidTaskObject($task)) {
301  // The task is valid, return it
302  $task->setScheduler();
303  } else {
304  // Forcibly set the disable flag to 1 in the database,
305  // so that the task does not come up again and again for execution
306  $db->exec_UPDATEquery('tx_scheduler_task', 'uid = ' . $row['uid'], ['disable' => 1]);
307  // Throw an exception to raise the problem
308  throw new \UnexpectedValueException('Could not unserialize task', 1255083671);
309  }
310  $db->sql_free_result($res);
311  }
312  return $task;
313  }
314 
324  public function fetchTaskRecord($uid)
325  {
326  $db = $this->getDatabaseConnection();
327  $res = $db->exec_SELECTquery('*', 'tx_scheduler_task', 'uid = ' . (int)$uid);
328  // If the task is not found, throw an exception
329  if ($db->sql_num_rows($res) == 0) {
330  throw new \OutOfBoundsException('No task', 1247827245);
331  } else {
332  $row = $db->sql_fetch_assoc($res);
333  $db->sql_free_result($res);
334  }
335  return $row;
336  }
337 
346  public function fetchTasksWithCondition($where, $includeDisabledTasks = false)
347  {
348  $whereClause = '';
349  $tasks = [];
350  if (!empty($where)) {
351  $whereClause = $where;
352  }
353  if (!$includeDisabledTasks) {
354  if (!empty($whereClause)) {
355  $whereClause .= ' AND ';
356  }
357  $whereClause .= 'disable = 0';
358  }
359  $db = $this->getDatabaseConnection();
360  $res = $db->exec_SELECTquery('serialized_task_object', 'tx_scheduler_task', $whereClause);
361  if ($res) {
362  while ($row = $db->sql_fetch_assoc($res)) {
364  $task = unserialize($row['serialized_task_object']);
365  // Add the task to the list only if it is valid
366  if ($this->isValidTaskObject($task)) {
367  $task->setScheduler();
368  $tasks[] = $task;
369  }
370  }
371  $db->sql_free_result($res);
372  }
373  return $tasks;
374  }
375 
388  public function isValidTaskObject($task)
389  {
390  return $task instanceof Task\AbstractTask;
391  }
392 
402  public function log($message, $status = 0, $code = 'scheduler')
403  {
404  // Log only if enabled
405  if (!empty($this->extConf['enableBELog'])) {
406  $GLOBALS['BE_USER']->writelog(4, 0, $status, $code, '[scheduler]: ' . $message, []);
407  }
408  }
409 
417  public function scheduleNextSchedulerRunUsingAtDaemon()
418  {
419  if ((int)$this->extConf['useAtdaemon'] !== 1) {
420  return false;
421  }
423  $registry = GeneralUtility::makeInstance(Registry::class);
424  // Get at job id from registry and remove at job
425  $atJobId = $registry->get('tx_scheduler', 'atJobId');
426  if (MathUtility::canBeInterpretedAsInteger($atJobId)) {
427  shell_exec('atrm ' . (int)$atJobId . ' 2>&1');
428  }
429  // Can not use fetchTask() here because if tasks have just executed
430  // they are not in the list of next executions
431  $tasks = $this->fetchTasksWithCondition('');
432  $nextExecution = false;
433  foreach ($tasks as $task) {
434  try {
436  $tempNextExecution = $task->getNextDueExecution();
437  if ($nextExecution === false || $tempNextExecution < $nextExecution) {
438  $nextExecution = $tempNextExecution;
439  }
440  } catch (\OutOfBoundsException $e) {
441  // The event will not be executed again or has already ended - we don't have to consider it for
442  // scheduling the next "at" run
443  }
444  }
445  if ($nextExecution !== false) {
446  if ($nextExecution > $GLOBALS['EXEC_TIME']) {
447  $startTime = strftime('%H:%M %F', $nextExecution);
448  } else {
449  $startTime = 'now+1minute';
450  }
451  $cliDispatchPath = PATH_site . 'typo3/cli_dispatch.phpsh';
452  list($cliDispatchPathEscaped, $startTimeEscaped) =
453  CommandUtility::escapeShellArguments([$cliDispatchPath, $startTime]);
454  $cmd = 'echo ' . $cliDispatchPathEscaped . ' scheduler | at ' . $startTimeEscaped . ' 2>&1';
455  $output = shell_exec($cmd);
456  $outputParts = '';
457  foreach (explode(LF, $output) as $outputLine) {
458  if (GeneralUtility::isFirstPartOfStr($outputLine, 'job')) {
459  $outputParts = explode(' ', $outputLine, 3);
460  break;
461  }
462  }
463  if ($outputParts[0] === 'job' && MathUtility::canBeInterpretedAsInteger($outputParts[1])) {
464  $atJobId = (int)$outputParts[1];
465  $registry->set('tx_scheduler', 'atJobId', $atJobId);
466  }
467  }
468  return true;
469  }
470 
474  protected function getDatabaseConnection()
475  {
476  return $GLOBALS['TYPO3_DB'];
477  }
478 }
static isFirstPartOfStr($str, $partStr)
addTask(Task\AbstractTask $task)
Definition: Scheduler.php:58
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']