From 3ddd4a7d178b5f25bd1691b7add1d25655e20d7f Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Sun, 10 Jun 2007 08:47:14 +0000 Subject: [PATCH] bugfix: async service "sometimes" misses jobs (db-class was not cloned but copied) --- phpgwapi/inc/class.asyncservice.inc.php | 1245 +++++++++++------------ 1 file changed, 620 insertions(+), 625 deletions(-) diff --git a/phpgwapi/inc/class.asyncservice.inc.php b/phpgwapi/inc/class.asyncservice.inc.php index b2a9ad1377..26767012c2 100644 --- a/phpgwapi/inc/class.asyncservice.inc.php +++ b/phpgwapi/inc/class.asyncservice.inc.php @@ -1,702 +1,697 @@ * - * Class for creating cron-job like timed calls of eGroupWare methods * - * -------------------------------------------------------------------------* - * This library is part of the eGroupWare API * - * http://www.eGroupWare.org * - * ------------------------------------------------------------------------ * - * This program is free software; you can redistribute it and/or modify it * - * under the terms of the GNU General Public License as published by the * - * Free Software Foundation; either version 2 of the License, or (at your * - * option) any later version. * - \**************************************************************************/ +/** + * API - Timed Asynchron Services for eGroupWare + * + * @link http://www.egroupware.org + * @author Ralf Becker + * @copyright Ralf Becker + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package api + * @access public + * @version $Id$ + */ - /* $Id$ */ +/** + * The class implements a general eGW service to execute callbacks at a given time. + * + * see http://www.egroupware.org/wiki/TimedAsyncServices + */ +class asyncservice +{ + var $php = ''; + var $crontab = ''; + /** + * Our instance of the db-class + * + * @var egw_db + */ + var $db; + var $db_table = 'egw_async'; + var $debug = 0; + + /** + * constructor of the class + */ + function asyncservice() + { + if (is_object($GLOBALS['egw']->db)) + { + $this->db = clone($GLOBALS['egw']->db); + } + else + { + $this->db = clone($GLOBALS['egw_setup']->db); + } + $this->db->set_app('phpgwapi'); + + $this->cronline = EGW_SERVER_ROOT . '/phpgwapi/cron/asyncservices.php '.$GLOBALS['egw_info']['user']['domain']; + + $this->only_fallback = substr(php_uname(), 0, 7) == "Windows"; // atm cron-jobs dont work on win + } /** - * The class implements a general eGW service to execute callbacks at a given time. + * calculates the next run of the timer and puts that with the rest of the data in the db for later execution. * - * see http://www.egroupware.org/wiki/TimedAsyncServices - * - * @author Ralf Becker - * @copyright GPL - GNU General Public License + * @param int/array $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 string $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 string $method Method to be called via ExecMethod($method,$data). $method has the form + * '..'. + * @param mixed $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. + * @param int $account_id account_id, under which the methode should be called or False for the actual user + * @return boolean False if $id already exists, else True */ - class asyncservice + function set_timer($times,$id,$method,$data,$account_id=False) { - var $public_functions = array( - 'set_timer' => True, - 'check_run' => True, - 'cancel_timer' => True, - 'read' => True, - 'install' => True, - 'installed' => True, - 'last_check_run' => True + if (empty($id) || empty($method) || $this->read($id) || + !($next = $this->next_run($times))) + { + return False; + } + if ($account_id === False) + { + $account_id = $GLOBALS['egw_info']['user']['account_id']; + } + $job = array( + 'id' => $id, + 'next' => $next, + 'times' => $times, + 'method' => $method, + 'data' => $data, + 'account_id' => $account_id ); - var $php = ''; - var $crontab = ''; - var $db; - var $db_table = 'egw_async'; - var $debug = 0; + $this->write($job); - /** - * constructor of the class - */ - function asyncservice() + return True; + } + + /** + * calculates the next execution time for $times + * + * @param int/array $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. + * @param boolean $debug if True some debug-messages about syntax-errors in $times are echoed + * @return int a unix timestamp of the next execution time or False if no more executions + */ + function next_run($times,$debug=False) + { + if ($this->debug) { - $this->db = is_object($GLOBALS['egw']->db) ? $GLOBALS['egw']->db : $GLOBALS['phpgw_setup']->db; - $this->db->set_app('phpgwapi'); - - $this->cronline = EGW_SERVER_ROOT . '/phpgwapi/cron/asyncservices.php '.$GLOBALS['egw_info']['user']['domain']; - - $this->only_fallback = substr(php_uname(), 0, 7) == "Windows"; // atm cron-jobs dont work on win + echo "

