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