[TASK] Remove the "@todo define visibility"
[Packages/TYPO3.CMS.git] / typo3 / sysext / scheduler / Classes / Scheduler.php
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
17 /**
18 * TYPO3 Scheduler. This class handles scheduling and execution of tasks.
19 * Formerly known as "Gabriel TYPO3 arch angel"
20 *
21 * @author Fran├žois Suter <francois@typo3.org>
22 * @author Christian Jul Jensen <julle@typo3.org>
23 */
24 class Scheduler implements \TYPO3\CMS\Core\SingletonInterface {
25
26 /**
27 * @var array $extConf Settings from the extension manager
28 */
29 public $extConf = array();
30
31 /**
32 * Constructor, makes sure all derived client classes are included
33 *
34 * @return \TYPO3\CMS\Scheduler\Scheduler
35 */
36 public function __construct() {
37 // Get configuration from the extension manager
38 $this->extConf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['scheduler']);
39 if (empty($this->extConf['maxLifetime'])) {
40 $this->extConf['maxLifetime'] = 1440;
41 }
42 if (empty($this->extConf['useAtdaemon'])) {
43 $this->extConf['useAtdaemon'] = 0;
44 }
45 // Clean up the serialized execution arrays
46 $this->cleanExecutionArrays();
47 }
48
49 /**
50 * Adds a task to the pool
51 *
52 * @param \TYPO3\CMS\Scheduler\Task\AbstractTask $task The object representing the task to add
53 * @return boolean TRUE if the task was successfully added, FALSE otherwise
54 */
55 public function addTask(\TYPO3\CMS\Scheduler\Task\AbstractTask $task) {
56 $taskUid = $task->getTaskUid();
57 if (empty($taskUid)) {
58 $fields = array(
59 'crdate' => $GLOBALS['EXEC_TIME'],
60 'disable' => $task->isDisabled(),
61 'description' => $task->getDescription(),
62 'task_group' => $task->getTaskGroup(),
63 'serialized_task_object' => 'RESERVED'
64 );
65 $result = $GLOBALS['TYPO3_DB']->exec_INSERTquery('tx_scheduler_task', $fields);
66 if ($result) {
67 $task->setTaskUid($GLOBALS['TYPO3_DB']->sql_insert_id());
68 $task->save();
69 $result = TRUE;
70 } else {
71 $result = FALSE;
72 }
73 } else {
74 $result = FALSE;
75 }
76 return $result;
77 }
78
79 /**
80 * Cleans the execution lists of the scheduled tasks, executions older than 24h are removed
81 * TODO: find a way to actually kill the job
82 *
83 * @return void
84 */
85 protected function cleanExecutionArrays() {
86 $tstamp = $GLOBALS['EXEC_TIME'];
87 // Select all tasks with executions
88 // NOTE: this cleanup is done for disabled tasks too,
89 // to avoid leaving old executions lying around
90 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('uid, serialized_executions, serialized_task_object', 'tx_scheduler_task', 'serialized_executions <> \'\'');
91 $maxDuration = $this->extConf['maxLifetime'] * 60;
92 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
93 $executions = array();
94 if ($serialized_executions = unserialize($row['serialized_executions'])) {
95 foreach ($serialized_executions as $task) {
96 if ($tstamp - $task < $maxDuration) {
97 $executions[] = $task;
98 } else {
99 $task = unserialize($row['serialized_task_object']);
100 $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());
101 $this->log($logMessage);
102 }
103 }
104 }
105 if (count($serialized_executions) != count($executions)) {
106 if (count($executions) == 0) {
107 $value = '';
108 } else {
109 $value = serialize($executions);
110 }
111 $GLOBALS['TYPO3_DB']->exec_UPDATEquery('tx_scheduler_task', 'uid = ' . (int)$row['uid'], array('serialized_executions' => $value));
112 }
113 }
114 $GLOBALS['TYPO3_DB']->sql_free_result($res);
115 }
116
117 /**
118 * This method executes the given task and properly marks and records that execution
119 * It is expected to return FALSE if the task was barred from running or if it was not saved properly
120 *
121 * @param \TYPO3\CMS\Scheduler\Task\AbstractTask $task The task to execute
122 * @return boolean Whether the task was saved successfully to the database or not
123 */
124 public function executeTask(\TYPO3\CMS\Scheduler\Task\AbstractTask $task) {
125 // Trigger the saving of the task, as this will calculate its next execution time
126 // This should be calculated all the time, even if the execution is skipped
127 // (in case it is skipped, this pushes back execution to the next possible date)
128 $task->save();
129 // Set a scheduler object for the task again,
130 // as it was removed during the save operation
131 $task->setScheduler();
132 $result = TRUE;
133 // Task is already running and multiple executions are not allowed
134 if (!$task->areMultipleExecutionsAllowed() && $task->isExecutionRunning()) {
135 // Log multiple execution error
136 $logMessage = 'Task is already running and multiple executions are not allowed, skipping! Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
137 $this->log($logMessage);
138 $result = FALSE;
139 } else {
140 // Log scheduler invocation
141 $logMessage = 'Start execution. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
142 $this->log($logMessage);
143 // Register execution
144 $executionID = $task->markExecution();
145 $failure = NULL;
146 try {
147 // Execute task
148 $successfullyExecuted = $task->execute();
149 if (!$successfullyExecuted) {
150 throw new \TYPO3\CMS\Scheduler\FailedExecutionException('Task failed to execute successfully. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid(), 1250596541);
151 }
152 } catch (\Exception $e) {
153 // Store exception, so that it can be saved to database
154 $failure = $e;
155 }
156 // Un-register execution
157 $task->unmarkExecution($executionID, $failure);
158 // Log completion of execution
159 $logMessage = 'Task executed. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
160 $this->log($logMessage);
161 // Now that the result of the task execution has been handled,
162 // throw the exception again, if any
163 if ($failure instanceof \Exception) {
164 throw $failure;
165 }
166 }
167 return $result;
168 }
169
170 /**
171 * This method stores information about the last run of the Scheduler into the system registry
172 *
173 * @param string $type Type of run (manual or command-line (assumed to be cron))
174 * @return void
175 */
176 public function recordLastRun($type = 'cron') {
177 // Validate input value
178 if ($type !== 'manual' && $type !== 'cli-by-id') {
179 $type = 'cron';
180 }
181 /** @var $registry \TYPO3\CMS\Core\Registry */
182 $registry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Registry');
183 $runInformation = array('start' => $GLOBALS['EXEC_TIME'], 'end' => time(), 'type' => $type);
184 $registry->set('tx_scheduler', 'lastRun', $runInformation);
185 }
186
187 /**
188 * Removes a task completely from the system.
189 * TODO: find a way to actually kill the existing jobs
190 *
191 * @param \TYPO3\CMS\Scheduler\Task\AbstractTask $task The object representing the task to delete
192 * @return boolean TRUE if task was successfully deleted, FALSE otherwise
193 */
194 public function removeTask(\TYPO3\CMS\Scheduler\Task\AbstractTask $task) {
195 $taskUid = $task->getTaskUid();
196 if (!empty($taskUid)) {
197 $result = $GLOBALS['TYPO3_DB']->exec_DELETEquery('tx_scheduler_task', 'uid = ' . $taskUid);
198 } else {
199 $result = FALSE;
200 }
201 if ($result) {
202 $this->scheduleNextSchedulerRunUsingAtDaemon();
203 }
204 return $result;
205 }
206
207 /**
208 * Updates a task in the pool
209 *
210 * @param \TYPO3\CMS\Scheduler\Task\AbstractTask $task Scheduler task object
211 * @return boolean False if submitted task was not of proper class
212 */
213 public function saveTask(\TYPO3\CMS\Scheduler\Task\AbstractTask $task) {
214 $taskUid = $task->getTaskUid();
215 if (!empty($taskUid)) {
216 try {
217 $executionTime = $task->getNextDueExecution();
218 $task->setExecutionTime($executionTime);
219 } catch (\Exception $e) {
220 $task->setDisabled(TRUE);
221 $executionTime = 0;
222 }
223 $task->unsetScheduler();
224 $fields = array(
225 'nextexecution' => $executionTime,
226 'disable' => $task->isDisabled(),
227 'description' => $task->getDescription(),
228 'task_group' => $task->getTaskGroup(),
229 'serialized_task_object' => serialize($task)
230 );
231 $result = $GLOBALS['TYPO3_DB']->exec_UPDATEquery('tx_scheduler_task', 'uid = ' . $taskUid, $fields);
232 } else {
233 $result = FALSE;
234 }
235 if ($result) {
236 $this->scheduleNextSchedulerRunUsingAtDaemon();
237 }
238 return $result;
239 }
240
241 /**
242 * Fetches and unserializes a task object from the db. If an uid is given the object
243 * with the uid is returned, else the object representing the next due task is returned.
244 * If there are no due tasks the method throws an exception.
245 *
246 * @param integer $uid Primary key of a task
247 * @return \TYPO3\CMS\Scheduler\Task\AbstractTask The fetched task object
248 * @throws \OutOfBoundsException
249 * @throws \UnexpectedValueException
250 */
251 public function fetchTask($uid = 0) {
252 // Define where clause
253 // If no uid is given, take any non-disabled task which has a next execution time in the past
254 if (empty($uid)) {
255 $queryArray = array(
256 'SELECT' => 'tx_scheduler_task.uid AS uid, serialized_task_object',
257 'FROM' => 'tx_scheduler_task LEFT JOIN tx_scheduler_task_group ON tx_scheduler_task.task_group = tx_scheduler_task_group.uid',
258 '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)',
259 'LIMIT' => 1
260 );
261 } else {
262 $queryArray = array(
263 'SELECT' => 'uid, serialized_task_object',
264 'FROM' => 'tx_scheduler_task',
265 'WHERE' => 'uid = ' . (int)$uid,
266 'LIMIT' => 1
267 );
268 }
269
270 $res = $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($queryArray);
271 // If there are no available tasks, thrown an exception
272 if ($GLOBALS['TYPO3_DB']->sql_num_rows($res) == 0) {
273 throw new \OutOfBoundsException('No task', 1247827244);
274 } else {
275 $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
276 /** @var $task \TYPO3\CMS\Scheduler\Task\AbstractTask */
277 $task = unserialize($row['serialized_task_object']);
278 if ($this->isValidTaskObject($task)) {
279 // The task is valid, return it
280 $task->setScheduler();
281 } else {
282 // Forcibly set the disable flag to 1 in the database,
283 // so that the task does not come up again and again for execution
284 $GLOBALS['TYPO3_DB']->exec_UPDATEquery('tx_scheduler_task', 'uid = ' . $row['uid'], array('disable' => 1));
285 // Throw an exception to raise the problem
286 throw new \UnexpectedValueException('Could not unserialize task', 1255083671);
287 }
288 $GLOBALS['TYPO3_DB']->sql_free_result($res);
289 }
290 return $task;
291 }
292
293 /**
294 * This method is used to get the database record for a given task
295 * It returns the database record and not the task object
296 *
297 * @param integer $uid Primary key of the task to get
298 * @return array Database record for the task
299 * @see \TYPO3\CMS\Scheduler\Scheduler::fetchTask()
300 * @throws \OutOfBoundsException
301 */
302 public function fetchTaskRecord($uid) {
303 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'tx_scheduler_task', 'uid = ' . (int)$uid);
304 // If the task is not found, throw an exception
305 if ($GLOBALS['TYPO3_DB']->sql_num_rows($res) == 0) {
306 throw new \OutOfBoundsException('No task', 1247827245);
307 } else {
308 $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
309 $GLOBALS['TYPO3_DB']->sql_free_result($res);
310 }
311 return $row;
312 }
313
314 /**
315 * Fetches and unserializes task objects selected with some (SQL) condition
316 * Objects are returned as an array
317 *
318 * @param string $where Part of a SQL where clause (without the "WHERE" keyword)
319 * @param boolean $includeDisabledTasks TRUE if disabled tasks should be fetched too, FALSE otherwise
320 * @return array List of task objects
321 */
322 public function fetchTasksWithCondition($where, $includeDisabledTasks = FALSE) {
323 $whereClause = '';
324 $tasks = array();
325 if (!empty($where)) {
326 $whereClause = $where;
327 }
328 if (!$includeDisabledTasks) {
329 if (!empty($whereClause)) {
330 $whereClause .= ' AND ';
331 }
332 $whereClause .= 'disable = 0';
333 }
334 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('serialized_task_object', 'tx_scheduler_task', $whereClause);
335 if ($res) {
336 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
337 /** @var $task Task */
338 $task = unserialize($row['serialized_task_object']);
339 // Add the task to the list only if it is valid
340 if ($this->isValidTaskObject($task)) {
341 $task->setScheduler();
342 $tasks[] = $task;
343 }
344 }
345 $GLOBALS['TYPO3_DB']->sql_free_result($res);
346 }
347 return $tasks;
348 }
349
350 /**
351 * This method encapsulates a very simple test for the purpose of clarity.
352 * Registered tasks are stored in the database along with a serialized task object.
353 * When a registered task is fetched, its object is unserialized.
354 * At that point, if the class corresponding to the object is not available anymore
355 * (e.g. because the extension providing it has been uninstalled),
356 * the unserialization will produce an incomplete object.
357 * This test checks whether the unserialized object is of the right (parent) class or not.
358 *
359 * @param object $task The object to test
360 * @return boolean TRUE if object is a task, FALSE otherwise
361 */
362 public function isValidTaskObject($task) {
363 return $task instanceof \TYPO3\CMS\Scheduler\Task\AbstractTask;
364 }
365
366 /**
367 * This is a utility method that writes some message to the BE Log
368 * It could be expanded to write to some other log
369 *
370 * @param string $message The message to write to the log
371 * @param integer $status Status (0 = message, 1 = error)
372 * @param mixed $code Key for the message
373 * @return void
374 */
375 public function log($message, $status = 0, $code = 'scheduler') {
376 // Log only if enabled
377 if (!empty($this->extConf['enableBELog'])) {
378 $GLOBALS['BE_USER']->writelog(4, 0, $status, $code, '[scheduler]: ' . $message, array());
379 }
380 }
381
382 /**
383 * Schedule the next run of scheduler
384 * For the moment only the "at"-daemon is used, and only if it is enabled
385 *
386 * @return boolean Successfully scheduled next execution using "at"-daemon
387 * @see tx_scheduler::fetchTask()
388 */
389 public function scheduleNextSchedulerRunUsingAtDaemon() {
390 if ((int)$this->extConf['useAtdaemon'] !== 1) {
391 return FALSE;
392 }
393 /** @var $registry \TYPO3\CMS\Core\Registry */
394 $registry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Registry');
395 // Get at job id from registry and remove at job
396 $atJobId = $registry->get('tx_scheduler', 'atJobId');
397 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($atJobId)) {
398 shell_exec('atrm ' . (int)$atJobId . ' 2>&1');
399 }
400 // Can not use fetchTask() here because if tasks have just executed
401 // they are not in the list of next executions
402 $tasks = $this->fetchTasksWithCondition('');
403 $nextExecution = FALSE;
404 foreach ($tasks as $task) {
405 try {
406 /** @var $task \TYPO3\CMS\Scheduler\Task\AbstractTask */
407 $tempNextExecution = $task->getNextDueExecution();
408 if ($nextExecution === FALSE || $tempNextExecution < $nextExecution) {
409 $nextExecution = $tempNextExecution;
410 }
411 } catch (\OutOfBoundsException $e) {
412 // The event will not be executed again or has already ended - we don't have to consider it for
413 // scheduling the next "at" run
414 }
415 }
416 if ($nextExecution !== FALSE) {
417 if ($nextExecution > $GLOBALS['EXEC_TIME']) {
418 $startTime = strftime('%H:%M %F', $nextExecution);
419 } else {
420 $startTime = 'now+1minute';
421 }
422 $cliDispatchPath = PATH_site . 'typo3/cli_dispatch.phpsh';
423 $currentLocale = setlocale(LC_CTYPE, 0);
424 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']) {
425 setlocale(LC_CTYPE, $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale']);
426 }
427 $cmd = 'echo ' . escapeshellarg($cliDispatchPath) . ' scheduler | at ' . escapeshellarg($startTime) . ' 2>&1';
428 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']) {
429 setlocale(LC_CTYPE, $currentLocale);
430 }
431 $output = shell_exec($cmd);
432 $outputParts = '';
433 foreach (explode(LF, $output) as $outputLine) {
434 if (\TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($outputLine, 'job')) {
435 $outputParts = explode(' ', $outputLine, 3);
436 break;
437 }
438 }
439 if ($outputParts[0] === 'job' && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($outputParts[1])) {
440 $atJobId = (int)$outputParts[1];
441 $registry->set('tx_scheduler', 'atJobId', $atJobId);
442 }
443 }
444 return TRUE;
445 }
446
447 }