]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - modules/SchedulersJobs/SchedulersJob.php
Release 6.5.0beta5
[Github/sugarcrm.git] / modules / SchedulersJobs / SchedulersJob.php
1 <?php
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.
6  * 
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.
13  * 
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
17  * details.
18  * 
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
22  * 02110-1301 USA.
23  * 
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.
26  * 
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.
30  * 
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  ********************************************************************************/
37
38
39 /**
40  * Job queue job
41  * @api
42  */
43 class SchedulersJob extends Basic
44 {
45     const JOB_STATUS_QUEUED = 'queued';
46     const JOB_STATUS_RUNNING = 'running';
47     const JOB_STATUS_DONE = 'done';
48
49     const JOB_PENDING = 'queued';
50     const JOB_PARTIAL = 'partial';
51     const JOB_SUCCESS = 'success';
52     const JOB_FAILURE = 'failure';
53
54     // schema attributes
55         public $id;
56         public $name;
57         public $deleted;
58         public $date_entered;
59         public $date_modified;
60         public $scheduler_id;
61         public $execute_time; // when to execute
62     public $status;
63     public $resolution;
64     public $message;
65         public $target; // URL or function name
66     public $data; // Data set
67     public $requeue; // Requeue on failure?
68     public $retry_count;
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
75
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;
82         // related fields
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;
90
91         /**
92          * Job constructor.
93          */
94         function SchedulersJob()
95         {
96         parent::Basic();
97         if(!empty($GLOBALS['sugar_config']['jobs']['min_retry_interval'])) {
98             $this->min_interval = $GLOBALS['sugar_config']['jobs']['min_retry_interval'];
99         }
100         }
101
102         public function check_date_relationships_load()
103         {
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();
107         }
108
109
110         ///////////////////////////////////////////////////////////////////////////
111         ////    SCHEDULERSJOB HELPER FUNCTIONS
112
113         /**
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.
117          */
118         public function fireUrl($job, $timeout=30)
119         {
120         // TODO: figure out what error is thrown when no more apache instances can be spun off
121             // cURL inits
122                 $ch = curl_init();
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;
138                         } else {
139                                 $urlparts['port'] = 80;
140                         }
141                 }
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
146
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";
156                 }
157                 curl_close($ch);
158
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)));
162                         return true;
163                 } else {
164                         $GLOBALS['log']->fatal("Job failed: $job");
165                         return false;
166                 }
167         }
168         ////    END SCHEDULERSJOB HELPER FUNCTIONS
169         ///////////////////////////////////////////////////////////////////////////
170
171
172         ///////////////////////////////////////////////////////////////////////////
173         ////    STANDARD SUGARBEAN OVERRIDES
174         /**
175          * This function gets DB data and preps it for ListViews
176          */
177         function get_list_view_data()
178         {
179                 global $mod_strings;
180
181                 $temp_array = $this->get_list_view_array();
182                 $temp_array['JOB_NAME'] = $this->job_name;
183                 $temp_array['JOB']              = $this->job;
184
185         return $temp_array;
186         }
187
188         /** method stub for future customization
189          *
190          */
191         function fill_in_additional_list_fields()
192         {
193                 $this->fill_in_additional_detail_fields();
194         }
195
196
197         /**
198          * Mark this job as failed
199          * @param string $message
200          */
201     public function failJob($message = null)
202     {
203         return $this->resolveJob(self::JOB_FAILURE, $message);
204     }
205
206         /**
207          * Mark this job as success
208          * @param string $message
209          */
210     public function succeedJob($message = null)
211     {
212         return $this->resolveJob(self::JOB_SUCCESS, $message);
213     }
214
215     /**
216      * Called if job failed but will be retried
217      */
218     public function onFailureRetry()
219     {
220         // TODO: what we do if job fails, notify somebody?
221         $this->call_custom_logic("job_failure_retry");
222     }
223
224     /**
225      * Called if job has failed and will not be retried
226      */
227     public function onFinalFailure()
228     {
229         // TODO: what we do if job fails, notify somebody?
230         $this->call_custom_logic("job_failure");
231     }
232
233     /**
234      * Resolve job as success or failure
235      * @param string $resolution One of JOB_ constants that define job status
236      * @param string $message
237      * @return bool
238      */
239     public function resolveJob($resolution, $message = null)
240     {
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) {
245                 // retry failed job
246                 $this->status = self::JOB_STATUS_QUEUED;
247                 if($this->job_delay < $this->min_interval) {
248                     $this->job_delay = $this->min_interval;
249                 }
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();
254             } else {
255                 // final failure
256                 $this->status = self::JOB_STATUS_DONE;
257                 $this->onFinalFailure();
258             }
259         } else {
260             $this->status = self::JOB_STATUS_DONE;
261         }
262         $this->addMessages($message);
263         $this->resolution = $resolution;
264         $this->save();
265         return true;
266     }
267
268     /**
269      * Assemle job messages
270      * Takes messages in $this->message, errors & $message and assembles them into $this->message
271      * @param string $message
272      */
273     protected function addMessages($message)
274     {
275         if(!empty($this->errors)) {
276             $this->message .= $this->errors;
277             $this->errors = '';
278         }
279         if(!empty($message)) {
280             $this->message .= "$message\n";
281         }
282     }
283
284     /**
285      * Rerun this job again
286      * @param string $message
287      * @param string $delay how long to delay (default is job's delay)
288      * @return bool
289      */
290     public function postponeJob($message = null, $delay = null)
291     {
292         $this->status = self::JOB_STATUS_QUEUED;
293         $this->addMessages($message);
294         $this->resolution = self::JOB_PARTIAL;
295         if(empty($delay)) {
296             $delay = intval($this->job_delay);
297         }
298         $this->execute_time = $GLOBALS['timedate']->getNow()->modify("+$delay seconds")->asDb();
299         $GLOBALS['log']->info("Postponing job {$this->id} to {$this->execute_time}: $message");
300
301         $this->save();
302         return true;
303     }
304
305     /**
306      * Delete a job
307      * @see SugarBean::mark_deleted($id)
308      */
309     public function mark_deleted($id)
310     {
311         return $this->db->query("DELETE FROM {$this->table_name} WHERE id=".$this->db->quoted($id));
312     }
313
314     /**
315      * Shutdown handler to be called if something breaks in the middle of the job
316      */
317     public function unexpectedExit()
318     {
319         if(!$this->job_done) {
320             // Job wasn't properly finished, fail it
321             $this->resolveJob(self::JOB_FAILURE, translate('ERR_FAILED', 'SchedulersJobs'));
322         }
323     }
324
325     /**
326      * Run the job by ID
327      * @param string $id
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
330      */
331     public static function runJobId($id, $client)
332     {
333         $job = new self();
334         $job->retrieve($id);
335         if(empty($job->id)) {
336             $GLOBALS['log']->fatal("Job $id not found.");
337             return "Job $id not found.";
338         }
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.";
342         }
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.";
346         }
347         $job->job_done = false;
348         register_shutdown_function(array($job, "unexpectedExit"));
349         $res = $job->runJob();
350         $job->job_done = true;
351         return $res;
352     }
353
354     /**
355      * Error handler, assembles the error messages
356      * @param int $errno
357      * @param string $errstr
358      * @param string $errfile
359      * @param int $errline
360      */
361     public function errorHandler($errno, $errstr, $errfile, $errline)
362     {
363         switch($errno)
364         {
365                 case E_USER_WARNING:
366                 case E_COMPILE_WARNING:
367                 case E_CORE_WARNING:
368                 case E_WARNING:
369                         $type = "Warning";
370                         break;
371                 case E_USER_ERROR:
372                 case E_COMPILE_ERROR:
373                 case E_CORE_ERROR:
374                 case E_ERROR:
375                         $type = "Fatal Error";
376                         break;
377                 case E_PARSE:
378                         $type = "Parse Error";
379                         break;
380                 case E_RECOVERABLE_ERROR:
381                         $type = "Recoverable Error";
382                         break;
383                     default:
384                         // Ignore errors we don't know about
385                         return;
386         }
387         $errstr = strip_tags($errstr);
388         $this->errors .= sprintf(translate('ERR_PHP', 'SchedulersJobs'), $type, $errno, $errstr, $errfile, $errline)."\n";
389     }
390
391     /**
392      * Change current user to given user
393      * @param User $user
394      */
395     protected function sudo($user)
396     {
397         $GLOBALS['current_user'] = $user;
398         // Reset the session
399         if(session_id()) {
400             session_write_close();
401         }
402         if(!headers_sent()) {
403                 session_start();
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;
409         }
410     }
411
412     /**
413      * Run this job
414      * @return bool Was the job successful?
415      */
416     public function runJob()
417     {
418         $this->errors = "";
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)) {
423                 $user = new User();
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));
427                     return;
428                 }
429                 $old_user = $GLOBALS['current_user'];
430                 $this->sudo($user);
431             } else {
432                 $this->resolveJob(self::JOB_FAILURE, translate('ERR_NOUSER', 'SchedulersJobs'));
433                 return;
434             }
435                 require_once('modules/Schedulers/_AddJobsHere.php');
436                 $func = $exJob[1];
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));
441                         }
442                         $data = array($this);
443                         if(!empty($this->data)) {
444                             $data[] = $this->data;
445                         }
446             $res = call_user_func_array($func, $data);
447             restore_error_handler();
448             if(isset($old_user)) {
449                 $this->sudo($old_user);
450                 unset($old_user);
451             }
452                         if($this->status == self::JOB_STATUS_RUNNING) {
453                             // nobody updated the status yet - job function could do that
454                         if($res) {
455                             $this->resolveJob(self::JOB_SUCCESS);
456                                 return true;
457                         } else {
458                             $this->resolveJob(self::JOB_FAILURE);
459                             return false;
460                         }
461                         } else {
462                             return $this->resolution != self::JOB_FAILURE;
463                         }
464                 }
465         elseif($exJob[0] == 'url')
466         {
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);
473                                         return true;
474                                 } else {
475                     restore_error_handler();
476                                     $this->resolveJob(self::JOB_FAILURE);
477                                         return false;
478                                 }
479                         } else {
480                             $this->resolveJob(self::JOB_FAILURE, translate('ERR_CURL', 'SchedulersJobs'));
481                         }
482                 }
483         else if ($exJob[0] == 'class')
484         {
485             $tmpJob = new $exJob[1]();
486             if($tmpJob instanceof RunnableSchedulerJob)
487             {
488                 $tmpJob->setJob($this);
489                 return $tmpJob->run($this->data);
490             }
491             else {
492                 $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_JOBTYPE', 'SchedulersJobs'), strip_tags($this->target)));
493             }
494         }
495         else {
496                     $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_JOBTYPE', 'SchedulersJobs'), strip_tags($this->target)));
497                 }
498                 return false;
499     }
500
501 }  // end class Job
502
503 /**
504  * Runnable job queue job
505  *
506  */
507 interface RunnableSchedulerJob
508 {
509     /**
510      * @abstract
511      * @param SchedulersJob $job
512      */
513     public function setJob(SchedulersJob $job);
514
515     /**
516      * @abstract
517      *
518      */
519     public function run($data);
520 }