next_run("; print_r($times); echo ",'$debug')

\n"; + $debug = True; // enable syntax-error messages too } - - /** - * calculates the next run of the timer and puts that with the rest of the data in the db for later execution. - * - * @param int/array $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 string $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 string $method Method to be called via ExecMethod($method,$data). $method has the form - * '..'. - * @param mixed $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. - * @param int $account_id account_id, under which the methode should be called or False for the actual user - * @return boolean False if $id already exists, else True - */ - function set_timer($times,$id,$method,$data,$account_id=False) + $now = time(); + + // $times is unix timestamp => if it's not expired return it, else False + // + if (!is_array($times)) { - if (empty($id) || empty($method) || $this->read($id) || - !($next = $this->next_run($times))) - { - return False; - } - if ($account_id === False) - { - $account_id = $GLOBALS['egw_info']['user']['account_id']; - } - $job = array( - 'id' => $id, - 'next' => $next, - 'times' => $times, - 'method' => $method, - 'data' => $data, - 'account_id' => $account_id - ); - $this->write($job); - - return True; + $next = (int)$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') + ); - /** - * calculates the next execution time for $times - * - * @param int/array $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. - * @param boolean $debug if True some debug-messages about syntax-errors in $times are echoed - * @return int a unix timestamp of the next execution time or False if no more executions - */ - function next_run($times,$debug=False) + // 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) { - if ($this->debug) + ++$n; + if (isset($times[$u])) { - echo "

next_run("; print_r($times); echo ",'$debug')

\n"; - $debug = True; // enable syntax-error messages too - } - $now = time(); - - // $times is unix timestamp => if it's not expired return it, else False - // - if (!is_array($times)) - { - $next = (int)$times; + $last_set = $n; - 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])) + if (!$first_set) { - $last_set = $n; - - if (!$first_set) - { - $first_set = $n; - } + $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) + // 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; + if ($this->debug) { echo "

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

\n"; } + if (isset($times[$u])) { - ++$n; - if ($this->debug) { echo "

