* * Class for creating cron-job like timed calls of phpGroupWare methods * * -------------------------------------------------------------------------* * This library is part of the phpGroupWare API * * http://www.phpgroupware.org/ * * ------------------------------------------------------------------------ * * This library is free software; you can redistribute it and/or modify it * * under the terms of the GNU Lesser General Public License as published by * * the Free Software Foundation; either version 2.1 of the License, * * or any later version. * * This library is distributed in the hope that it will be useful, but * * WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * * See the GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * * along with this library; if not, write to the Free Software Foundation, * * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * \**************************************************************************/ /* $Id$ */ class asyncservice { var $public_functions = array( 'set_timer' => True, 'check_run' => True, 'cancel_timer' => True, 'read' => True, 'install' => True, 'installed' => True, 'test' => True ); var $php = ''; var $crontab = ''; var $db; var $db_table = 'phpgw_async'; function asyncservice() { $this->db = $GLOBALS['phpgw']->db; } /*! @function set_timer @abstract calculates the next run of the timer and puts that with the rest of the data in the db @syntax set_timer($times,$id,$method,$data) @param $times unix timestamp or array('min','hour','dow','day','month','year') with execution time. \ Repeated events are possible to shedule by setting the array only partly, eg. \ array('day' => 1) for first day in each month 0am or array('min' => '* /5', 'hour' => '9-17') \ for every 5mins in the time from 9am to 5pm. @param $id unique id to cancel the request later, if necessary. Should be in a form like \ eg. 'X' where id is the internal id of app and X might indicate the action. @param $method Method to be called via ExecMethod($method,$data). $method has the form \ '..'. @param $data This data is passed back when the method is called. It might simply be an \ integer id, but it can also be a complete array. @Returns False if $id already exists, else True */ function set_timer($times,$id,$method,$data) { if (empty($id) || empty($method) || $this->read($id) || !($next = $this->next_run($times))) { return False; } $job = array( 'id' => $id, 'next' => $next, 'times' => $times, 'method' => $method, 'data' => $data ); $this->write($job); return True; } /*! @function next_run @abstract calculates the next execution time for $time @syntax next_run($time) @param $times unix timestamp or array('year'=>$year,'month'=>$month,'dow'=>$dow,'day'=>$day,'hour'=>$hour,'min'=>$min) \ with execution time. Repeated execution is possible to shedule by setting the array only partly, \ eg. array('day' => 1) for first day in each month 0am or array('min' => '/5', 'hour' => '9-17') \ for every 5mins in the time from 9am to 5pm. All not set units before the smallest one set, \ are taken into account as every possible value, all after as the smallest possible value. @returns a unix timestamp of the next execution time or False if no more executions */ function next_run($times,$debug=False) { $now = time(); // $times is unix timestamp => if it's not expired return it, else False // if (!is_array($times)) { $next = intval($times); return $next > $now ? $next : False; } // If an array is given, we have to enumerate the possible times first // $units = array( 'year' => 'Y', 'month' => 'm', 'day' => 'd', 'dow' => 'w', 'hour' => 'H', 'min' => 'i' ); $max_unit = array( 'min' => 59, 'hour' => 23, 'dow' => 6, 'day' => 31, 'month' => 12, 'year' => date('Y')+10 // else */[0-9] would never stop returning numbers ); $min_unit = array( 'min' => 0, 'hour' => 0, 'dow' => 0, 'day' => 1, 'month' => 1, 'year' => date('Y') ); // get the number of the first and last pattern set in $times, // as empty patterns get enumerated before the the last pattern and // get set to the minimum after // $n = $first_set = $last_set = 0; foreach($units as $u => $date_pattern) { ++$n; if (isset($times[$u])) { $last_set = $n; if (!$first_set) { $first_set = $n; } } } // now we go through all units and enumerate all patterns and not set patterns // (as descript above), enumerations are arrays with unit-values as keys // $n = 0; foreach($units as $u => $date_pattern) { ++$n; //echo "

$u: isset(times[$u]="; print_r($times[$u]); echo ")=".(isset($times[$u])?'True':'False')."

\n"; if (isset($times[$u])) { $time = explode(',',$times[$u]); $times[$u] = array(); foreach($time as $t) { if (strstr($t,'-') !== False && strstr($t,'/') === False) { list($min,$max) = $arr = explode('-',$t); if (count($arr) != 2 || !is_numeric($min) || !is_numeric($max) || $min > $max) { if ($debug) echo "

Syntax error in $u='$t', allowed is 'min-max', min <= max, min='$min', max='$max'

\n"; return False; } for ($i = intval($min); $i <= $max; ++$i) { $times[$u][$i] = True; } } else { list($one,$inc) = $arr = explode('/',$t); if (!(is_numeric($one) && count($arr) == 1 || count($arr) == 2 && is_numeric($inc))) { if ($debug) echo "

Syntax error in $u='$t', allowed is a number or '{*|range}/inc', inc='$inc'

