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