n=$n, $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) { - $time = explode(',',$times[$u]); - - $times[$u] = array(); - - foreach($time as $t) + if (strpos($t,'-') !== False && strpos($t,'/') === False) { - if (strpos($t,'-') !== False && strpos($t,'/') === False) + list($min,$max) = $arr = explode('-',$t); + + if (count($arr) != 2 || !is_numeric($min) || !is_numeric($max) || $min > $max) { - 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"; + if ($debug) echo "

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

\n"; + return False; + } + for ($i = (int)$min; $i <= $max; ++$i) + { + $times[$u][$i] = True; + } + } + else + { + if ($t == '*') $t = '*/1'; + + 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][(int)$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 = (int)$min; $i <= $max; ++$i) + for ($i = $min; $i <= $max; $i += $inc) { $times[$u][$i] = True; } } - else - { - if ($t == '*') $t = '*/1'; - - 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][(int)$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 || $u == 'dow') // before last value set (or dow) => empty gets enumerated - { - for ($i = $min_unit[$u]; $i <= $max_unit[$u]; ++$i) - { - $times[$u][$i] = True; - } - } - else // => after last value set => empty is min-value - { - $times[$u][$min_unit[$u]] = True; - } } - if ($this->debug) { 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'])) + elseif ($n < $last_set || $u == 'dow') // before last value set (or dow) => empty gets enumerated { - $future = False; - - foreach($units as $u => $date_pattern) + for ($i = $min_unit[$u]; $i <= $max_unit[$u]; ++$i) { - $unit_now = $u != 'dow' ? (int)date($date_pattern) : - (int)date($date_pattern,mktime(12,0,0,$found['month'],$found['day'],$found['year'])); + $times[$u][$i] = True; + } + } + else // => after last value set => empty is min-value + { + $times[$u][$min_unit[$u]] = True; + } + } + if ($this->debug) { 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'])) + { + $future = False; - if (isset($found[$u])) + foreach($units as $u => $date_pattern) + { + $unit_now = $u != 'dow' ? (int)date($date_pattern) : + (int)date($date_pattern,mktime(12,0,0,$found['month'],$found['day'],$found['year'])); + + if (isset($found[$u])) + { + $future = $future || $found[$u] > $unit_now; + if ($this->debug) echo "--> already have a $u = ".$found[$u].", future='$future'
\n"; + continue; // already set + } + foreach($times[$u] as $unit_value => $nul) + { + switch($u) { - $future = $future || $found[$u] > $unit_now; - if ($this->debug) echo "--> already have a $u = ".$found[$u].", future='$future'
\n"; - continue; // already set - } - 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; + 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 (!isset($found[$u])) // we have to try the next one, if it exists + if ($valid && ($u != $next || $unit_value > $over)) // valid and not over { - $next = array_keys($units); - if (!isset($next[count($found)-1])) - { - if ($this->debug) echo "

Nothing found, exiting !!!

\n"; - return False; - } - $next = $next[count($found)-1]; - $over = $found[$next]; - unset($found[$next]); - if ($this->debug) echo "

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

\n"; + $found[$u] = $unit_value; + $future = $future || $unit_value > $unit_now; break; } } - } - if ($this->debug) { echo "

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

\n"; } - - return mktime($found['hour'],$found['min'],0,$found['month'],$found['day'],$found['year']); - } - - /** - * cancels a timer - * - * @param string $id has to be the one used to set it. - * @return boolean True if the timer exists and is not expired. - */ - function cancel_timer($id) - { - return $this->delete($id); - } - - /** - * checks when the last check_run was run or set the run-semaphore if $semaphore == True - * - * @param boolean $semaphore if False only check, if true try to set/release the semaphore - * @param boolean $release if $semaphore == True, tells if we should set or release the semaphore - * @return mixed if !$set array('start' => $start,'end' => $end) with timestamps of last check_run start and end, \ - * !$end means check_run is just running. If $set returns True if it was able to get the semaphore, else False - */ - function last_check_run($semaphore=False,$release=False,$run_by='') - { - //echo "

last_check_run(semaphore=".($semaphore?'True':'False').",release=".($release?'True':'False').")

\n"; - if ($semaphore) - { - $this->db->lock($this->db_table,'write'); // this will block til we get exclusive access to the table - - @set_time_limit(0); // dont stop for an execution-time-limit - ignore_user_abort(True); - } - if ($exists = $this->read('##last-check-run##')) - { - list(,$last_run) = each($exists); - } - //echo "last_run (from db)=
"; print_r($last_run); echo "
\n"; - - if (!$semaphore) - { - return $last_run['data']; - } - elseif (!$release && !$last_run['data']['end'] && $last_run['data']['start'] > time()-600) - { - // already one instance running (started not more then 10min ago, else we ignore it) - - $this->db->unlock(); // unlock the table again - - //echo "

An other instance is running !!!

\n"; - return False; - } - // no other instance runs ==> we should run - // - if ($release) - { - $last_run['data']['end'] = time(); - } - else - { - $last_run = array( - 'id' => '##last-check-run##', - 'next' => 0, - 'times' => array(), - 'method' => 'none', - 'data' => array( - 'run_by'=> $run_by, - 'start' => time(), - 'end' => 0 - ) - ); - } - //echo "last_run=
"; print_r($last_run); echo "
\n"; - $this->write($last_run,!!$exits); - - $this->db->unlock(); - - return True; - } - - /** - * checks if there are any jobs ready to run (timer expired) and executes them - */ - function check_run($run_by='') - { - flush(); - - if (!$this->last_check_run(True,False,$run_by)) - { - return False; // cant obtain semaphore - } - if ($jobs = $this->read()) - { - foreach($jobs as $id => $job) + if (!isset($found[$u])) // we have to try the next one, if it exists { - // checking / setting up phpgw_info/user - // - if ($GLOBALS['egw_info']['user']['account_id'] != $job['account_id']) + $next = array_keys($units); + if (!isset($next[count($found)-1])) { - $domain = $GLOBALS['egw_info']['user']['domain']; - $lang = $GLOBALS['egw_info']['user']['preferences']['common']['lang']; - unset($GLOBALS['egw_info']['user']); - - if ($GLOBALS['egw']->session->account_id = $job['account_id']) - { - $GLOBALS['egw']->session->account_lid = $GLOBALS['egw']->accounts->id2name($job['account_id']); - $GLOBALS['egw']->session->account_domain = $domain; - $GLOBALS['egw']->session->read_repositories(False,False); - $GLOBALS['egw_info']['user'] = $GLOBALS['egw']->session->user; - - if ($lang != $GLOBALS['egw_info']['user']['preferences']['common']['lang']) - { - unset($GLOBALS['lang']); - $GLOBALS['egw']->translation->add_app('common'); - } - } - else - { - $GLOBALS['egw_info']['user']['domain'] = $domain; - } - } - list($app) = explode('.',$job['method']); - $GLOBALS['egw']->translation->add_app($app); - - ExecMethod($job['method'],$job['data']); - - // re-read job, in case it had been updated or even deleted in the method - $updated = $this->read($id); - if ($updated && isset($updated[$id])) - { - $job = $updated[$id]; - - if ($job['next'] = $this->next_run($job['times'])) - { - $this->write($job,True); - } - else // no further runs - { - $this->delete($job['id']); - } + if ($this->debug) echo "

Nothing found, exiting !!!

\n"; + return False; } + $next = $next[count($found)-1]; + $over = $found[$next]; + unset($found[$next]); + if ($this->debug) echo "

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

\n"; + break; } } - $this->last_check_run(True,True,$run_by); // release semaphore - - return $jobs ? count($jobs) : False; } + if ($this->debug) { echo "

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

\n"; } - /** - * reads all matching db-rows / jobs - * - * @param string $id =0 reads all expired rows / jobs ready to run\ - * != 0 reads all rows/jobs matching $id (sql-wildcards '%' and '_' can be used) - * @return array/boolean db-rows / jobs as array or False if no matches - */ - function read($id=0) + return mktime($found['hour'],$found['min'],0,$found['month'],$found['day'],$found['year']); + } + + /** + * cancels a timer + * + * @param string $id has to be the one used to set it. + * @return boolean True if the timer exists and is not expired. + */ + function cancel_timer($id) + { + return $this->delete($id); + } + + /** + * checks when the last check_run was run or set the run-semaphore if $semaphore == True + * + * @param boolean $semaphore if False only check, if true try to set/release the semaphore + * @param boolean $release if $semaphore == True, tells if we should set or release the semaphore + * @return mixed if !$set array('start' => $start,'end' => $end) with timestamps of last check_run start and end, \ + * !$end means check_run is just running. If $set returns True if it was able to get the semaphore, else False + */ + function last_check_run($semaphore=False,$release=False,$run_by='') + { + //echo "

last_check_run(semaphore=".($semaphore?'True':'False').",release=".($release?'True':'False').")

\n"; + if ($semaphore) { - if (!is_array($id) && (strpos($id,'%') !== False || strpos($id,'_') !== False)) - { - $id = $this->db->quote($id); - $where = "async_id LIKE $id AND async_id != '##last-check-run##'"; - } - elseif (!$id) - { - $where = 'async_next <= '.time()." AND async_id != '##last-check-run##'"; - } - else - { - $where = array('async_id' => $id); - } - $this->db->select($this->db_table,'*',$where,__LINE__,__FILE__); + $this->db->lock($this->db_table,'write'); // this will block til we get exclusive access to the table - $jobs = array(); - while ($this->db->next_record()) - { - $id = $this->db->f('async_id'); - - $jobs[$id] = array( - 'id' => $id, - 'next' => $this->db->f('async_next'), - 'times' => unserialize($this->db->f('async_times')), - 'method' => $this->db->f('async_method'), - 'data' => unserialize($this->db->f('async_data')), - 'account_id' => $this->db->f('async_account_id') - ); - //echo "job id='$id'
"; print_r($jobs[$id]); echo "
\n"; - } - if (!count($jobs)) - { - return False; - } - return $jobs; + @set_time_limit(0); // dont stop for an execution-time-limit + ignore_user_abort(True); } - - /** - * write a job / db-row to the db - * - * @param array $job db-row as array - * @param boolean $exits if True, we do an update, else we check if update or insert necesary - */ - function write($job,$exists = False) + if ($exists = $this->read('##last-check-run##')) { - $data = array( - 'async_next' => $job['next'], - 'async_times' => serialize($job['times']), - 'async_method' => $job['method'], - 'async_data' => serialize($job['data']), - 'async_account_id'=> $job['account_id'], + list(,$last_run) = each($exists); + } + //echo "last_run (from db)=
"; print_r($last_run); echo "
\n"; + + if (!$semaphore) + { + return $last_run['data']; + } + elseif (!$release && !$last_run['data']['end'] && $last_run['data']['start'] > time()-600) + { + // already one instance running (started not more then 10min ago, else we ignore it) + + $this->db->unlock(); // unlock the table again + + //echo "

An other instance is running !!!

\n"; + return False; + } + // no other instance runs ==> we should run + // + if ($release) + { + $last_run['data']['end'] = time(); + } + else + { + $last_run = array( + 'id' => '##last-check-run##', + 'next' => 0, + 'times' => array(), + 'method' => 'none', + 'data' => array( + 'run_by'=> $run_by, + 'start' => time(), + 'end' => 0 + ) ); - if ($exists) - { - $this->db->update($this->db_table,$data,array('async_id' => $job['id']),__LINE__,__FILE__); - } - else - { - $this->db->insert($this->db_table,$data,array('async_id' => $job['id']),__LINE__,__FILE__); - } } + //echo "last_run=
"; print_r($last_run); echo "
\n"; + $this->write($last_run,!!$exits); + + $this->db->unlock(); - /** - * delete db-row / job with $id - * - * @return boolean False if $id not found else True - */ - function delete($id) + return True; + } + + /** + * checks if there are any jobs ready to run (timer expired) and executes them + */ + function check_run($run_by='') + { + flush(); + + if (!$this->last_check_run(True,False,$run_by)) { - $this->db->delete($this->db_table,array('async_id' => $id),__LINE__,__FILE__); - - return $this->db->affected_rows(); + return False; // cant obtain semaphore } - - function find_binarys() + if ($jobs = $this->read()) { - static $run = False; - if ($run) + foreach($jobs as $id => $job) { - return; - } - $run = True; + // checking / setting up egw_info/user + // + if ($GLOBALS['egw_info']['user']['account_id'] != $job['account_id']) + { + $domain = $GLOBALS['egw_info']['user']['domain']; + $lang = $GLOBALS['egw_info']['user']['preferences']['common']['lang']; + unset($GLOBALS['egw_info']['user']); - if (substr(php_uname(), 0, 7) == "Windows") - { - // ToDo: find php-cgi on windows - } - else - { - $binarys = array( - 'php' => '/usr/bin/php', - 'php4' => '/usr/bin/php4', // this is for debian - 'php5' => '/usr/bin/php5', // SuSE 9.3 with php5 - 'crontab' => '/usr/bin/crontab' - ); - foreach ($binarys as $name => $path) - { - $this->$name = $path; // a reasonable default for *nix - - if (!($Ok = @is_executable($this->$name))) + if ($GLOBALS['egw']->session->account_id = $job['account_id']) { - if (file_exists($this->$name)) + $GLOBALS['egw']->session->account_lid = $GLOBALS['egw']->accounts->id2name($job['account_id']); + $GLOBALS['egw']->session->account_domain = $domain; + $GLOBALS['egw']->session->read_repositories(False,False); + $GLOBALS['egw_info']['user'] = $GLOBALS['egw']->session->user; + + if ($lang != $GLOBALS['egw_info']['user']['preferences']['common']['lang']) { - echo '

'.lang('%1 is not executable by the webserver !!!',$this->$name)."

\n"; - $perms = fileperms($this->$name); - if (!($perms & 0x0001) && ($perms & 0x0008)) // only executable by group - { - $group = posix_getgrgid(filegroup($this->$name)); - $webserver = posix_getpwuid(posix_getuid ()); - echo '

'.lang("You need to add the webserver user '%1' to the group '%2'.",$webserver['name'],$group['name'])."

\n"; } + unset($GLOBALS['lang']); + $GLOBALS['egw']->translation->add_app('common'); } - if ($fd = popen('/bin/sh -c "type -p '.$name.'"','r')) - { - $this->$name = fgets($fd,256); - @pclose($fd); - } - if ($pos = strpos($this->$name,"\n")) - { - $this->$name = substr($this->$name,0,$pos); - } - } - if (!$Ok && !@is_executable($this->$name)) - { - $this->$name = $name; // hopefully its in the path - } - //echo "

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

\n"; - } - if ($this->php4[0] == '/') // we found a php4 binary - { - $this->php = $this->php4; - } - if ($this->php5[0] == '/') // we found a php5 binary - { - $this->php = $this->php5; - } - } - - } - - /** - * checks if phpgwapi/cron/asyncservices.php is installed as cron-job - * - * @return array the times asyncservices are run (normaly 'min'=>'* /5') or False if not installed or 0 if crontab not found - * Not implemented for Windows at the moment, always returns 0 - */ - function installed() - { - if ($this->only_fallback) { - return 0; - } - $this->find_binarys(); - - if (!is_executable($this->crontab)) - { - //echo "

Error: $this->crontab not found !!!

"; - return 0; - } - $times = False; - $this->other_cronlines = array(); - if (($crontab = popen('/bin/sh -c "'.$this->crontab.' -l" 2>&1','r')) !== False) - { - while ($line = fgets($crontab,256)) - { - if ($this->debug) echo 'line '.++$n.": $line
\n"; - $parts = split(' ',$line,6); - - if ($line[0] == '#' || count($parts) < 6 || ($parts[5][0] != '/' && substr($parts[5],0,3) != 'php')) - { - // ignore comments - if ($line[0] != '#') - { - $times['error'] .= $line; - } - } - elseif (strpos($line,$this->cronline) !== False) - { - $cron_units = array('min','hour','day','month','dow'); - foreach($cron_units as $n => $u) - { - $times[$u] = $parts[$n]; - } - $times['cronline'] = $line; } else { - $this->other_cronlines[] = $line; + $GLOBALS['egw_info']['user']['domain'] = $domain; + } + } + list($app) = explode('.',$job['method']); + $GLOBALS['egw']->translation->add_app($app); + ExecMethod($job['method'],$job['data']); + + // re-read job, in case it had been updated or even deleted in the method + $updated = $this->read($id); + if ($updated && isset($updated[$id])) + { + $job = $updated[$id]; + + if ($job['next'] = $this->next_run($job['times'])) + { + $this->write($job,True); + } + else // no further runs + { + $this->delete($job['id']); } } - @pclose($crontab); } - return $times; } - - /** - * installs /phpgwapi/cron/asyncservices.php as cron-job - * - * Not implemented for Windows at the moment, always returns 0 - * - * @param array $times array with keys 'min','hour','day','month','dow', not set is equal to '*'. - * False means de-install our own crontab line - * @return mixed the times asyncservices are run, False if they are not installed, - * 0 if crontab not found and ' ' if crontab is deinstalled - */ - function install($times) - { - if ($this->only_fallback && $times !== False) { - return 0; - } - $this->installed(); // find other installed cronlines + $this->last_check_run(True,True,$run_by); // release semaphore - if (($crontab = popen('/bin/sh -c "'.$this->crontab.' -" 2>&1','w')) !== False) - { - if (is_array($this->other_cronlines)) - { - foreach ($this->other_cronlines as $cronline) - { - fwrite($crontab,$cronline); // preserv the other lines on install - } - } - if ($times !== False) - { - $cron_units = array('min','hour','day','month','dow'); - $cronline = ''; - foreach($cron_units as $cu) - { - $cronline .= (isset($times[$cu]) ? $times[$cu] : '*') . ' '; - } - $cronline .= $this->php.' -qC '.$this->cronline."\n"; - //echo "

Installing: '$cronline'

\n"; - fwrite($crontab,$cronline); - } - @pclose($crontab); - } - return $times !== False ? $this->installed() : ' '; + return $jobs ? count($jobs) : False; + } + + /** + * reads all matching db-rows / jobs + * + * @param string $id =0 reads all expired rows / jobs ready to run\ + * != 0 reads all rows/jobs matching $id (sql-wildcards '%' and '_' can be used) + * @return array/boolean db-rows / jobs as array or False if no matches + */ + function read($id=0) + { + if (!is_array($id) && (strpos($id,'%') !== False || strpos($id,'_') !== False)) + { + $id = $this->db->quote($id); + $where = "async_id LIKE $id AND async_id != '##last-check-run##'"; + } + elseif (!$id) + { + $where = 'async_next <= '.time()." AND async_id != '##last-check-run##'"; + } + else + { + $where = array('async_id' => $id); + } + $this->db->select($this->db_table,'*',$where,__LINE__,__FILE__); + + $jobs = array(); + while ($this->db->next_record()) + { + $id = $this->db->f('async_id'); + + $jobs[$id] = array( + 'id' => $id, + 'next' => $this->db->f('async_next'), + 'times' => unserialize($this->db->f('async_times')), + 'method' => $this->db->f('async_method'), + 'data' => unserialize($this->db->f('async_data')), + 'account_id' => $this->db->f('async_account_id') + ); + //echo "job id='$id'
"; print_r($jobs[$id]); echo "
\n"; + } + if (!count($jobs)) + { + return False; + } + return $jobs; + } + + /** + * write a job / db-row to the db + * + * @param array $job db-row as array + * @param boolean $exits if True, we do an update, else we check if update or insert necesary + */ + function write($job,$exists = False) + { + $data = array( + 'async_next' => $job['next'], + 'async_times' => serialize($job['times']), + 'async_method' => $job['method'], + 'async_data' => serialize($job['data']), + 'async_account_id'=> $job['account_id'], + ); + if ($exists) + { + $this->db->update($this->db_table,$data,array('async_id' => $job['id']),__LINE__,__FILE__); + } + else + { + $this->db->insert($this->db_table,$data,array('async_id' => $job['id']),__LINE__,__FILE__); } } + + /** + * delete db-row / job with $id + * + * @return boolean False if $id not found else True + */ + function delete($id) + { + $this->db->delete($this->db_table,array('async_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', + 'php4' => '/usr/bin/php4', // this is for debian + 'php5' => '/usr/bin/php5', // SuSE 9.3 with php5 + 'crontab' => '/usr/bin/crontab' + ); + foreach ($binarys as $name => $path) + { + $this->$name = $path; // a reasonable default for *nix + + if (!($Ok = @is_executable($this->$name))) + { + if (file_exists($this->$name)) + { + echo '

'.lang('%1 is not executable by the webserver !!!',$this->$name)."

\n"; + $perms = fileperms($this->$name); + if (!($perms & 0x0001) && ($perms & 0x0008)) // only executable by group + { + $group = posix_getgrgid(filegroup($this->$name)); + $webserver = posix_getpwuid(posix_getuid ()); + echo '

'.lang("You need to add the webserver user '%1' to the group '%2'.",$webserver['name'],$group['name'])."

\n"; } + } + if ($fd = popen('/bin/sh -c "type -p '.$name.'"','r')) + { + $this->$name = fgets($fd,256); + @pclose($fd); + } + if ($pos = strpos($this->$name,"\n")) + { + $this->$name = substr($this->$name,0,$pos); + } + } + if (!$Ok && !@is_executable($this->$name)) + { + $this->$name = $name; // hopefully its in the path + } + //echo "

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

\n"; + } + if ($this->php4{0} == '/') // we found a php4 binary + { + $this->php = $this->php4; + } + if ($this->php5{0} == '/') // we found a php5 binary + { + $this->php = $this->php5; + } + } + } + + /** + * checks if phpgwapi/cron/asyncservices.php is installed as cron-job + * + * @return array the times asyncservices are run (normaly 'min'=>'* /5') or False if not installed or 0 if crontab not found + * Not implemented for Windows at the moment, always returns 0 + */ + function installed() + { + if ($this->only_fallback) { + return 0; + } + $this->find_binarys(); + + if (!is_executable($this->crontab)) + { + //echo "

Error: $this->crontab not found !!!

"; + return 0; + } + $times = False; + $this->other_cronlines = array(); + if (($crontab = popen('/bin/sh -c "'.$this->crontab.' -l" 2>&1','r')) !== False) + { + while ($line = fgets($crontab,256)) + { + if ($this->debug) echo 'line '.++$n.": $line
\n"; + $parts = split(' ',$line,6); + + if ($line{0} == '#' || count($parts) < 6 || ($parts[5]{0} != '/' && substr($parts[5],0,3) != 'php')) + { + // ignore comments + if ($line{0} != '#') + { + $times['error'] .= $line; + } + } + elseif (strpos($line,$this->cronline) !== False) + { + $cron_units = array('min','hour','day','month','dow'); + foreach($cron_units as $n => $u) + { + $times[$u] = $parts[$n]; + } + $times['cronline'] = $line; + } + else + { + $this->other_cronlines[] = $line; + } + } + @pclose($crontab); + } + return $times; + } + + /** + * installs /phpgwapi/cron/asyncservices.php as cron-job + * + * Not implemented for Windows at the moment, always returns 0 + * + * @param array $times array with keys 'min','hour','day','month','dow', not set is equal to '*'. + * False means de-install our own crontab line + * @return mixed the times asyncservices are run, False if they are not installed, + * 0 if crontab not found and ' ' if crontab is deinstalled + */ + function install($times) + { + if ($this->only_fallback && $times !== False) { + return 0; + } + $this->installed(); // find other installed cronlines + + if (($crontab = popen('/bin/sh -c "'.$this->crontab.' -" 2>&1','w')) !== False) + { + if (is_array($this->other_cronlines)) + { + foreach ($this->other_cronlines as $cronline) + { + fwrite($crontab,$cronline); // preserv the other lines on install + } + } + if ($times !== False) + { + $cron_units = array('min','hour','day','month','dow'); + $cronline = ''; + foreach($cron_units as $cu) + { + $cronline .= (isset($times[$cu]) ? $times[$cu] : '*') . ' '; + } + $cronline .= $this->php.' -qC '.$this->cronline."\n"; + //echo "

Installing: '$cronline'

\n"; + fwrite($crontab,$cronline); + } + @pclose($crontab); + } + return $times !== False ? $this->installed() : ' '; + } +}