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