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