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