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-2011 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 ********************************************************************************/
41 class SchedulerDaemon extends Scheduler {
49 // replicated SugarBean attributes
52 var $object_name = 'SchedulerDaemon';
53 var $table_name = 'schedulers_times';
55 var $watch_name; // this object's watch name
56 var $sleepInterval = 5; // how long to sleep before checking on jobs
57 var $lifespan = 45; // 2 mins to kill off this object
58 var $sessId; // admin PHPSESSID
59 var $runAsUserName; // admin user
60 var $runAsUserPassword; // admin pword
63 var $shutdown = false;
69 function SchedulerDaemon () {
70 if(empty($this->db)) {
72 $this->db = DBManagerFactory::getInstance();
75 $GLOBALS['log']->debug('New Scheduler Instantiated.....................................');
85 * This function takes a look at the schedulers_times table and pulls the
86 * jobs to be run at this moment in time (anything not run and with a run
87 * time or earlier than right now)
88 * @return $successful Boolean flag whether a job(s) is found
90 function checkPendingJobs() {
94 $GLOBALS['log']->debug('');
95 $GLOBALS['log']->debug('----->Scheduler checking for qualified jobs to run.');
96 if(empty($this->db)) {
97 $this->db = DBManagerFactory::getInstance();
100 $fireTimeMinus = $timedate->asDb($timedate->getNow()->get('-1 minute'));
101 $fireTimePlus = $timedate->asDb($timedate->getNow()->get('+1 minute'));
103 // collapse list of schedulers where "catch_up" is 0 and status is "ready" (not "in progress, completed, etc.");
104 if($sugar_config['dbconfig']['db_type'] == 'oci8') {
106 $q = 'UPDATE schedulers_times st LEFT JOIN schedulers s ON st.scheduler_id = s.id SET st.status = \'not run\' WHERE st.execute_time < '.db_convert('\''.$fireTimeMinus.'\'', 'datetime').' AND st.status = \'ready\' AND s.catch_up = 0';
108 $this->db->query($q);
110 $q = 'SELECT DISTINCT st.id, st.scheduler_id, st.status, s.name, s.job FROM schedulers_times st LEFT JOIN schedulers s ON st.scheduler_id = s.id WHERE st.execute_time < '.db_convert('\''.$fireTimePlus.'\'', 'datetime').' AND st.deleted=0 AND s.deleted=0 AND st.status=\'ready\' AND s.status=\'Active\' ORDER BY s.name';
111 $r = $this->db->query($q);
114 if($sugar_config['dbconfig']['db_type'] == 'mysql') {
115 $loopCount = $this->db->getRowCount($r);
116 $GLOBALS['log']->debug('----->Scheduler has '.$loopCount.' jobs to fire.');
119 while($a = $this->db->fetchByAssoc($r)) {
121 $job = new SchedulersJob();
123 $paramJob = $a['scheduler_id'];
124 $job->fire($sugar_config['site_url'].'/index.php?entryPoint=schedulers&type=job&job_id='.$paramJob.'&record='.$a['id']);
130 $GLOBALS['log']->debug('----->Scheduler has found 0 Jobs to fire');
135 * This function takes a Scheduler object and uses its job_interval
136 * attribute to derive DB-standard datetime strings, as many as are
137 * qualified by its ranges. The times are from the time of calling the
140 * @param $focus Scheduler object
141 * @return $dateTimes array loaded with DB datetime strings derived from
142 * the job_interval attribute
143 * @return false If we the Scheduler is not in scope, return false.
145 function deriveDBDateTimes($focus) {
147 $GLOBALS['log']->debug('deriveDBDateTimes got an object of type: '.$focus->object_name);
148 /* [min][hr][dates][mon][days] */
149 $dateTimes = array();
150 $ints = explode('::', str_replace(' ','',$focus->job_interval));
156 $today = getdate($timedate->getNow()->ts);
161 $GLOBALS['log']->debug('got * day');
163 } elseif(strstr($days, '*/')) {
164 // the "*/x" format is nonsensical for this field
165 // do basically nothing.
166 $theDay = str_replace('*/','',$days);
167 $dayName[] = str_replace($focus->dayInt, $focus->dayLabel, $theDay);
168 } elseif($days != '*') { // got particular day(s)
169 if(strstr($days, ',')) {
170 $exDays = explode(',',$days);
171 foreach($exDays as $k1 => $dayGroup) {
172 if(strstr($dayGroup,'-')) {
173 $exDayGroup = explode('-', $dayGroup); // build up range and iterate through
174 for($i=$exDayGroup[0];$i<=$exDayGroup[1];$i++) {
175 $dayName[] = str_replace($focus->dayInt, $focus->dayLabel, $i);
177 } else { // individuals
178 $dayName[] = str_replace($focus->dayInt, $focus->dayLabel, $dayGroup);
181 } elseif(strstr($days, '-')) {
182 $exDayGroup = explode('-', $days); // build up range and iterate through
183 for($i=$exDayGroup[0];$i<=$exDayGroup[1];$i++) {
184 $dayName[] = str_replace($focus->dayInt, $focus->dayLabel, $i);
187 $dayName[] = str_replace($focus->dayInt, $focus->dayLabel, $days);
190 // check the day to be in scope:
191 if(!in_array($today['weekday'], $dayName)) {
199 // derive months part
201 $GLOBALS['log']->debug('got * months');
202 } elseif(strstr($mons, '*/')) {
203 $mult = str_replace('*/','',$mons);
204 $startMon = $timedate->fromTimestamp($focus->date_time_start)->month;
205 $startFrom = ($startMon % $mult);
207 for($i=$startFrom;$i<=12;$i+$mult) {
208 $compMons[] = $i+$mult;
211 // this month is not in one of the multiplier months
212 if(!in_array($today['mon'],$compMons)) {
215 } elseif($mons != '*') {
216 if(strstr($mons,',')) { // we have particular (groups) of months
217 $exMons = explode(',',$mons);
218 foreach($exMons as $k1 => $monGroup) {
219 if(strstr($monGroup, '-')) { // we have a range of months
220 $exMonGroup = explode('-',$monGroup);
221 for($i=$exMonGroup[0];$i<=$exMonGroup[1];$i++) {
225 $monName[] = $monGroup;
228 } elseif(strstr($mons, '-')) {
229 $exMonGroup = explode('-', $mons);
230 for($i=$exMonGroup[0];$i<=$exMonGroup[1];$i++) {
233 } else { // one particular month
237 // check that particular months are in scope
238 if(!in_array($today['mon'], $monName)) {
245 $GLOBALS['log']->debug('got * dates');
246 } elseif(strstr($dates, '*/')) {
247 $mult = str_replace('*/','',$dates);
248 $startDate = $timedate->fromTimestamp($focus->date_time_start)->day;
249 $startFrom = ($startDate % $mult);
251 for($i=$startFrom; $i<=31; $i+$mult) {
252 $dateName[] = str_pad(($i+$mult),2,'0',STR_PAD_LEFT);
256 if(!in_array($today['mday'], $dateName)) {
259 } elseif($dates != '*') {
260 if(strstr($dates, ',')) {
261 $exDates = explode(',', $dates);
262 foreach($exDates as $k1 => $dateGroup) {
263 if(strstr($dateGroup, '-')) {
264 $exDateGroup = explode('-', $dateGroup);
265 for($i=$exDateGroup[0];$i<=$exDateGroup[1];$i++) {
269 $dateName[] = $dateGroup;
272 } elseif(strstr($dates, '-')) {
273 $exDateGroup = explode('-', $dates);
274 for($i=$exDateGroup[0];$i<=$exDateGroup[1];$i++) {
278 $dateName[] = $dates;
281 // check that dates are in scope
282 if(!in_array($today['mday'], $dateName)) {
288 //$startHour = date('G', strtotime($focus->date_time_start));
289 //$currentHour = ($startHour < 1) ? 23 : date('G', strtotime($focus->date_time_start));
290 $currentHour = $timedate->getNow()->hour;
292 $GLOBALS['log']->debug('got * hours');
293 for($i=0;$i<=24; $i++) {
294 if($currentHour + $i > 23) {
295 $hrName[] = $currentHour + $i - 24;
297 $hrName[] = $currentHour + $i;
300 } elseif(strstr($hrs, '*/')) {
301 $mult = str_replace('*/','',$hrs);
302 for($i=0; $i<24; $i) { // weird, i know
303 if($currentHour + $i > 23) {
304 $hrName[] = $currentHour + $i - 24;
306 $hrName[] = $currentHour + $i;
310 } elseif($hrs != '*') {
311 if(strstr($hrs, ',')) {
312 $exHrs = explode(',',$hrs);
313 foreach($exHrs as $k1 => $hrGroup) {
314 if(strstr($hrGroup, '-')) {
315 $exHrGroup = explode('-', $hrGroup);
316 for($i=$exHrGroup[0];$i<=$exHrGroup[1];$i++) {
320 $hrName[] = $hrGroup;
323 } elseif(strstr($hrs, '-')) {
324 $exHrs = explode('-', $hrs);
325 for($i=$exHrs[0];$i<=$exHrs[1];$i++) {
333 $currentMin = $timedate->getNow()->minute;
334 if(substr($currentMin, 0, 1) == '0') {
335 $currentMin = substr($currentMin, 1, 1);
338 $GLOBALS['log']->debug('got * mins');
339 for($i=0; $i<60; $i++) {
340 if(($currentMin + $i) > 59) {
341 $minName[] = ($i + $currentMin - 60);
343 $minName[] = ($i+$currentMin);
346 } elseif(strstr($mins,'*/')) {
347 $mult = str_replace('*/','',$mins);
348 $startMin = $timedate->fromTimestmp($focus->date_time_start)->minute;
349 $startFrom = ($startMin % $mult);
351 for($i=$startFrom; $i<=59; $i+$mult) {
352 if(($currentMin + $i) > 59) {
353 $minName[] = ($i + $currentMin - 60);
355 $minName[] = ($i+$currentMin);
359 } elseif($mins != '*') {
360 if(strstr($mins, ',')) {
361 $exMins = explode(',',$mins);
362 foreach($exMins as $k1 => $minGroup) {
363 if(strstr($minGroup, '-')) {
364 $exMinGroup = explode('-', $minGroup);
365 for($i=$exMinGroup[0]; $i<=$exMinGroup[1]; $i++) {
369 $minName[] = $minGroup;
372 } elseif(strstr($mins, '-')) {
373 $exMinGroup = explode('-', $mins);
374 for($i=$exMinGroup[0]; $i<=$exMinGroup[1]; $i++) {
382 // prep some boundaries - these are not in GMT b/c gmt is a 24hour period, possibly bridging 2 local days
383 if(empty($focus->time_from) && empty($focus->time_to) ) {
385 $timeToTs = strtotime('+1 day');
387 $timeFromTs = strtotime($focus->time_from); // these are now GMT (timestamps are all GMT)
388 $timeToTs = strtotime($focus->time_to); // see above
389 if($timeFromTs > $timeToTs) { // we've crossed into the next day
390 $timeToTs = strtotime('+1 day '. $focus->time_to); // also in GMT
395 if(empty($focus->last_run)) {
398 $lastRunTs = strtotime($focus->last_run);
402 // now smush the arrays together =)
403 $validJobTime = array();
405 $dts = explode(' ',$focus->date_time_start); // split up datetime field into date & time
407 $dts2 = $timedate->to_db_date_time($dts[0],$dts[1]); // get date/time into DB times (GMT)
408 $dateTimeStart = $dts2[0]." ".$dts2[1];
409 $timeStartTs = strtotime($dateTimeStart);
410 if(!empty($focus->date_time_end) && !$focus->date_time_end == '2021-01-01 07:59:00') { // do the same for date_time_end if not empty
411 $dte = explode(' ', $focus->date_time_end);
412 $dte2 = $timedate->to_db_date_time($dte[0],$dte[1]);
413 $dateTimeEnd = $dte2[0]." ".$dte2[1];
415 $dateTimeEnd = $timedate->getNow()->get('+1 day')->asDb();
416 // $dateTimeEnd = '2020-12-31 23:59:59'; // if empty, set it to something ridiculous
418 $timeEndTs = strtotime($dateTimeEnd); // GMT end timestamp if necessary
420 /*_pp('hours:'); _pp($hrName);_pp('mins:'); _pp($minName);*/
421 $nowTs = $timedate->getNow()->ts;
423 // _pp('currentHour: '. $currentHour);
424 // _pp('timeStartTs: '.date('r',$timeStartTs));
425 // _pp('timeFromTs: '.date('r',$timeFromTs));
426 // _pp('timeEndTs: '.date('r',$timeEndTs));
427 // _pp('timeToTs: '.date('r',$timeToTs));
428 // _pp('mktime: '.date('r',mktime()));
429 // _pp('timeLastRun: '.date('r',$lastRunTs));
436 foreach($hrName as $kHr=>$hr) {
438 foreach($minName as $kMin=>$min) {
439 if($hr < $currentHour || $hourSeen == 25) {
440 $theDate = $timedate->asDbDate($timedate->getNow()->get('+1 day'));
442 $theDate = $timedate->nowDbDate();
444 $tsGmt = strtotime($theDate.' '.str_pad($hr,2,'0',STR_PAD_LEFT).":".str_pad($min,2,'0',STR_PAD_LEFT).":00"); // this is LOCAL
445 // _pp(date('Y-m-d H:i:s',$tsGmt));
447 if( $tsGmt >= $timeStartTs ) { // start is greater than the date specified by admin
448 if( $tsGmt >= $timeFromTs ) { // start is greater than the time_to spec'd by admin
449 if( $tsGmt <= $timeEndTs ) { // this is taken care of by the initial query - start is less than the date spec'd by admin
450 if( $tsGmt <= $timeToTs ) { // start is less than the time_to
451 if( $tsGmt >= $nowTs ) { // we only want to add jobs that are in the future
452 if( $tsGmt > $lastRunTs ) { //TODO figure if this is better than the above check
453 $validJobTime[] = $timedate->fromTimestamp($tsGmt)->asDb(); //_pp("Job Qualified for: ".date('Y-m-d H:i:s', $tsGmt));
455 //_pp('Job Time is NOT greater than Last Run');
458 //_pp('Job Time is NOT larger than NOW'); _pp(date('Y-m-d H:i:s', $nowTs));
461 //_pp('Job Time is NOT smaller that TimeTO: '.$tsGmt .'<='. $timeToTs);
464 //_pp('Job Time is NOT smaller that DateTimeEnd: '.date('Y-m-d H:i:s',$tsGmt) .'<='. $dateTimeEnd); _pp( $tsGmt .'<='. $timeEndTs );
467 //_pp('Job Time is NOT bigger that TimeFrom: '.$tsGmt .'>='. $timeFromTs);
470 //_pp('Job Time is NOT Bigger than DateTimeStart: '.date('Y-m-d H:i',$tsGmt) .'>='. $dateTimeStart);
475 // _ppd($validJobTime);
476 return $validJobTime;
481 * This function takes an array of jobs build up by retrieveSchedulers and
482 * puts them into the schedulers_times table
484 function insertSchedules() {
485 $GLOBALS['log']->info('----->Scheduler retrieving scheduled items and adding them to Job queue.');
486 $jobsArr = $this->retrieveSchedulers();
487 if(is_array($jobsArr['ids']) && !empty($jobsArr['ids']) && is_array($jobsArr['times']) && !empty($jobsArr['times'])) {
488 foreach($jobsArr['ids'] as $k => $ids) {
489 foreach($jobsArr['times'][$k] as $j => $time) {
490 $guid = create_guid();
491 $q = "INSERT INTO schedulers_times
492 (id, deleted, date_entered, date_modified, scheduler_id, execute_time, status)
496 ".db_convert("'".TimeDate::getInstance()->nowDb()."'", 'datetime').",
497 ".db_convert("'".TimeDate::getInstance()->nowDb()."'", 'datetime').",
498 '".$jobsArr['ids'][$k]."',
499 ".db_convert("'".$time."'", 'datetime').",
502 $this->db->query($q);
503 $GLOBALS['log']->info('Query: '.$q);
510 * This function drops all rows in the schedulers_times table.
512 function dropSchedules($truncate=false) {
513 global $sugar_config;
515 if(empty($this->db)) {
516 $this->db = DBManagerFactory::getInstance();
520 if($sugar_config['dbconfig']['db_type'] == 'oci8') {
522 $query = 'TRUNCATE schedulers_times';
524 $this->db->query($query);
525 $GLOBALS['log']->debug('----->Scheduler TRUNCATED ALL Jobs: '.$query);
527 $query = 'UPDATE schedulers_times SET deleted = 1';
528 $this->db->query($query);
529 $GLOBALS['log']->debug('----->Scheduler soft deleting all Jobs: '.$query);
531 //TODO make sure this will fail gracefully
535 * This function retrieves valid jobs, parses the cron format, then returns
536 * an array of [JOB_ID][EXEC_TIME][JOB]
538 * @return $executeJobs multi-dimensional array
539 * [job_id][execute_time]
541 function retrieveSchedulers() {
542 $GLOBALS['log']->info('Gathering Schedulers');
543 $executeJobs = array();
544 $query = "SELECT id " .
547 "AND status = 'Active' " .
548 "AND date_time_start < ".db_convert("'".TimeDate::getInstance()->nowDb()."'",'datetime')." " .
549 "AND (date_time_end > ".db_convert("'".TimeDate::getInstance()->nowDb()."'",'datetime')." OR date_time_end IS NULL)";
551 $result = $this->db->query($query);
553 $executeTimes = array();
554 $executeIds = array();
555 $executeJobTimes = array();
556 while(($arr = $this->db->fetchByAssoc($result)) != null) {
557 $focus = new Scheduler();
558 $focus->retrieve($arr['id']);
559 $executeTimes[$rows] = $this->deriveDBDateTimes($focus);
560 if(count($executeTimes) > 0) {
561 foreach($executeTimes as $k => $time) {
562 $executeIds[$rows] = $focus->id;
563 $executeJobTimes[$rows] = $time;
568 $executeJobs['ids'] = $executeIds;
569 $executeJobs['times'] = $executeJobTimes;
573 } // end SchedulerDaemon class desc.