\n"; return False; } if (count($arr) == 1) { $times[$u][intval($one)] = True; } else { list($min,$max) = $arr = explode('-',$one); if (empty($one) || $one == '*') { $min = $min_unit[$u]; $max = $max_unit[$u]; } elseif (count($arr) != 2 || $min > $max) { if ($debug) echo "

Syntax error in $u='$t', allowed is '{*|min-max}/inc', min='$min',max='$max', inc='$inc'

\n"; return False; } for ($i = $min; $i <= $max; $i += $inc) { $times[$u][$i] = True; } } } } } elseif ($n < $last_set) // => empty gets enumerated { for ($i = $min_unit[$u]; $i <= $max_unit[$u]; ++$i) { $times[$u][$i] = True; } } else // => empty is min-value { $times[$u][$min_unit[$u]] = True; } } //echo "enumerated times=
"; print_r($times); echo "
\n"; // now we have the times enumerated, lets find the first not expired one // $found = array(); while (!isset($found['min'])) { $found = array(); $future = False; $n = 0; foreach($units as $u => $date_pattern) { $unit_now = $u != 'dow' ? intval(date($date_pattern)) : intval(date($date_pattern,mktime(12,0,0,$found['month'],$found['day'],$found['year']))); foreach($times[$u] as $unit_value => $nul) { switch($u) { case 'dow': $valid = $unit_value == $unit_now; break; case 'min': $valid = $future || $unit_value > $unit_now; break; default: $valid = $future || $unit_value >= $unit_now; break; } if ($valid && ($u != $next || $unit_value > $over)) // valid and not over { $found[$u] = $unit_value; $future = $future || $unit_value > $unit_now; break; } } if (!isset($found[$u])) // we have to try the next one, if it exists { $next = array_keys($units); if (!isset($next[$n-1])) { //echo "

Nothing found, exiting !!!

\n"; return False; } $next = $next[$n-1]; $over = $found[$next]; //echo "

Have to try the next $next, $u's are over for $next=$over !!!

\n"; break; } $n++; } } //echo "

next="; print_r($found); echo "

\n"; return mktime($found['hour'],$found['min'],0,$found['month'],$found['day'],$found['year']); } /*! @function cancel_timer @abstract cancels a timer @syntax cancel_timer($id) @param $id has to be the one used to set it. @returns True if the timer exists and is not expired. */ function cancel_timer($id) { return $this->delete($id); } /*! @function check_run @abstract checks if there are any jobs ready to run (timer expired) and executes them */ function check_run() { if (!($jobs = $this->read())) { return False; } foreach($jobs as $id => $job) { ExecMethod($job['method'],$job['data']); if ($job['next'] = $this->next_run($job['times'])) { $this->write($job,True); } else // no further runs { $this->delete($job['id']); } } return count($jobs); } /*! @function read @abstract reads all matching db-rows / jobs @syntax reay($id=0) @param $id =0 reads all expired rows / jobs ready to run\ != 0 reads all rows/jobs matching $id (sql-wildcards '%' and '_' can be used) @returns db-rows / jobs as array or False if no matches */ function read($id=0) { $id = $this->db->db_addslashes($id); if (strpos($id,'%') !== False || strpos($id,'_') !== False) { $where = "id LIKE '$id'"; } elseif (!$id) { $where = 'next<='.time(); } else { $where = "id='$id'"; } $this->db->query($sql="SELECT * FROM $this->db_table WHERE $where",__LINE__,__FILE__); $jobs = array(); while ($this->db->next_record()) { $id = $this->db->f('id'); $jobs[$id] = array( 'id' => $id, 'next' => $this->db->f('next'), 'times' => unserialize($this->db->f('times')), 'method' => $this->db->f('method'), 'data' => unserialize($this->db->f('data')) ); } if (!count($jobs)) { return False; } return $jobs; } /*! @function write @abstract write a job / db-row to the db @syntax write($job,$exists = False) @param $job db-row as array @param $exits if True, we do an update, else we check if update or insert necesary */ function write($job,$exists = False) { $job['times'] = $this->db->db_addslashes(serialize($job['times'])); $job['data'] = $this->db->db_addslashes(serialize($job['data'])); if ($exists || $this->read($job['id'])) { $this->db->query("UPDATE $this->db_table SET next=$job[next],times='$job[times]',". "method='$job[method]',data='$job[data]' WHERE id='$job[id]'",__FILE__,__LINE__); } else { $this->db->query("INSERT INTO $this->db_table (id,next,times,method,data) VALUES ". "('$job[id]',$job[next],'$job[times]','$job[method]','$job[data]')",__FILE__,__LINE__); } } /*! @function delete @abstract delete db-row / job with $id @returns False if $id not found else True */ function delete($id) { $this->db->query("DELETE FROM $this->db_table WHERE id='$id'",__LINE__,__FILE__); return $this->db->affected_rows(); } function find_binarys() { static $run = False; if ($run) { return; } $run = True; if (substr(php_uname(), 0, 7) == "Windows") { // ToDo: find php-cgi on windows } else { $binarys = array('php' => '/usr/bin/php','crontab' => '/usr/bin/crontab'); foreach ($binarys as $name => $path) { $this->$name = $path; // a reasonable default for *nix if (!is_executable($this->$name)) { if ($fd = popen('/bin/sh -c "which '.$name.'"','r')) { $this->$name = fgets($fd,256); @pclose($fd); } if ($pos = strpos($this->$name,"\n")) { $this->$name = substr($this->$name,0,$pos); } } //echo "

