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