2 /***************************************************************
5 * (c) 2005 Christian Jul Jensen <julle@typo3.org>
9 * This script is part of the TYPO3 project. The TYPO3 project is
10 * free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
15 * The GNU General Public License can be found at
16 * http://www.gnu.org/copyleft/gpl.html.
18 * This script is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
23 * This copyright notice MUST APPEAR in all copies of the script!
24 ***************************************************************/
26 * TYPO3 Scheduler. This class handles scheduling and execution of tasks.
27 * Formerly known as "Gabriel TYPO3 arch angel"
29 * @author Francois Suter <francois@typo3.org>
30 * @author Christian Jul Jensen <julle@typo3.org>
33 * @subpackage tx_scheduler
35 * $Id: class.tx_scheduler.php 1198 2009-09-06 21:11:11Z francois $
38 class tx_scheduler
implements t3lib_Singleton
{
40 * @var array $extConf: settings from the extension manager
42 var $extConf = array();
45 * Constructor, makes sure all derived client classes are included
49 public function __construct() {
50 // Get configuration from the extension manager
51 $this->extConf
= unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['scheduler']);
52 if (empty($this->extConf
['maxLifetime'])) {
53 $this->extConf
['maxLifetime'] = 1440;
56 // Clean up the serialized execution arrays
57 $this->cleanExecutionArrays();
61 * Adds a task to the pool
63 * @param tx_scheduler_Task $task: the object representing the task to add
64 * @param string $identifier: the identified of the task
65 * @return boolean True if the task was successfully added, false otherwise
67 public function addTask(tx_scheduler_Task
$task) {
68 $taskUid = $task->getTaskUid();
69 if (empty($taskUid)) {
71 'crdate' => $GLOBALS['EXEC_TIME'],
72 'classname' => get_class($task),
73 'disable' => $task->isDisabled(),
74 'serialized_task_object' => 'RESERVED'
76 $result = $GLOBALS['TYPO3_DB']->exec_INSERTquery('tx_scheduler_task', $fields);
78 $task->setTaskUid($GLOBALS['TYPO3_DB']->sql_insert_id());
91 * Cleans the execution lists of the scheduled tasks, executions older than 24h are removed
92 * TODO: find a way to actually kill the job
96 protected function cleanExecutionArrays() {
97 $tstamp = $GLOBALS['EXEC_TIME'];
99 // Select all tasks with executions
100 // NOTE: this cleanup is done for disabled tasks too,
101 // to avoid leaving old executions lying around
102 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
103 'uid, classname, serialized_executions',
105 'serialized_executions != \'\''
108 $maxDuration = $this->extConf
['maxLifetime'] * 60;
109 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
110 if (($serialized_executions = unserialize($row['serialized_executions']))) {
111 $executions = array();
112 foreach ($serialized_executions AS $task) {
113 if (($tstamp - $task) < $maxDuration) {
114 $executions[] = $task;
116 $logMessage = 'Removing logged execution, assuming that the process is dead. Execution of \'' . $row['classname'] . '\' (UID: ' . $row['uid']. ') was started at '.date('Y-m-d H:i:s', $task);
117 $this->log($logMessage);
122 if (count($serialized_executions) != count($executions)) {
123 if (count($executions) == 0) {
126 $value = serialize($executions);
129 $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
131 'uid = ' . intval($row['uid']),
132 array('serialized_executions' => $value)
136 $GLOBALS['TYPO3_DB']->sql_free_result($res);
140 * This method executes the given task and properly marks and records that execution
141 * It is expected to return false if the task was barred from running or if it was not saved properly
143 * @param tx_scheduler_Task $task: the task to execute
144 * @return boolean Whether the task was saved succesfully to the database or not
146 public function executeTask(tx_scheduler_Task
$task) {
147 // Trigger the saving of the task, as this will calculate its next execution time
148 // This should be calculated all the time, even if the execution is skipped
149 // (in case it is skipped, this pushes back execution to the next possible date)
151 // Set a scheduler object for the task again,
152 // as it was removed during the save operation
153 $task->setScheduler();
156 // Task is already running and multiple executions are not allowed
157 if (!$task->areMultipleExecutionsAllowed() && $task->isExecutionRunning()) {
158 // Log multiple execution error
159 $logMessage = 'Task is already running and multiple executions are not allowed, skipping! Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
160 $this->log($logMessage);
164 // Task isn't running or multiple executions are allowed
166 // Log scheduler invocation
167 $logMessage = 'Start execution. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
168 $this->log($logMessage);
170 // Register execution
171 $executionID = $task->markExecution();
176 $successfullyExecuted = $task->execute();
178 if (!$successfullyExecuted) {
179 throw new tx_scheduler_FailedExecutionException(
180 'Task failed to execute successfully. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid(),
184 } catch(Exception
$e) {
185 // Store exception, so that it can be saved to database
189 // Unregister excution
190 $task->unmarkExecution($executionID, $failure);
192 // Log completion of execution
193 $logMessage = 'Task executed. Class: ' . get_class($task). ', UID: ' . $task->getTaskUid();
194 $this->log($logMessage);
196 // Now that the result of the task execution has been handled,
197 // throw the exception again, if any
198 if ($failure instanceof Exception
) {
207 * This method stores information about the last run of the Scheduler into the system registry
209 * @param string $type: Type of run (manual or command-line (assumed to be cron))
212 public function recordLastRun($type = 'cron') {
213 // Validate input value
214 if ($type != 'manual') {
219 * @var t3lib_Registry
221 $registry = t3lib_div
::makeInstance('t3lib_Registry');
222 $runInformation = array('start' => $GLOBALS['EXEC_TIME'], 'end' => time(), 'type' => $type);
223 $registry->set('tx_scheduler', 'lastRun', $runInformation);
227 * Removes a task completely from the system.
228 * TODO: find a way to actually kill the existing jobs
230 * @param tx_scheduler_Task $task: the object representing the task to delete
231 * @return boolean True if task was successfully deleted, false otherwise
233 public function removeTask(tx_scheduler_Task
$task) {
234 $taskUid = $task->getTaskUid();
235 if (!empty($taskUid)) {
236 return $GLOBALS['TYPO3_DB']->exec_DELETEquery('tx_scheduler_task', 'uid = ' . $taskUid);
243 * Updates a task in the pool
245 * @param tx_scheduler_Task $task: Scheduler task object
246 * @return boolean False if submitted task was not of proper class
248 public function saveTask(tx_scheduler_Task
$task) {
249 $taskUid = $task->getTaskUid();
250 if (!empty($taskUid)) {
252 $executionTime = $task->getNextDueExecution();
253 $task->setExecutionTime($executionTime);
255 catch (Exception
$e) {
256 $task->setDisabled(true);
259 $task->unsetScheduler();
261 'nextexecution' => $executionTime,
262 'classname' => get_class($task),
263 'disable' => $task->isDisabled(),
264 'serialized_task_object' => serialize($task)
266 return $GLOBALS['TYPO3_DB']->exec_UPDATEquery('tx_scheduler_task', "uid = '" . $taskUid . "'", $fields);
273 * Fetches and unserializes a task object from the db. If an uid is given the object
274 * with the uid is returned, else the object representing the next due task is returned.
275 * If there are no due tasks the method throws an exception.
277 * @param integer $uid: primary key of a task
278 * @return tx_scheduler_Task The fetched task object
280 public function fetchTask($uid = 0) {
282 // Define where clause
283 // If no uid is given, take any non-disabled task which has a next execution time in the past
285 $whereClause = 'disable = 0 AND nextexecution != 0 AND nextexecution <= ' . $GLOBALS['EXEC_TIME'];
287 $whereClause = 'uid = ' . intval($uid);
291 'SELECT' => 'uid, serialized_task_object',
292 'FROM' => 'tx_scheduler_task',
293 'WHERE' => $whereClause,
296 $res = $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($queryArray);
298 // If there are no available tasks, thrown an exception
299 if ($GLOBALS['TYPO3_DB']->sql_num_rows($res) == 0) {
300 throw new OutOfBoundsException('No task', 1247827244);
302 // Otherwise unserialize the task and return it
304 $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
305 $task = unserialize($row['serialized_task_object']);
307 if ($this->isValidTaskObject($task)) {
308 // The task is valid, return it
310 $task->setScheduler();
313 // Forcibly set the disable flag to 1 in the database,
314 // so that the task does not come up again and again for execution
316 $GLOBALS['TYPO3_DB']->exec_UPDATEquery('tx_scheduler_task', "uid = '" . $row['uid'] . "'", array('disable' => 1));
317 // Throw an exception to raise the problem
318 throw new UnexpectedValueException('Could not unserialize task', 1255083671);
320 $GLOBALS['TYPO3_DB']->sql_free_result($res);
326 * This method is used to get the database record for a given task
327 * It returns the database record and not the task object
329 * @param integer $uid: primary key of the task to get
330 * @return array Database record for the task
331 * @see tx_scheduler::fetchTask()
333 public function fetchTaskRecord($uid) {
334 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'tx_scheduler_task', "uid = '" . intval($uid) . "'");
336 // If the task is not found, throw an exception
337 if ($GLOBALS['TYPO3_DB']->sql_num_rows($res) == 0) {
338 throw new OutOfBoundsException('No task', 1247827244);
340 // Otherwise get the task's record
342 $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
343 $GLOBALS['TYPO3_DB']->sql_free_result($res);
349 * Fetches and unserializes task objects selected with some (SQL) condition
350 * Objects are returned as an array
352 * @param string $where: part of a SQL where clause (without the "WHERE" keyword)
353 * @param boolean $includeDisabledTasks: true if disabled tasks should be fetched too, false otherwise
354 * @return array List of task objects
356 public function fetchTasksWithCondition($where, $includeDisabledTasks = false) {
358 if (!empty($where)) {
359 $whereClause = $where;
361 if (!$includeDisabledTasks) {
362 if (!empty($whereClause)) {
363 $whereClause .= ' AND ';
365 $whereClause .= 'disable = 0';
367 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('serialized_task_object', 'tx_scheduler_task', $whereClause);
369 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
370 $task = unserialize($row['serialized_task_object']);
371 // Add the task to the list only if it is valid
372 if ($this->isValidTaskObject($task)) {
373 $task->setScheduler();
377 $GLOBALS['TYPO3_DB']->sql_free_result($res);
383 * This method encapsulates a very simple test for the purpose of clarity.
384 * Registered tasks are stored in the database along with a serialized task object.
385 * When a registered task is fetched, its object is unserialized.
386 * At that point, if the class corresponding to the object is not available anymore
387 * (e.g. because the extension providing it has been uninstalled),
388 * the unserialization will produce an incomplete object.
389 * This test checks whether the unserialized object is of the right (parent) class or not.
391 * @param object The object to test
392 * @return boolean True if object is a task, false otherwise
394 public function isValidTaskObject($task) {
395 return $task instanceof tx_scheduler_Task
;
399 * This is a utility method that writes some message to the BE Log
400 * It could be expanded to write to some other log
402 * @param string The message to write to the log
403 * @param integer Status (0 = message, 1 = error)
404 * @param mixed Key for the message
407 public function log($message, $status = 0, $code = 'scheduler') {
408 // Log only if enabled
409 if ($this->extConf
['enableBELog']) {
410 $GLOBALS['BE_USER']->writelog(
415 '[scheduler]: ' . $message,
422 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE
]['XCLASS']['ext/scheduler/class.tx_scheduler.php']) {
423 include_once($TYPO3_CONF_VARS[TYPO3_MODE
]['XCLASS']['ext/scheduler/class.tx_scheduler.php']);