2 if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
3 /*********************************************************************************
4 * SugarCRM Community Edition is a customer relationship management program developed by
5 * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc.
7 * This program is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU Affero General Public License version 3 as published by the
9 * Free Software Foundation with the addition of the following permission added
10 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
11 * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
12 * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
14 * This program is distributed in the hope that it will be useful, but WITHOUT
15 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
19 * You should have received a copy of the GNU Affero General Public License along with
20 * this program; if not, see http://www.gnu.org/licenses or write to the Free
21 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
24 * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
25 * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
27 * The interactive user interfaces in modified source and object code versions
28 * of this program must display Appropriate Legal Notices, as required under
29 * Section 5 of the GNU Affero General Public License version 3.
31 * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
32 * these Appropriate Legal Notices must retain the display of the "Powered by
33 * SugarCRM" logo. If the display of the logo is not reasonably feasible for
34 * technical reasons, the Appropriate Legal Notices must display the words
35 * "Powered by SugarCRM".
36 ********************************************************************************/
43 class SchedulersJob extends Basic
45 const JOB_STATUS_QUEUED = 'queued';
46 const JOB_STATUS_RUNNING = 'running';
47 const JOB_STATUS_DONE = 'done';
49 const JOB_PENDING = 'queued';
50 const JOB_PARTIAL = 'partial';
51 const JOB_SUCCESS = 'success';
52 const JOB_FAILURE = 'failure';
59 public $date_modified;
61 public $execute_time; // when to execute
65 public $target; // URL or function name
66 public $data; // Data set
67 public $requeue; // Requeue on failure?
69 public $failure_count;
70 public $job_delay=0; // Frequency to run it
71 public $assigned_user_id; // User under which the task is running
72 public $client; // Client ID that owns this job
73 public $execute_time_db;
74 public $percent_complete; // how much of the job is done
76 // standard SugarBean child attrs
77 var $table_name = "job_queue";
78 var $object_name = "SchedulersJob";
79 var $module_dir = "SchedulersJobs";
80 var $new_schema = true;
81 var $process_save_dates = true;
83 var $job_name; // the Scheduler's 'name' field
84 var $job; // the Scheduler's 'job' field
85 // object specific attributes
86 public $user; // User object
87 var $scheduler; // Scheduler parent
88 public $min_interval = 30; // minimal interval for job reruns
89 protected $job_done = true;
95 function SchedulersJob()
98 if(!empty($GLOBALS['sugar_config']['jobs']['min_retry_interval'])) {
99 $this->min_interval = $GLOBALS['sugar_config']['jobs']['min_retry_interval'];
103 public function check_date_relationships_load()
105 // Hack to work around the mess with dates being auto-converted to user format on retrieve
106 $this->execute_time_db = $this->db->fromConvert($this->execute_time, 'datetime');
107 parent::check_date_relationships_load();
112 * This function handles returning a datetime value. It allows a user instance to be passed in, but will default to the
113 * user member variable instance if none is found.
115 * @param string $date String value of the date to calculate, defaults to 'now'
116 * @param object $user The User instance to use in calculating the time value, if empty, it will default to user member variable
117 * @param boolean $user_format Boolean indicating whether or not to convert to user's time format, defaults to false
119 * @return string Formatted datetime value
121 function handleDateFormat($date='now', $user=null, $user_format=false) {
124 if(!isset($timedate) || empty($timedate))
126 $timedate = new TimeDate();
129 // get user for calculation
130 $user = (empty($user)) ? $this->user : $user;
134 $dbTime = $timedate->asUser($timedate->getNow(), $user);
136 $dbTime = $timedate->asUser($timedate->fromString($date, $user), $user);
139 // if $user_format is set to true then just return as th user's time format, otherwise, return as database format
140 return $user_format ? $dbTime : $timedate->fromUser($dbTime, $user)->asDb();
144 ///////////////////////////////////////////////////////////////////////////
145 //// SCHEDULERSJOB HELPER FUNCTIONS
148 * This function takes a passed URL and cURLs it to fake multi-threading with another httpd instance
149 * @param $job String in URI-clean format
150 * @param $timeout Int value in secs for cURL to timeout. 30 default.
152 public function fireUrl($job, $timeout=30)
154 // TODO: figure out what error is thrown when no more apache instances can be spun off
157 curl_setopt($ch, CURLOPT_URL, $job); // set url
158 curl_setopt($ch, CURLOPT_FAILONERROR, true); // silent failure (code >300);
159 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // do not follow location(); inits - we always use the current
160 curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
161 curl_setopt($ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false); // not thread-safe
162 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // return into a variable to continue program execution
163 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); // never times out - bad idea?
164 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); // 5 secs for connect timeout
165 curl_setopt($ch, CURLOPT_FRESH_CONNECT, true); // open brand new conn
166 curl_setopt($ch, CURLOPT_HEADER, true); // do not return header info with result
167 curl_setopt($ch, CURLOPT_NOPROGRESS, true); // do not have progress bar
168 $urlparts = parse_url($job);
169 if(empty($urlparts['port'])) {
170 if($urlparts['scheme'] == 'https'){
171 $urlparts['port'] = 443;
173 $urlparts['port'] = 80;
176 curl_setopt($ch, CURLOPT_PORT, $urlparts['port']); // set port as reported by Server
177 //TODO make the below configurable
178 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // most customers will not have Certificate Authority account
179 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // most customers will not have Certificate Authority account
181 curl_setopt($ch, CURLOPT_NOSIGNAL, true); // ignore any cURL signals to PHP (for multi-threading)
182 $result = curl_exec($ch);
183 $cInfo = curl_getinfo($ch); //url,content_type,header_size,request_size,filetime,http_code
184 //ssl_verify_result,total_time,namelookup_time,connect_time
185 //pretransfer_time,size_upload,size_download,speed_download,
186 //speed_upload,download_content_length,upload_content_length
187 //starttransfer_time,redirect_time
188 if(curl_errno($ch)) {
189 $this->errors .= curl_errno($ch)."\n";
193 if($result !== FALSE && $cInfo['http_code'] < 400) {
194 $GLOBALS['log']->debug("----->Firing was successful: $job");
195 $GLOBALS['log']->debug('----->WTIH RESULT: '.strip_tags($result).' AND '.strip_tags(print_r($cInfo, true)));
198 $GLOBALS['log']->fatal("Job failed: $job");
202 //// END SCHEDULERSJOB HELPER FUNCTIONS
203 ///////////////////////////////////////////////////////////////////////////
206 ///////////////////////////////////////////////////////////////////////////
207 //// STANDARD SUGARBEAN OVERRIDES
209 * This function gets DB data and preps it for ListViews
211 function get_list_view_data()
215 $temp_array = $this->get_list_view_array();
216 $temp_array['JOB_NAME'] = $this->job_name;
217 $temp_array['JOB'] = $this->job;
222 /** method stub for future customization
225 function fill_in_additional_list_fields()
227 $this->fill_in_additional_detail_fields();
232 * Mark this job as failed
233 * @param string $message
235 public function failJob($message = null)
237 return $this->resolveJob(self::JOB_FAILURE, $message);
241 * Mark this job as success
242 * @param string $message
244 public function succeedJob($message = null)
246 return $this->resolveJob(self::JOB_SUCCESS, $message);
250 * Called if job failed but will be retried
252 public function onFailureRetry()
254 // TODO: what we do if job fails, notify somebody?
255 $this->call_custom_logic("job_failure_retry");
259 * Called if job has failed and will not be retried
261 public function onFinalFailure()
263 // TODO: what we do if job fails, notify somebody?
264 $this->call_custom_logic("job_failure");
268 * Resolve job as success or failure
269 * @param string $resolution One of JOB_ constants that define job status
270 * @param string $message
273 public function resolveJob($resolution, $message = null)
275 $GLOBALS['log']->info("Resolving job {$this->id} as $resolution: $message");
276 if($resolution == self::JOB_FAILURE) {
277 $this->failure_count++;
278 if($this->requeue && $this->retry_count > 0) {
280 $this->status = self::JOB_STATUS_QUEUED;
281 if($this->job_delay < $this->min_interval) {
282 $this->job_delay = $this->min_interval;
284 $this->execute_time = $GLOBALS['timedate']->getNow()->modify("+{$this->job_delay} seconds")->asDb();
285 $this->retry_count--;
286 $GLOBALS['log']->info("Will retry job {$this->id} at {$this->execute_time} ($this->retry_count)");
287 $this->onFailureRetry();
290 $this->status = self::JOB_STATUS_DONE;
291 $this->onFinalFailure();
294 $this->status = self::JOB_STATUS_DONE;
296 $this->addMessages($message);
297 $this->resolution = $resolution;
299 if($this->status == self::JOB_STATUS_DONE && $this->resolution == self::JOB_SUCCESS) {
300 $this->updateSchedulerSuccess();
306 * Update schedulers table on job success
308 protected function updateSchedulerSuccess()
310 if(empty($this->scheduler_id)) {
313 $this->db->query("UPDATE schedulers SET last_run={$this->db->now()} WHERE id=".$this->db->quoted($this->scheduler_id));
317 * Assemle job messages
318 * Takes messages in $this->message, errors & $message and assembles them into $this->message
319 * @param string $message
321 protected function addMessages($message)
323 if(!empty($this->errors)) {
324 $this->message .= $this->errors;
327 if(!empty($message)) {
328 $this->message .= "$message\n";
333 * Rerun this job again
334 * @param string $message
335 * @param string $delay how long to delay (default is job's delay)
338 public function postponeJob($message = null, $delay = null)
340 $this->status = self::JOB_STATUS_QUEUED;
341 $this->addMessages($message);
342 $this->resolution = self::JOB_PARTIAL;
344 $delay = intval($this->job_delay);
346 $this->execute_time = $GLOBALS['timedate']->getNow()->modify("+$delay seconds")->asDb();
347 $GLOBALS['log']->info("Postponing job {$this->id} to {$this->execute_time}: $message");
355 * @see SugarBean::mark_deleted($id)
357 public function mark_deleted($id)
359 return $this->db->query("DELETE FROM {$this->table_name} WHERE id=".$this->db->quoted($id));
363 * Shutdown handler to be called if something breaks in the middle of the job
365 public function unexpectedExit()
367 if(!$this->job_done) {
368 // Job wasn't properly finished, fail it
369 $this->resolveJob(self::JOB_FAILURE, translate('ERR_FAILED', 'SchedulersJobs'));
376 * @param string $client Client that is trying to run the job
377 * @return bool|string true on success, false on job failure, error message on failure to run
379 public static function runJobId($id, $client)
383 if(empty($job->id)) {
384 $GLOBALS['log']->fatal("Job $id not found.");
385 return "Job $id not found.";
387 if($job->status != self::JOB_STATUS_RUNNING) {
388 $GLOBALS['log']->fatal("Job $id is not marked as running.");
389 return "Job $id is not marked as running.";
391 if($job->client != $client) {
392 $GLOBALS['log']->fatal("Job $id belongs to client {$job->client}, can not run as $client.");
393 return "Job $id belongs to another client, can not run as $client.";
395 $job->job_done = false;
396 register_shutdown_function(array($job, "unexpectedExit"));
397 $res = $job->runJob();
398 $job->job_done = true;
403 * Error handler, assembles the error messages
405 * @param string $errstr
406 * @param string $errfile
407 * @param int $errline
409 public function errorHandler($errno, $errstr, $errfile, $errline)
414 case E_COMPILE_WARNING:
420 case E_COMPILE_ERROR:
423 $type = "Fatal Error";
426 $type = "Parse Error";
428 case E_RECOVERABLE_ERROR:
429 $type = "Recoverable Error";
432 // Ignore errors we don't know about
435 $errstr = strip_tags($errstr);
436 $this->errors .= sprintf(translate('ERR_PHP', 'SchedulersJobs'), $type, $errno, $errstr, $errfile, $errline)."\n";
440 * Change current user to given user
443 protected function sudo($user)
445 $GLOBALS['current_user'] = $user;
450 if(!headers_sent()) {
452 session_regenerate_id();
454 $_SESSION['is_valid_session']= true;
455 $_SESSION['user_id'] = $user->id;
456 $_SESSION['type'] = 'user';
457 $_SESSION['authenticated_user_id'] = $user->id;
461 * Set environment to the user of this job
464 protected function setJobUser()
466 // set up the current user and drop session
467 if(!empty($this->assigned_user_id)) {
468 $this->old_user = $GLOBALS['current_user'];
469 if(empty($this->user->id) || $this->assigned_user_id != $this->user->id) {
470 $this->user = BeanFactory::getBean('Users', $this->assigned_user_id);
471 if(empty($this->user->id)) {
472 $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_NOSUCHUSER', 'SchedulersJobs'), $this->assigned_user_id));
476 $this->sudo($this->user);
478 $this->resolveJob(self::JOB_FAILURE, translate('ERR_NOUSER', 'SchedulersJobs'));
485 * Restore previous user environment
487 protected function restoreJobUser()
489 if(!empty($this->old_user->id) && $this->old_user->id != $this->user->id) {
490 $this->sudo($this->old_user);
496 * @return bool Was the job successful?
498 public function runJob()
500 require_once('modules/Schedulers/_AddJobsHere.php');
503 $exJob = explode('::', $this->target, 2);
504 if($exJob[0] == 'function') {
505 // set up the current user and drop session
506 if(!$this->setJobUser()) {
510 $GLOBALS['log']->debug("----->SchedulersJob calling function: $func");
511 set_error_handler(array($this, "errorHandler"), E_ALL & ~E_NOTICE & ~E_STRICT);
512 if(!is_callable($func)) {
513 $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_CALL', 'SchedulersJobs'), $func));
515 $data = array($this);
516 if(!empty($this->data)) {
517 $data[] = $this->data;
519 $res = call_user_func_array($func, $data);
520 restore_error_handler();
521 $this->restoreJobUser();
522 if($this->status == self::JOB_STATUS_RUNNING) {
523 // nobody updated the status yet - job function could do that
525 $this->resolveJob(self::JOB_SUCCESS);
528 $this->resolveJob(self::JOB_FAILURE);
532 return $this->resolution != self::JOB_FAILURE;
534 } elseif($exJob[0] == 'url') {
535 if(function_exists('curl_init')) {
536 $GLOBALS['log']->debug('----->SchedulersJob firing URL job: '.$exJob[1]);
537 set_error_handler(array($this, "errorHandler"), E_ALL & ~E_NOTICE & ~E_STRICT);
538 if($this->fireUrl($exJob[1])) {
539 restore_error_handler();
540 $this->resolveJob(self::JOB_SUCCESS);
543 restore_error_handler();
544 $this->resolveJob(self::JOB_FAILURE);
548 $this->resolveJob(self::JOB_FAILURE, translate('ERR_CURL', 'SchedulersJobs'));
550 } elseif ($exJob[0] == 'class') {
551 $tmpJob = new $exJob[1]();
552 if($tmpJob instanceof RunnableSchedulerJob)
554 // set up the current user and drop session
555 if(!$this->setJobUser()) {
558 $tmpJob->setJob($this);
559 $result = $tmpJob->run($this->data);
560 $this->restoreJobUser();
561 if ($this->status == self::JOB_STATUS_RUNNING) {
562 // nobody updated the status yet - job class could do that
564 $this->resolveJob(self::JOB_SUCCESS);
567 $this->resolveJob(self::JOB_FAILURE);
571 return $this->resolution != self::JOB_FAILURE;
575 $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_JOBTYPE', 'SchedulersJobs'), strip_tags($this->target)));
579 $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_JOBTYPE', 'SchedulersJobs'), strip_tags($this->target)));
587 * Runnable job queue job
590 interface RunnableSchedulerJob
594 * @param SchedulersJob $job
596 public function setJob(SchedulersJob $job);
602 public function run($data);