f14014f1142dd1e8278289ff8d1108b554c9a288
[Packages/TYPO3.CMS.git] / typo3 / sysext / scheduler / Classes / Task / AbstractTask.php
1 <?php
2 namespace TYPO3\CMS\Scheduler\Task;
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 TYPO3\CMS\Core\Database\Connection;
20 use TYPO3\CMS\Core\Database\ConnectionPool;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22 use TYPO3\CMS\Scheduler\Execution;
23
24 /**
25 * This is the base class for all Scheduler tasks
26 * It's an abstract class, not designed to be instantiated directly
27 * All Scheduler tasks should inherit from this class
28 */
29 abstract class AbstractTask implements LoggerAwareInterface
30 {
31 use LoggerAwareTrait;
32
33 const TYPE_SINGLE = 1;
34 const TYPE_RECURRING = 2;
35
36 /**
37 * Reference to a scheduler object
38 *
39 * @var \TYPO3\CMS\Scheduler\Scheduler
40 */
41 protected $scheduler;
42
43 /**
44 * The unique id of the task used to identify it in the database
45 *
46 * @var int
47 */
48 protected $taskUid;
49
50 /**
51 * Disable flag, TRUE if task is disabled, FALSE otherwise
52 *
53 * @var bool
54 */
55 protected $disabled = false;
56
57 /**
58 * Run on next cron job flag, TRUE if task should run on next cronjob, FALSE otherwise
59 *
60 * @var bool
61 */
62 protected $runOnNextCronJob = false;
63
64 /**
65 * The execution object related to the task
66 *
67 * @var Execution
68 */
69 protected $execution;
70
71 /**
72 * This variable contains the time of next execution of the task
73 *
74 * @var int
75 */
76 protected $executionTime = 0;
77
78 /**
79 * Description for the task
80 *
81 * @var string
82 */
83 protected $description = '';
84
85 /**
86 * Task group for this task
87 *
88 * @var int
89 */
90 protected $taskGroup;
91
92 /**
93 * Constructor
94 */
95 public function __construct()
96 {
97 $this->setScheduler();
98 $this->execution = GeneralUtility::makeInstance(Execution::class);
99 }
100
101 /**
102 * This is the main method that is called when a task is executed
103 * It MUST be implemented by all classes inheriting from this one
104 * Note that there is no error handling, errors and failures are expected
105 * to be handled and logged by the client implementations.
106 * Should return TRUE on successful execution, FALSE on error.
107 *
108 * @return bool Returns TRUE on successful execution, FALSE on error
109 */
110 abstract public function execute();
111
112 /**
113 * This method is designed to return some additional information about the task,
114 * that may help to set it apart from other tasks from the same class
115 * This additional information is used - for example - in the Scheduler's BE module
116 * This method should be implemented in most task classes
117 *
118 * @return string Information to display
119 */
120 public function getAdditionalInformation()
121 {
122 return '';
123 }
124
125 /**
126 * This method is used to set the unique id of the task
127 *
128 * @param int $id Primary key (from the database record) of the scheduled task
129 */
130 public function setTaskUid($id)
131 {
132 $this->taskUid = (int)$id;
133 }
134
135 /**
136 * This method returns the unique id of the task
137 *
138 * @return int The id of the task
139 */
140 public function getTaskUid()
141 {
142 return $this->taskUid;
143 }
144
145 /**
146 * This method returns the title of the scheduler task
147 *
148 * @return string
149 */
150 public function getTaskTitle()
151 {
152 return $GLOBALS['LANG']->sL($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][static::class]['title']);
153 }
154
155 /**
156 * This method returns the description of the scheduler task
157 *
158 * @return string
159 */
160 public function getTaskDescription()
161 {
162 return $GLOBALS['LANG']->sL($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][static::class]['description']);
163 }
164
165 /**
166 * This method returns the class name of the scheduler task
167 *
168 * @return string
169 */
170 public function getTaskClassName()
171 {
172 return static::class;
173 }
174
175 /**
176 * This method returns the disable status of the task
177 *
178 * @return bool TRUE if task is disabled, FALSE otherwise
179 */
180 public function isDisabled()
181 {
182 return $this->disabled;
183 }
184
185 /**
186 * This method is used to set the disable status of the task
187 *
188 * @param bool $flag TRUE if task should be disabled, FALSE otherwise
189 */
190 public function setDisabled($flag)
191 {
192 if ($flag) {
193 $this->disabled = true;
194 } else {
195 $this->disabled = false;
196 }
197 }
198
199 /**
200 * This method set the flag for next cron job execution
201 *
202 * @param bool $flag TRUE if task should run with the next cron job, FALSE otherwise
203 */
204 public function setRunOnNextCronJob($flag)
205 {
206 $this->runOnNextCronJob = $flag;
207 }
208
209 /**
210 * This method returns the run on next cron job status of the task
211 *
212 * @return bool TRUE if task should run on next cron job, FALSE otherwise
213 */
214 public function getRunOnNextCronJob()
215 {
216 return $this->runOnNextCronJob;
217 }
218
219 /**
220 * This method is used to set the timestamp corresponding to the next execution time of the task
221 *
222 * @param int $timestamp Timestamp of next execution
223 */
224 public function setExecutionTime($timestamp)
225 {
226 $this->executionTime = (int)$timestamp;
227 }
228
229 /**
230 * This method returns the task group (uid) of the task
231 *
232 * @return int Uid of task group
233 */
234 public function getTaskGroup()
235 {
236 return $this->taskGroup;
237 }
238
239 /**
240 * This method is used to set the task group (uid) of the task
241 *
242 * @param int $taskGroup Uid of task group
243 */
244 public function setTaskGroup($taskGroup)
245 {
246 $this->taskGroup = (int)$taskGroup;
247 }
248
249 /**
250 * This method returns the timestamp corresponding to the next execution time of the task
251 *
252 * @return int Timestamp of next execution
253 */
254 public function getExecutionTime()
255 {
256 return $this->executionTime;
257 }
258
259 /**
260 * This method is used to set the description of the task
261 *
262 * @param string $description Description
263 */
264 public function setDescription($description)
265 {
266 $this->description = $description;
267 }
268
269 /**
270 * This method returns the description of the task
271 *
272 * @return string Description
273 */
274 public function getDescription()
275 {
276 return $this->description;
277 }
278
279 /**
280 * Sets the internal reference to the singleton instance of the Scheduler
281 */
282 public function setScheduler()
283 {
284 $this->scheduler = GeneralUtility::makeInstance(\TYPO3\CMS\Scheduler\Scheduler::class);
285 }
286
287 /**
288 * Unsets the internal reference to the singleton instance of the Scheduler
289 * This is done before a task is serialized, so that the scheduler instance
290 * is not saved to the database too
291 */
292 public function unsetScheduler()
293 {
294 unset($this->scheduler);
295 }
296
297 /**
298 * Registers a single execution of the task
299 *
300 * @param int $timestamp Timestamp of the next execution
301 */
302 public function registerSingleExecution($timestamp)
303 {
304 /** @var $execution Execution */
305 $execution = GeneralUtility::makeInstance(Execution::class);
306 $execution->setStart($timestamp);
307 $execution->setInterval(0);
308 $execution->setEnd($timestamp);
309 $execution->setCronCmd('');
310 $execution->setMultiple(0);
311 $execution->setIsNewSingleExecution(true);
312 // Replace existing execution object
313 $this->execution = $execution;
314 }
315
316 /**
317 * Registers a recurring execution of the task
318 *
319 * @param int $start The first date/time where this execution should occur (timestamp)
320 * @param string $interval Execution interval in seconds
321 * @param int $end The last date/time where this execution should occur (timestamp)
322 * @param bool $multiple Set to FALSE if multiple executions of this task are not permitted in parallel
323 * @param string $cron_cmd Used like in crontab (minute hour day month weekday)
324 */
325 public function registerRecurringExecution($start, $interval, $end = 0, $multiple = false, $cron_cmd = '')
326 {
327 /** @var $execution Execution */
328 $execution = GeneralUtility::makeInstance(Execution::class);
329 // Set general values
330 $execution->setStart($start);
331 $execution->setEnd($end);
332 $execution->setMultiple($multiple);
333 if (empty($cron_cmd)) {
334 // Use interval
335 $execution->setInterval($interval);
336 $execution->setCronCmd('');
337 } else {
338 // Use cron syntax
339 $execution->setInterval(0);
340 $execution->setCronCmd($cron_cmd);
341 }
342 // Replace existing execution object
343 $this->execution = $execution;
344 }
345
346 /**
347 * Sets the internal execution object
348 *
349 * @param Execution $execution The execution to add
350 */
351 public function setExecution(Execution $execution)
352 {
353 $this->execution = $execution;
354 }
355
356 /**
357 * Returns the execution object
358 *
359 * @return Execution The internal execution object
360 */
361 public function getExecution()
362 {
363 return $this->execution;
364 }
365
366 /**
367 * Returns the timestamp for next due execution of the task
368 *
369 * @return int Date and time of the next execution as a timestamp
370 */
371 public function getNextDueExecution()
372 {
373 // NOTE: this call may throw an exception, but we let it bubble up
374 return $this->execution->getNextExecution();
375 }
376
377 /**
378 * Returns TRUE if several runs of the task are allowed concurrently
379 *
380 * @return bool TRUE if concurrent executions are allowed, FALSE otherwise
381 */
382 public function areMultipleExecutionsAllowed()
383 {
384 return $this->execution->getMultiple();
385 }
386
387 /**
388 * Returns TRUE if an instance of the task is already running
389 *
390 * @return bool TRUE if an instance is already running, FALSE otherwise
391 */
392 public function isExecutionRunning()
393 {
394 $isRunning = false;
395 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
396 ->getQueryBuilderForTable('tx_scheduler_task');
397 $row = $queryBuilder
398 ->select('serialized_executions')
399 ->from('tx_scheduler_task')
400 ->where(
401 $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->taskUid, \PDO::PARAM_INT))
402 )
403 ->execute()
404 ->fetch();
405
406 if ($row && !empty($row['serialized_executions'])) {
407 $isRunning = true;
408 }
409 return $isRunning;
410 }
411
412 /**
413 * This method adds current execution to the execution list
414 * It also logs the execution time and mode
415 *
416 * @return int Execution id
417 */
418 public function markExecution()
419 {
420 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
421 ->getQueryBuilderForTable('tx_scheduler_task');
422
423 $row = $queryBuilder
424 ->select('serialized_executions')
425 ->from('tx_scheduler_task')
426 ->where(
427 $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->taskUid, \PDO::PARAM_INT))
428 )
429 ->execute()
430 ->fetch();
431
432 $runningExecutions = [];
433 if ($row && !empty($row['serialized_executions'])) {
434 $runningExecutions = unserialize($row['serialized_executions']);
435 }
436 // Count the number of existing executions and use that number as a key
437 // (we need to know that number, because it is returned at the end of the method)
438 $numExecutions = count($runningExecutions);
439 $runningExecutions[$numExecutions] = time();
440 // Define the context in which the script is running
441 $context = 'BE';
442 if (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_CLI) {
443 $context = 'CLI';
444 }
445 GeneralUtility::makeInstance(ConnectionPool::class)
446 ->getConnectionForTable('tx_scheduler_task')
447 ->update(
448 'tx_scheduler_task',
449 [
450 'serialized_executions' => serialize($runningExecutions),
451 'lastexecution_time' => time(),
452 'lastexecution_context' => $context
453 ],
454 [
455 'uid' => $this->taskUid
456 ],
457 [
458 'serialized_executions' => Connection::PARAM_LOB,
459 ]
460 );
461 return $numExecutions;
462 }
463
464 /**
465 * Removes given execution from list
466 *
467 * @param int $executionID Id of the execution to remove.
468 * @param \Exception $failure An exception to signal a failed execution
469 */
470 public function unmarkExecution($executionID, \Exception $failure = null)
471 {
472 // Get the executions for the task
473 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
474 ->getQueryBuilderForTable('tx_scheduler_task');
475
476 $row = $queryBuilder
477 ->select('serialized_executions')
478 ->from('tx_scheduler_task')
479 ->where(
480 $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->taskUid, \PDO::PARAM_INT))
481 )
482 ->execute()
483 ->fetch();
484
485 if ($row && $row['serialized_executions'] !== '') {
486 $runningExecutions = unserialize($row['serialized_executions']);
487 // Remove the selected execution
488 unset($runningExecutions[$executionID]);
489 if (!empty($runningExecutions)) {
490 // Re-serialize the updated executions list (if necessary)
491 $runningExecutionsSerialized = serialize($runningExecutions);
492 } else {
493 $runningExecutionsSerialized = '';
494 }
495 if ($failure instanceof \Exception) {
496 // Log failed execution
497 $logMessage = 'Task failed to execute successfully. Class: ' . static::class
498 . ', UID: ' . $this->taskUid . ', Code: ' . $failure->getCode() . ', ' . $failure->getMessage();
499 $this->logger->error($logMessage, ['exception' => $failure]);
500 // Do not serialize the complete exception or the trace, this can lead to huge strings > 50MB
501 $failureString = serialize([
502 'code' => $failure->getCode(),
503 'message' => $failure->getMessage(),
504 'file' => $failure->getFile(),
505 'line' => $failure->getLine(),
506 'traceString' => $failure->getTraceAsString(),
507 ]);
508 } else {
509 $failureString = '';
510 }
511 // Save the updated executions list
512 GeneralUtility::makeInstance(ConnectionPool::class)
513 ->getConnectionForTable('tx_scheduler_task')
514 ->update(
515 'tx_scheduler_task',
516 [
517 'serialized_executions' => $runningExecutionsSerialized,
518 'lastexecution_failure' => $failureString
519 ],
520 [
521 'uid' => $this->taskUid
522 ],
523 [
524 'serialized_executions' => Connection::PARAM_LOB,
525 ]
526 );
527 }
528 }
529
530 /**
531 * Clears all marked executions
532 *
533 * @return bool TRUE if the clearing succeeded, FALSE otherwise
534 */
535 public function unmarkAllExecutions()
536 {
537 // Set the serialized executions field to empty
538 $result = GeneralUtility::makeInstance(ConnectionPool::class)
539 ->getConnectionForTable('tx_scheduler_task')
540 ->update(
541 'tx_scheduler_task',
542 [
543 'serialized_executions' => ''
544 ],
545 [
546 'uid' => $this->taskUid
547 ],
548 [
549 'serialized_executions' => Connection::PARAM_LOB,
550 ]
551 );
552 return (bool)$result;
553 }
554
555 /**
556 * Saves the details of the task to the database.
557 *
558 * @return bool
559 */
560 public function save()
561 {
562 return $this->scheduler->saveTask($this);
563 }
564
565 /**
566 * Stops the task, by replacing the execution object by an empty one
567 * NOTE: the task still needs to be saved after that
568 */
569 public function stop()
570 {
571 $this->execution = GeneralUtility::makeInstance(Execution::class);
572 }
573
574 /**
575 * Removes the task totally from the system.
576 */
577 public function remove()
578 {
579 $this->scheduler->removeTask($this);
580 }
581
582 /**
583 * Guess task type from the existing information
584 * If an interval or a cron command is defined, it's a recurring task
585 *
586 * @return int
587 */
588 public function getType()
589 {
590 if (!empty($this->getExecution()->getInterval()) || !empty($this->getExecution()->getCronCmd())) {
591 return self::TYPE_RECURRING;
592 }
593 return self::TYPE_SINGLE;
594 }
595
596 /**
597 * @param \Exception $e
598 */
599 protected function logException(\Exception $e)
600 {
601 $this->logger->error('A Task Exception was captured: ' . $e->getMessage() . ' (' . $e->getCode() . ')', ['exception' => $e]);
602 }
603 }