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 var $user; // User object
87 var $scheduler; // Scheduler parent
88 public $min_interval = 30; // minimal interval for job reruns
89 protected $job_done = true;
94 function SchedulersJob()
97 if(!empty($GLOBALS['sugar_config']['jobs']['min_retry_interval'])) {
98 $this->min_interval = $GLOBALS['sugar_config']['jobs']['min_retry_interval'];
102 public function check_date_relationships_load()
104 // Hack to work around the mess with dates being auto-converted to user format on retrieve
105 $this->execute_time_db = $this->db->fromConvert($this->execute_time, 'datetime');
106 parent::check_date_relationships_load();
111 * This function handles returning a datetime value. It allows a user instance to be passed in, but will default to the
112 * user member variable instance if none is found.
114 * @param string $date String value of the date to calculate, defaults to 'now'
115 * @param object $user The User instance to use in calculating the time value, if empty, it will default to user member variable
116 * @param boolean $user_format Boolean indicating whether or not to convert to user's time format, defaults to false
118 * @return string Formatted datetime value
120 function handleDateFormat($date='now', $user=null, $user_format=false) {
123 if(!isset($timedate) || empty($timedate))
125 $timedate = new TimeDate();
128 // get user for calculation
129 $user = (empty($user)) ? $this->user : $user;
133 $dbTime = $timedate->asUser($timedate->getNow(), $user);
135 $dbTime = $timedate->asUser($timedate->fromString($date, $user), $user);
138 // if $user_format is set to true then just return as th user's time format, otherwise, return as database format
139 return $user_format ? $dbTime : $timedate->fromUser($dbTime, $user)->asDb();
143 ///////////////////////////////////////////////////////////////////////////
144 //// SCHEDULERSJOB HELPER FUNCTIONS
147 * This function takes a passed URL and cURLs it to fake multi-threading with another httpd instance
148 * @param $job String in URI-clean format
149 * @param $timeout Int value in secs for cURL to timeout. 30 default.
151 public function fireUrl($job, $timeout=30)
153 // TODO: figure out what error is thrown when no more apache instances can be spun off
156 curl_setopt($ch, CURLOPT_URL, $job); // set url
157 curl_setopt($ch, CURLOPT_FAILONERROR, true); // silent failure (code >300);
158 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // do not follow location(); inits - we always use the current
159 curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
160 curl_setopt($ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false); // not thread-safe
161 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // return into a variable to continue program execution
162 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); // never times out - bad idea?
163 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); // 5 secs for connect timeout
164 curl_setopt($ch, CURLOPT_FRESH_CONNECT, true); // open brand new conn
165 curl_setopt($ch, CURLOPT_HEADER, true); // do not return header info with result
166 curl_setopt($ch, CURLOPT_NOPROGRESS, true); // do not have progress bar
167 $urlparts = parse_url($job);
168 if(empty($urlparts['port'])) {
169 if($urlparts['scheme'] == 'https'){
170 $urlparts['port'] = 443;
172 $urlparts['port'] = 80;
175 curl_setopt($ch, CURLOPT_PORT, $urlparts['port']); // set port as reported by Server
176 //TODO make the below configurable
177 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // most customers will not have Certificate Authority account
178 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // most customers will not have Certificate Authority account
180 curl_setopt($ch, CURLOPT_NOSIGNAL, true); // ignore any cURL signals to PHP (for multi-threading)
181 $result = curl_exec($ch);
182 $cInfo = curl_getinfo($ch); //url,content_type,header_size,request_size,filetime,http_code
183 //ssl_verify_result,total_time,namelookup_time,connect_time
184 //pretransfer_time,size_upload,size_download,speed_download,
185 //speed_upload,download_content_length,upload_content_length
186 //starttransfer_time,redirect_time
187 if(curl_errno($ch)) {
188 $this->errors .= curl_errno($ch)."\n";
192 if($result !== FALSE && $cInfo['http_code'] < 400) {
193 $GLOBALS['log']->debug("----->Firing was successful: $job");
194 $GLOBALS['log']->debug('----->WTIH RESULT: '.strip_tags($result).' AND '.strip_tags(print_r($cInfo, true)));
197 $GLOBALS['log']->fatal("Job failed: $job");
201 //// END SCHEDULERSJOB HELPER FUNCTIONS
202 ///////////////////////////////////////////////////////////////////////////
205 ///////////////////////////////////////////////////////////////////////////
206 //// STANDARD SUGARBEAN OVERRIDES
208 * This function gets DB data and preps it for ListViews
210 function get_list_view_data()
214 $temp_array = $this->get_list_view_array();
215 $temp_array['JOB_NAME'] = $this->job_name;
216 $temp_array['JOB'] = $this->job;
221 /** method stub for future customization
224 function fill_in_additional_list_fields()
226 $this->fill_in_additional_detail_fields();
231 * Mark this job as failed
232 * @param string $message
234 public function failJob($message = null)
236 return $this->resolveJob(self::JOB_FAILURE, $message);
240 * Mark this job as success
241 * @param string $message
243 public function succeedJob($message = null)
245 return $this->resolveJob(self::JOB_SUCCESS, $message);
249 * Called if job failed but will be retried
251 public function onFailureRetry()
253 // TODO: what we do if job fails, notify somebody?
254 $this->call_custom_logic("job_failure_retry");
258 * Called if job has failed and will not be retried
260 public function onFinalFailure()
262 // TODO: what we do if job fails, notify somebody?
263 $this->call_custom_logic("job_failure");
267 * Resolve job as success or failure
268 * @param string $resolution One of JOB_ constants that define job status
269 * @param string $message
272 public function resolveJob($resolution, $message = null)
274 $GLOBALS['log']->info("Resolving job {$this->id} as $resolution: $message");
275 if($resolution == self::JOB_FAILURE) {
276 $this->failure_count++;
277 if($this->requeue && $this->retry_count > 0) {
279 $this->status = self::JOB_STATUS_QUEUED;
280 if($this->job_delay < $this->min_interval) {
281 $this->job_delay = $this->min_interval;
283 $this->execute_time = $GLOBALS['timedate']->getNow()->modify("+{$this->job_delay} seconds")->asDb();
284 $this->retry_count--;
285 $GLOBALS['log']->info("Will retry job {$this->id} at {$this->execute_time} ($this->retry_count)");
286 $this->onFailureRetry();
289 $this->status = self::JOB_STATUS_DONE;
290 $this->onFinalFailure();
293 $this->status = self::JOB_STATUS_DONE;
295 $this->addMessages($message);
296 $this->resolution = $resolution;
298 if($this->status == self::JOB_STATUS_DONE && $this->resolution == self::JOB_SUCCESS) {
299 $this->updateSchedulerSuccess();
305 * Update schedulers table on job success
307 protected function updateSchedulerSuccess()
309 if(empty($this->scheduler_id)) {
312 $this->db->query("UPDATE schedulers SET last_run={$this->db->now()} WHERE id=".$this->db->quoted($this->scheduler_id));
316 * Assemle job messages
317 * Takes messages in $this->message, errors & $message and assembles them into $this->message
318 * @param string $message
320 protected function addMessages($message)
322 if(!empty($this->errors)) {
323 $this->message .= $this->errors;
326 if(!empty($message)) {
327 $this->message .= "$message\n";
332 * Rerun this job again
333 * @param string $message
334 * @param string $delay how long to delay (default is job's delay)
337 public function postponeJob($message = null, $delay = null)
339 $this->status = self::JOB_STATUS_QUEUED;
340 $this->addMessages($message);
341 $this->resolution = self::JOB_PARTIAL;
343 $delay = intval($this->job_delay);
345 $this->execute_time = $GLOBALS['timedate']->getNow()->modify("+$delay seconds")->asDb();
346 $GLOBALS['log']->info("Postponing job {$this->id} to {$this->execute_time}: $message");
354 * @see SugarBean::mark_deleted($id)
356 public function mark_deleted($id)
358 return $this->db->query("DELETE FROM {$this->table_name} WHERE id=".$this->db->quoted($id));
362 * Shutdown handler to be called if something breaks in the middle of the job
364 public function unexpectedExit()
366 if(!$this->job_done) {
367 // Job wasn't properly finished, fail it
368 $this->resolveJob(self::JOB_FAILURE, translate('ERR_FAILED', 'SchedulersJobs'));
375 * @param string $client Client that is trying to run the job
376 * @return bool|string true on success, false on job failure, error message on failure to run
378 public static function runJobId($id, $client)
382 if(empty($job->id)) {
383 $GLOBALS['log']->fatal("Job $id not found.");
384 return "Job $id not found.";
386 if($job->status != self::JOB_STATUS_RUNNING) {
387 $GLOBALS['log']->fatal("Job $id is not marked as running.");
388 return "Job $id is not marked as running.";
390 if($job->client != $client) {
391 $GLOBALS['log']->fatal("Job $id belongs to client {$job->client}, can not run as $client.");
392 return "Job $id belongs to another client, can not run as $client.";
394 $job->job_done = false;
395 register_shutdown_function(array($job, "unexpectedExit"));
396 $res = $job->runJob();
397 $job->job_done = true;
402 * Error handler, assembles the error messages
404 * @param string $errstr
405 * @param string $errfile
406 * @param int $errline
408 public function errorHandler($errno, $errstr, $errfile, $errline)
413 case E_COMPILE_WARNING:
419 case E_COMPILE_ERROR:
422 $type = "Fatal Error";
425 $type = "Parse Error";
427 case E_RECOVERABLE_ERROR:
428 $type = "Recoverable Error";
431 // Ignore errors we don't know about
434 $errstr = strip_tags($errstr);
435 $this->errors .= sprintf(translate('ERR_PHP', 'SchedulersJobs'), $type, $errno, $errstr, $errfile, $errline)."\n";
439 * Change current user to given user
442 protected function sudo($user)
444 $GLOBALS['current_user'] = $user;
447 session_write_close();
449 if(!headers_sent()) {
451 session_regenerate_id();
452 $_SESSION['is_valid_session']= true;
453 $_SESSION['user_id'] = $user->id;
454 $_SESSION['type'] = 'user';
455 $_SESSION['authenticated_user_id'] = $user->id;
461 * @return bool Was the job successful?
463 public function runJob()
465 require_once('modules/Schedulers/_AddJobsHere.php');
467 $exJob = explode('::', $this->target, 2);
468 if($exJob[0] == 'function') {
469 // set up the current user and drop session
470 if(!empty($this->assigned_user_id)) {
472 $user->retrieve($this->assigned_user_id);
473 if(empty($user->id)) {
474 $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_NOSUCHUSER', 'SchedulersJobs'), $this->assigned_user_id));
477 $old_user = $GLOBALS['current_user'];
480 $this->resolveJob(self::JOB_FAILURE, translate('ERR_NOUSER', 'SchedulersJobs'));
484 $GLOBALS['log']->debug("----->SchedulersJob calling function: $func");
485 set_error_handler(array($this, "errorHandler"), E_ALL & ~E_NOTICE & ~E_STRICT);
486 if(!is_callable($func)) {
487 $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_CALL', 'SchedulersJobs'), $func));
489 $data = array($this);
490 if(!empty($this->data)) {
491 $data[] = $this->data;
493 $res = call_user_func_array($func, $data);
494 restore_error_handler();
495 if(isset($old_user)) {
496 $this->sudo($old_user);
499 if($this->status == self::JOB_STATUS_RUNNING) {
500 // nobody updated the status yet - job function could do that
502 $this->resolveJob(self::JOB_SUCCESS);
505 $this->resolveJob(self::JOB_FAILURE);
509 return $this->resolution != self::JOB_FAILURE;
512 elseif($exJob[0] == 'url')
514 if(function_exists('curl_init')) {
515 $GLOBALS['log']->debug('----->SchedulersJob firing URL job: '.$exJob[1]);
516 set_error_handler(array($this, "errorHandler"), E_ALL & ~E_NOTICE & ~E_STRICT);
517 if($this->fireUrl($exJob[1])) {
518 restore_error_handler();
519 $this->resolveJob(self::JOB_SUCCESS);
522 restore_error_handler();
523 $this->resolveJob(self::JOB_FAILURE);
527 $this->resolveJob(self::JOB_FAILURE, translate('ERR_CURL', 'SchedulersJobs'));
530 else if ($exJob[0] == 'class')
532 $tmpJob = new $exJob[1]();
533 if($tmpJob instanceof RunnableSchedulerJob)
535 $tmpJob->setJob($this);
536 $return_status = $tmpJob->run($this->data);
538 $this->resolveJob(self::JOB_SUCCESS);
541 $this->resolveJob(self::JOB_FAILURE);
546 $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_JOBTYPE', 'SchedulersJobs'), strip_tags($this->target)));
550 $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_JOBTYPE', 'SchedulersJobs'), strip_tags($this->target)));
558 * Runnable job queue job
561 interface RunnableSchedulerJob
565 * @param SchedulersJob $job
567 public function setJob(SchedulersJob $job);
573 public function run($data);