min_interval = $GLOBALS['sugar_config']['jobs']['min_retry_interval']; } } public function check_date_relationships_load() { // Hack to work around the mess with dates being auto-converted to user format on retrieve $this->execute_time_db = $this->db->fromConvert($this->execute_time, 'datetime'); parent::check_date_relationships_load(); } /** * handleDateFormat * * This function handles returning a datetime value. It allows a user instance to be passed in, but will default to the * user member variable instance if none is found. * * @param string $date String value of the date to calculate, defaults to 'now' * @param object $user The User instance to use in calculating the time value, if empty, it will default to user member variable * @param boolean $user_format Boolean indicating whether or not to convert to user's time format, defaults to false * * @return string Formatted datetime value */ function handleDateFormat($date='now', $user=null, $user_format=false) { global $timedate; if(!isset($timedate) || empty($timedate)) { $timedate = new TimeDate(); } // get user for calculation $user = (empty($user)) ? $this->user : $user; if($date == 'now') { $dbTime = $timedate->asUser($timedate->getNow(), $user); } else { $dbTime = $timedate->asUser($timedate->fromString($date, $user), $user); } // if $user_format is set to true then just return as th user's time format, otherwise, return as database format return $user_format ? $dbTime : $timedate->fromUser($dbTime, $user)->asDb(); } /////////////////////////////////////////////////////////////////////////// //// SCHEDULERSJOB HELPER FUNCTIONS /** * This function takes a passed URL and cURLs it to fake multi-threading with another httpd instance * @param $job String in URI-clean format * @param $timeout Int value in secs for cURL to timeout. 30 default. */ public function fireUrl($job, $timeout=30) { // TODO: figure out what error is thrown when no more apache instances can be spun off // cURL inits $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $job); // set url curl_setopt($ch, CURLOPT_FAILONERROR, true); // silent failure (code >300); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // do not follow location(); inits - we always use the current curl_setopt($ch, CURLOPT_FORBID_REUSE, 1); curl_setopt($ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false); // not thread-safe curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // return into a variable to continue program execution curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); // never times out - bad idea? curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); // 5 secs for connect timeout curl_setopt($ch, CURLOPT_FRESH_CONNECT, true); // open brand new conn curl_setopt($ch, CURLOPT_HEADER, true); // do not return header info with result curl_setopt($ch, CURLOPT_NOPROGRESS, true); // do not have progress bar $urlparts = parse_url($job); if(empty($urlparts['port'])) { if($urlparts['scheme'] == 'https'){ $urlparts['port'] = 443; } else { $urlparts['port'] = 80; } } curl_setopt($ch, CURLOPT_PORT, $urlparts['port']); // set port as reported by Server //TODO make the below configurable curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // most customers will not have Certificate Authority account curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // most customers will not have Certificate Authority account curl_setopt($ch, CURLOPT_NOSIGNAL, true); // ignore any cURL signals to PHP (for multi-threading) $result = curl_exec($ch); $cInfo = curl_getinfo($ch); //url,content_type,header_size,request_size,filetime,http_code //ssl_verify_result,total_time,namelookup_time,connect_time //pretransfer_time,size_upload,size_download,speed_download, //speed_upload,download_content_length,upload_content_length //starttransfer_time,redirect_time if(curl_errno($ch)) { $this->errors .= curl_errno($ch)."\n"; } curl_close($ch); if($result !== FALSE && $cInfo['http_code'] < 400) { $GLOBALS['log']->debug("----->Firing was successful: $job"); $GLOBALS['log']->debug('----->WTIH RESULT: '.strip_tags($result).' AND '.strip_tags(print_r($cInfo, true))); return true; } else { $GLOBALS['log']->fatal("Job failed: $job"); return false; } } //// END SCHEDULERSJOB HELPER FUNCTIONS /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// //// STANDARD SUGARBEAN OVERRIDES /** * This function gets DB data and preps it for ListViews */ function get_list_view_data() { global $mod_strings; $temp_array = $this->get_list_view_array(); $temp_array['JOB_NAME'] = $this->job_name; $temp_array['JOB'] = $this->job; return $temp_array; } /** method stub for future customization * */ function fill_in_additional_list_fields() { $this->fill_in_additional_detail_fields(); } /** * Mark this job as failed * @param string $message */ public function failJob($message = null) { return $this->resolveJob(self::JOB_FAILURE, $message); } /** * Mark this job as success * @param string $message */ public function succeedJob($message = null) { return $this->resolveJob(self::JOB_SUCCESS, $message); } /** * Called if job failed but will be retried */ public function onFailureRetry() { // TODO: what we do if job fails, notify somebody? $this->call_custom_logic("job_failure_retry"); } /** * Called if job has failed and will not be retried */ public function onFinalFailure() { // TODO: what we do if job fails, notify somebody? $this->call_custom_logic("job_failure"); } /** * Resolve job as success or failure * @param string $resolution One of JOB_ constants that define job status * @param string $message * @return bool */ public function resolveJob($resolution, $message = null) { $GLOBALS['log']->info("Resolving job {$this->id} as $resolution: $message"); if($resolution == self::JOB_FAILURE) { $this->failure_count++; if($this->requeue && $this->retry_count > 0) { // retry failed job $this->status = self::JOB_STATUS_QUEUED; if($this->job_delay < $this->min_interval) { $this->job_delay = $this->min_interval; } $this->execute_time = $GLOBALS['timedate']->getNow()->modify("+{$this->job_delay} seconds")->asDb(); $this->retry_count--; $GLOBALS['log']->info("Will retry job {$this->id} at {$this->execute_time} ($this->retry_count)"); $this->onFailureRetry(); } else { // final failure $this->status = self::JOB_STATUS_DONE; $this->onFinalFailure(); } } else { $this->status = self::JOB_STATUS_DONE; } $this->addMessages($message); $this->resolution = $resolution; $this->save(); if($this->status == self::JOB_STATUS_DONE && $this->resolution == self::JOB_SUCCESS) { $this->updateSchedulerSuccess(); } return true; } /** * Update schedulers table on job success */ protected function updateSchedulerSuccess() { if(empty($this->scheduler_id)) { return; } $this->db->query("UPDATE schedulers SET last_run={$this->db->now()} WHERE id=".$this->db->quoted($this->scheduler_id)); } /** * Assemle job messages * Takes messages in $this->message, errors & $message and assembles them into $this->message * @param string $message */ protected function addMessages($message) { if(!empty($this->errors)) { $this->message .= $this->errors; $this->errors = ''; } if(!empty($message)) { $this->message .= "$message\n"; } } /** * Rerun this job again * @param string $message * @param string $delay how long to delay (default is job's delay) * @return bool */ public function postponeJob($message = null, $delay = null) { $this->status = self::JOB_STATUS_QUEUED; $this->addMessages($message); $this->resolution = self::JOB_PARTIAL; if(empty($delay)) { $delay = intval($this->job_delay); } $this->execute_time = $GLOBALS['timedate']->getNow()->modify("+$delay seconds")->asDb(); $GLOBALS['log']->info("Postponing job {$this->id} to {$this->execute_time}: $message"); $this->save(); return true; } /** * Delete a job * @see SugarBean::mark_deleted($id) */ public function mark_deleted($id) { return $this->db->query("DELETE FROM {$this->table_name} WHERE id=".$this->db->quoted($id)); } /** * Shutdown handler to be called if something breaks in the middle of the job */ public function unexpectedExit() { if(!$this->job_done) { // Job wasn't properly finished, fail it $this->resolveJob(self::JOB_FAILURE, translate('ERR_FAILED', 'SchedulersJobs')); } } /** * Run the job by ID * @param string $id * @param string $client Client that is trying to run the job * @return bool|string true on success, false on job failure, error message on failure to run */ public static function runJobId($id, $client) { $job = new self(); $job->retrieve($id); if(empty($job->id)) { $GLOBALS['log']->fatal("Job $id not found."); return "Job $id not found."; } if($job->status != self::JOB_STATUS_RUNNING) { $GLOBALS['log']->fatal("Job $id is not marked as running."); return "Job $id is not marked as running."; } if($job->client != $client) { $GLOBALS['log']->fatal("Job $id belongs to client {$job->client}, can not run as $client."); return "Job $id belongs to another client, can not run as $client."; } $job->job_done = false; register_shutdown_function(array($job, "unexpectedExit")); $res = $job->runJob(); $job->job_done = true; return $res; } /** * Error handler, assembles the error messages * @param int $errno * @param string $errstr * @param string $errfile * @param int $errline */ public function errorHandler($errno, $errstr, $errfile, $errline) { switch($errno) { case E_USER_WARNING: case E_COMPILE_WARNING: case E_CORE_WARNING: case E_WARNING: $type = "Warning"; break; case E_USER_ERROR: case E_COMPILE_ERROR: case E_CORE_ERROR: case E_ERROR: $type = "Fatal Error"; break; case E_PARSE: $type = "Parse Error"; break; case E_RECOVERABLE_ERROR: $type = "Recoverable Error"; break; default: // Ignore errors we don't know about return; } $errstr = strip_tags($errstr); $this->errors .= sprintf(translate('ERR_PHP', 'SchedulersJobs'), $type, $errno, $errstr, $errfile, $errline)."\n"; } /** * Change current user to given user * @param User $user */ protected function sudo($user) { $GLOBALS['current_user'] = $user; // Reset the session if(session_id()) { session_write_close(); } if(!headers_sent()) { session_start(); session_regenerate_id(); $_SESSION['is_valid_session']= true; $_SESSION['user_id'] = $user->id; $_SESSION['type'] = 'user'; $_SESSION['authenticated_user_id'] = $user->id; } } /** * Run this job * @return bool Was the job successful? */ public function runJob() { require_once('modules/Schedulers/_AddJobsHere.php'); $this->errors = ""; $exJob = explode('::', $this->target, 2); if($exJob[0] == 'function') { // set up the current user and drop session if(!empty($this->assigned_user_id)) { $user = new User(); $user->retrieve($this->assigned_user_id); if(empty($user->id)) { $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_NOSUCHUSER', 'SchedulersJobs'), $this->assigned_user_id)); return; } $old_user = $GLOBALS['current_user']; $this->sudo($user); } else { $this->resolveJob(self::JOB_FAILURE, translate('ERR_NOUSER', 'SchedulersJobs')); return; } $func = $exJob[1]; $GLOBALS['log']->debug("----->SchedulersJob calling function: $func"); set_error_handler(array($this, "errorHandler"), E_ALL & ~E_NOTICE & ~E_STRICT); if(!is_callable($func)) { $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_CALL', 'SchedulersJobs'), $func)); } $data = array($this); if(!empty($this->data)) { $data[] = $this->data; } $res = call_user_func_array($func, $data); restore_error_handler(); if(isset($old_user)) { $this->sudo($old_user); unset($old_user); } if($this->status == self::JOB_STATUS_RUNNING) { // nobody updated the status yet - job function could do that if($res) { $this->resolveJob(self::JOB_SUCCESS); return true; } else { $this->resolveJob(self::JOB_FAILURE); return false; } } else { return $this->resolution != self::JOB_FAILURE; } } elseif($exJob[0] == 'url') { if(function_exists('curl_init')) { $GLOBALS['log']->debug('----->SchedulersJob firing URL job: '.$exJob[1]); set_error_handler(array($this, "errorHandler"), E_ALL & ~E_NOTICE & ~E_STRICT); if($this->fireUrl($exJob[1])) { restore_error_handler(); $this->resolveJob(self::JOB_SUCCESS); return true; } else { restore_error_handler(); $this->resolveJob(self::JOB_FAILURE); return false; } } else { $this->resolveJob(self::JOB_FAILURE, translate('ERR_CURL', 'SchedulersJobs')); } } else if ($exJob[0] == 'class') { $tmpJob = new $exJob[1](); if($tmpJob instanceof RunnableSchedulerJob) { $tmpJob->setJob($this); $return_status = $tmpJob->run($this->data); if($return_status) { $this->resolveJob(self::JOB_SUCCESS); return true; } else { $this->resolveJob(self::JOB_FAILURE); return false; } } else { $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_JOBTYPE', 'SchedulersJobs'), strip_tags($this->target))); } } else { $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_JOBTYPE', 'SchedulersJobs'), strip_tags($this->target))); } return false; } } // end class Job /** * Runnable job queue job * */ interface RunnableSchedulerJob { /** * @abstract * @param SchedulersJob $job */ public function setJob(SchedulersJob $job); /** * @abstract * */ public function run($data); }