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-2012 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();
110 ///////////////////////////////////////////////////////////////////////////
111 //// SCHEDULERSJOB HELPER FUNCTIONS
114 * This function takes a passed URL and cURLs it to fake multi-threading with another httpd instance
115 * @param $job String in URI-clean format
116 * @param $timeout Int value in secs for cURL to timeout. 30 default.
118 public function fireUrl($job, $timeout=30)
120 // TODO: figure out what error is thrown when no more apache instances can be spun off
123 curl_setopt($ch, CURLOPT_URL, $job); // set url
124 curl_setopt($ch, CURLOPT_FAILONERROR, true); // silent failure (code >300);
125 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // do not follow location(); inits - we always use the current
126 curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
127 curl_setopt($ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false); // not thread-safe
128 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // return into a variable to continue program execution
129 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); // never times out - bad idea?
130 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); // 5 secs for connect timeout
131 curl_setopt($ch, CURLOPT_FRESH_CONNECT, true); // open brand new conn
132 curl_setopt($ch, CURLOPT_HEADER, true); // do not return header info with result
133 curl_setopt($ch, CURLOPT_NOPROGRESS, true); // do not have progress bar
134 $urlparts = parse_url($job);
135 if(empty($urlparts['port'])) {
136 if($urlparts['scheme'] == 'https'){
137 $urlparts['port'] = 443;
139 $urlparts['port'] = 80;
142 curl_setopt($ch, CURLOPT_PORT, $urlparts['port']); // set port as reported by Server
143 //TODO make the below configurable
144 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // most customers will not have Certificate Authority account
145 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // most customers will not have Certificate Authority account
147 curl_setopt($ch, CURLOPT_NOSIGNAL, true); // ignore any cURL signals to PHP (for multi-threading)
148 $result = curl_exec($ch);
149 $cInfo = curl_getinfo($ch); //url,content_type,header_size,request_size,filetime,http_code
150 //ssl_verify_result,total_time,namelookup_time,connect_time
151 //pretransfer_time,size_upload,size_download,speed_download,
152 //speed_upload,download_content_length,upload_content_length
153 //starttransfer_time,redirect_time
154 if(curl_errno($ch)) {
155 $this->errors .= curl_errno($ch)."\n";
159 if($result !== FALSE && $cInfo['http_code'] < 400) {
160 $GLOBALS['log']->debug("----->Firing was successful: $job");
161 $GLOBALS['log']->debug('----->WTIH RESULT: '.strip_tags($result).' AND '.strip_tags(print_r($cInfo, true)));
164 $GLOBALS['log']->fatal("Job failed: $job");
168 //// END SCHEDULERSJOB HELPER FUNCTIONS
169 ///////////////////////////////////////////////////////////////////////////
172 ///////////////////////////////////////////////////////////////////////////
173 //// STANDARD SUGARBEAN OVERRIDES
175 * This function gets DB data and preps it for ListViews
177 function get_list_view_data()
181 $temp_array = $this->get_list_view_array();
182 $temp_array['JOB_NAME'] = $this->job_name;
183 $temp_array['JOB'] = $this->job;
188 /** method stub for future customization
191 function fill_in_additional_list_fields()
193 $this->fill_in_additional_detail_fields();
198 * Mark this job as failed
199 * @param string $message
201 public function failJob($message = null)
203 return $this->resolveJob(self::JOB_FAILURE, $message);
207 * Mark this job as success
208 * @param string $message
210 public function succeedJob($message = null)
212 return $this->resolveJob(self::JOB_SUCCESS, $message);
216 * Called if job failed but will be retried
218 public function onFailureRetry()
220 // TODO: what we do if job fails, notify somebody?
221 $this->call_custom_logic("job_failure_retry");
225 * Called if job has failed and will not be retried
227 public function onFinalFailure()
229 // TODO: what we do if job fails, notify somebody?
230 $this->call_custom_logic("job_failure");
234 * Resolve job as success or failure
235 * @param string $resolution One of JOB_ constants that define job status
236 * @param string $message
239 public function resolveJob($resolution, $message = null)
241 $GLOBALS['log']->info("Resolving job {$this->id} as $resolution: $message");
242 if($resolution == self::JOB_FAILURE) {
243 $this->failure_count++;
244 if($this->requeue && $this->retry_count > 0) {
246 $this->status = self::JOB_STATUS_QUEUED;
247 if($this->job_delay < $this->min_interval) {
248 $this->job_delay = $this->min_interval;
250 $this->execute_time = $GLOBALS['timedate']->getNow()->modify("+{$this->job_delay} seconds")->asDb();
251 $this->retry_count--;
252 $GLOBALS['log']->info("Will retry job {$this->id} at {$this->execute_time} ($this->retry_count)");
253 $this->onFailureRetry();
256 $this->status = self::JOB_STATUS_DONE;
257 $this->onFinalFailure();
260 $this->status = self::JOB_STATUS_DONE;
262 $this->addMessages($message);
263 $this->resolution = $resolution;
269 * Assemle job messages
270 * Takes messages in $this->message, errors & $message and assembles them into $this->message
271 * @param string $message
273 protected function addMessages($message)
275 if(!empty($this->errors)) {
276 $this->message .= $this->errors;
279 if(!empty($message)) {
280 $this->message .= "$message\n";
285 * Rerun this job again
286 * @param string $message
287 * @param string $delay how long to delay (default is job's delay)
290 public function postponeJob($message = null, $delay = null)
292 $this->status = self::JOB_STATUS_QUEUED;
293 $this->addMessages($message);
294 $this->resolution = self::JOB_PARTIAL;
296 $delay = intval($this->job_delay);
298 $this->execute_time = $GLOBALS['timedate']->getNow()->modify("+$delay seconds")->asDb();
299 $GLOBALS['log']->info("Postponing job {$this->id} to {$this->execute_time}: $message");
307 * @see SugarBean::mark_deleted($id)
309 public function mark_deleted($id)
311 return $this->db->query("DELETE FROM {$this->table_name} WHERE id=".$this->db->quoted($id));
315 * Shutdown handler to be called if something breaks in the middle of the job
317 public function unexpectedExit()
319 if(!$this->job_done) {
320 // Job wasn't properly finished, fail it
321 $this->resolveJob(self::JOB_FAILURE, translate('ERR_FAILED', 'SchedulersJobs'));
328 * @param string $client Client that is trying to run the job
329 * @return bool|string true on success, false on job failure, error message on failure to run
331 public static function runJobId($id, $client)
335 if(empty($job->id)) {
336 $GLOBALS['log']->fatal("Job $id not found.");
337 return "Job $id not found.";
339 if($job->status != self::JOB_STATUS_RUNNING) {
340 $GLOBALS['log']->fatal("Job $id is not marked as running.");
341 return "Job $id is not marked as running.";
343 if($job->client != $client) {
344 $GLOBALS['log']->fatal("Job $id belongs to client {$job->client}, can not run as $client.");
345 return "Job $id belongs to another client, can not run as $client.";
347 $job->job_done = false;
348 register_shutdown_function(array($job, "unexpectedExit"));
349 $res = $job->runJob();
350 $job->job_done = true;
355 * Error handler, assembles the error messages
357 * @param string $errstr
358 * @param string $errfile
359 * @param int $errline
361 public function errorHandler($errno, $errstr, $errfile, $errline)
366 case E_COMPILE_WARNING:
372 case E_COMPILE_ERROR:
375 $type = "Fatal Error";
378 $type = "Parse Error";
380 case E_RECOVERABLE_ERROR:
381 $type = "Recoverable Error";
384 // Ignore errors we don't know about
387 $errstr = strip_tags($errstr);
388 $this->errors .= sprintf(translate('ERR_PHP', 'SchedulersJobs'), $type, $errno, $errstr, $errfile, $errline)."\n";
392 * Change current user to given user
395 protected function sudo($user)
397 $GLOBALS['current_user'] = $user;
400 session_write_close();
402 if(!headers_sent()) {
404 session_regenerate_id();
405 $_SESSION['is_valid_session']= true;
406 $_SESSION['user_id'] = $user->id;
407 $_SESSION['type'] = 'user';
408 $_SESSION['authenticated_user_id'] = $user->id;
414 * @return bool Was the job successful?
416 public function runJob()
419 $exJob = explode('::', $this->target, 2);
420 if($exJob[0] == 'function') {
421 // set up the current user and drop session
422 if(!empty($this->assigned_user_id)) {
424 $user->retrieve($this->assigned_user_id);
425 if(empty($user->id)) {
426 $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_NOSUCHUSER', 'SchedulersJobs'), $this->assigned_user_id));
429 $old_user = $GLOBALS['current_user'];
432 $this->resolveJob(self::JOB_FAILURE, translate('ERR_NOUSER', 'SchedulersJobs'));
435 require_once('modules/Schedulers/_AddJobsHere.php');
437 $GLOBALS['log']->debug("----->SchedulersJob calling function: $func");
438 set_error_handler(array($this, "errorHandler"), E_ALL & ~E_NOTICE & ~E_STRICT);
439 if(!is_callable($func)) {
440 $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_CALL', 'SchedulersJobs'), $func));
442 $data = array($this);
443 if(!empty($this->data)) {
444 $data[] = $this->data;
446 $res = call_user_func_array($func, $data);
447 restore_error_handler();
448 if(isset($old_user)) {
449 $this->sudo($old_user);
452 if($this->status == self::JOB_STATUS_RUNNING) {
453 // nobody updated the status yet - job function could do that
455 $this->resolveJob(self::JOB_SUCCESS);
458 $this->resolveJob(self::JOB_FAILURE);
462 return $this->resolution != self::JOB_FAILURE;
465 elseif($exJob[0] == 'url')
467 if(function_exists('curl_init')) {
468 $GLOBALS['log']->debug('----->SchedulersJob firing URL job: '.$exJob[1]);
469 set_error_handler(array($this, "errorHandler"), E_ALL & ~E_NOTICE & ~E_STRICT);
470 if($this->fireUrl($exJob[1])) {
471 restore_error_handler();
472 $this->resolveJob(self::JOB_SUCCESS);
475 restore_error_handler();
476 $this->resolveJob(self::JOB_FAILURE);
480 $this->resolveJob(self::JOB_FAILURE, translate('ERR_CURL', 'SchedulersJobs'));
483 else if ($exJob[0] == 'class')
485 $tmpJob = new $exJob[1]();
486 if($tmpJob instanceof RunnableSchedulerJob)
488 $tmpJob->setJob($this);
489 return $tmpJob->run($this->data);
492 $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_JOBTYPE', 'SchedulersJobs'), strip_tags($this->target)));
496 $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_JOBTYPE', 'SchedulersJobs'), strip_tags($this->target)));
504 * Runnable job queue job
507 interface RunnableSchedulerJob
511 * @param SchedulersJob $job
513 public function setJob(SchedulersJob $job);
519 public function run($data);