$name = '".$this->$name."'

\n"; } } } /*! @function installed @abstract checks if phpgwapi/cron/asyncservices.php is installed as cron-job @syntax installed() @returns the times asyncservices are run (normaly 'min'=>'* /5') or False if not installed @note Not implemented for Windows at the moment, always returns False */ function installed() { if (substr(php_uname(), 0, 7) == "Windows") { False; } $this->find_binarys(); $times = False; if (($crontab = popen('/bin/sh -c "'.$this->crontab.' -l" 2>&1','r')) !== False) { while ($line = fgets($crontab,256)) { if ($line[0] != '#' && strstr($line,'asyncservices.php')) { $time = explode(' ',$line); $cron_units = array('min','hour','day','month','dow'); foreach($cron_units as $n => $u) { if ($time[$n] != '*') { $times[$u] = $time[$n]; } } $times['cronline'] = $line; break; } } @pclose($crontab); } return $times; } /*! @function insall @abstract installs /phpgwapi/cron/asyncservices.php as cron-job @syntax install($times) @param $times array with keys 'min','hour','day','month','dow', not set is equal to '*' @returns the times asyncservices are run or False if they are not installed @note Not implemented for Windows at the moment, always dies with an error-message */ function install($times) { if (substr(php_uname(), 0, 7) == "Windows") { die ("Sorry, no automatic on Windows at the moment !!!\n"); } $this->find_binarys(); if (($crontab = popen('/bin/sh -c "'.$this->crontab.' -" 2>&1','w')) !== False) { $cron_units = array('min','hour','day','month','dow'); foreach($cron_units as $cu) { $cronline .= (isset($times[$cu]) ? $times[$cu] : '*') . ' '; } $cronline .= $this->php.' -q '.PHPGW_SERVER_ROOT . '/phpgwapi/cron/asyncservices.php'."\n"; //echo "

Installing: '$cronline'

\n"; fwrite($crontab,$cronline); @pclose($crontab); } else { //echo "

Error: /usr/bin/crontab not found !!!

"; return False; } return $this->installed(); } function test($data) { echo "asyncservice::test: data =\n"; print_r($data); } } if (!isset($GLOBALS['phpgw_info'])) { $GLOBALS['phpgw_info']['flags'] = array( 'currentapp' => 'login' ); include('../../header.inc.php'); $async = new asyncservice; $units = array( 'year' => 'Year', 'month' => 'Month', 'day' => 'Day', 'dow' => 'Day of week', 'hour' => 'Hour', 'min' => 'Minute' ); if ($_POST['send'] || $_POST['test'] || $_POST['cancel'] || $_POST['install']) { $times = array(); foreach($units as $u => $ulabel) { if (!empty($_POST[$u])) { $times[$u] = $_POST[$u]; } } $next = $async->next_run($times,True); echo "

async::next_run(";print_r($times);echo")=".($next === False ? 'False':"'$next'=".date('D(w) d.m.Y H:i',$next))."

\n"; if ($_POST['test']) { if (!$async->set_timer($times,'test','phpgwapi.asyncservice.test','Hello World!!!')) { echo "

Error setting timer, maybe there's one already running !!!

\n"; } } if ($_POST['cancel']) { if (!$async->cancel_timer('test')) { echo "

Error canceling timer, maybe there's none set !!!

\n"; } } if ($_POST['install']) { if ($install = $async->install($times)) { echo "

Installing: '$install[cronline]'

\n"; } else { echo "

Error: $async->crontab not found or other error !!!

"; } } } echo '
'."\n"; foreach ($units as $u => $ulabel) { echo "$ulabel:  \n"; } echo "\n"; echo "\n"; echo "\n"; echo "

crontab: \n"; if ($installed = $async->installed()) { echo "$installed[cronline]

"; } else { echo "$async->crontab not found or asyncservices not installed !!!

"; } echo "\n"; echo "

jobs:

\n"; if ($jobs = $async->read('%')) { echo "\n\n\n"; foreach($jobs as $job) { echo "\n\n"; } echo "
IdNext runTimesMethodData
$job[id]".date('Y/m/d H:i',$job['next']).""; print_r($job['times']); echo "$job[method]"; print_r($job['data']); echo "
\n"; } else { echo "

No jobs in the database !!!

\n"; } echo "
\n"; }