Direkt zum Inhalt | Direkt zur Navigation

Benutzerspezifische Werkzeuge
Anmelden
Home Produkte

SaaS

Software as a Service

Hosting

schon ab €14,00!

Software-Entwicklung

Wir programmieren alles, was wir hosten.

Lösungen

Web & E-Commerce

Kollaboration

Hosting & Skalierbarkeit

Support Open Source

Python

PHP

Weitere

Über uns

Kontakt

Wir

Sie sind hier: Startseite Open Source PHP Cron.php4 Cron.php Sourcecode und Dokumentation

Cron.php Sourcecode und Dokumentation

<?php
/*
* File: Cron.php
*
* Copyright (c) 2007 by Daniel Kraft <dk@d9t.de>
*
* GNU General Public License (GPL)
*
* 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.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*
* If you find bugs, please feel free to report them by mail or on
* irc: freenode://#d9t
*
*
* Personal Note
* =========================
*
* I wrote this in php because I had to implement this in a php-
* project for a customer. I tried to make it as beautiful as
* possible, but php makes this very hard.
*
* I'm not only talking about cryptic methodnames (substr, usort, ...)
* and unpredictable parameter-order (strpos(haystack,needle) and
* array_search(needle,haystack) and many more), but also about
* the limited OO (also in php5) and the missing namespaces.
* You will have to touch every second line in this code if any
* of your used code uses a classname used here.
*
* The solution would of course be, to prefix any classname
* by something_. But this would make the code even less readable.
* The php-syntax does the rest.
*
* I've had the privilege to learn ruby 3 years ago and python
* 1.5 years ago. And since then, I'm very confused why anyone
* would write php.
*
* Often, I wanted to cry out, because most of this code would be
* writable in less than 50% of the lines in python. And it would
* also be much more readable. And it would have namespaces
* automatically, so it wouldn't touch any existing code. And I
* would have a shell where I could inspect objects and get instant
* help to any object.
*
* So please: Learn python:
* http://docs.python.org/tut/node3.html
*
*/

include_once("Exception.php");

class Cron {
/*
* Class to parse a single crontab entry and tell, if it matches a given timestamp.
* PLEASE NOTE: It requires a timestamp object - not just an integer.
*/

var $cron_entry;
var $timestampObj;

var $minuteField;
var $hourField;
var $domField;
var $monthField;
var $dowField;

function Cron($cron_entry, $timestampObj=null) {
/*
* constructor method
*/

$this->cron_entry = $cron_entry;
if ($timestampObj == null) $timestampObj = new Timestamp(time());
$this->timestampObj = $timestampObj;
$this->loadFields();
}

function splitFields($fields) {
/*
* Split fields by tab+ or space+ with limit 5 (like cron wants it)
*/
return preg_split('#(\t+|\s+)#', $fields,5);
}

function loadFields() {
$fields = $this->splitFields($this->cron_entry);
$this->minuteField = new MinuteField($fields[0]);
$this->hourField = new HourField($fields[1]);
$this->domField = new DomField($fields[2]);
$this->monthField = new MonthField($fields[3]);
$this->dowField = new DowField($fields[4]);
}

function hit() {
/*
* Checks, if $this->now is a hit for the cron_entry.
*/
return $this->minuteField->hit($this->timestampObj)
&& $this->hourField->hit($this->timestampObj)
&& $this->domField->hit($this->timestampObj)
&& $this->monthField->hit($this->timestampObj)
&& $this->dowField->hit($this->timestampObj);
}

function jumpToTime($timestampObj) {
/*
* This accepts a timestampObj and sets the current entry to this time.
* Do this if you want to test a specific time for a hit.
*/
$this->timestampObj = $timestampObj;
}

function moveInTime($deltaTimestampObj) {
$deltaTimestampObj->applyToTimestamp($this->timestampObj);
}

function _moveInTime($seconds) {
$this->timestampObj->_moveInTime($seconds);
}

function getTimestamp() {
return $this->timestampObj;
}

function debug() {
/*
* call debug on each field.
*/
print "\nChecking Date: ".$this->timestampObj->rfc2822()."\n========================================\n\n";

$this->minuteField->debug($this->timestampObj);
$this->hourField->debug($this->timestampObj);
$this->domField->debug($this->timestampObj);
$this->monthField->debug($this->timestampObj);
$this->dowField->debug($this->timestampObj);

print "\n----\n";
print "Sum hit: ".$this->hit()."\n";
}
}

class Searcher {
/*
* This is a base class for all Searcher Classes.
* Searcher Classes are a implementation of a searching algorithm
* which tries to find the closest previous or next hit for a
* Cron.
*
* The methods must return a Timestamp object.
*/

var $cron;

function Searcher($cron) {
$this->cron= $cron;
}

function previous() {
// dummy implementation
return $this->cron->getTimestamp();
}

function next() {
// dummy implementation
return $this->cron->getTimestamp();
}
}

class DumbIterateSearcher extends Searcher {
/*
* This searcher simply checks from now on every minute
* forward/backwards in time for a hit and returns the closest match.
*
* Stupid, simple, but correct.
* Use this only as reference.
*/

function DumbIterateSearcher($cron) {
parent::Searcher($cron);
}

function _iterate($deltaTimestampObj) {
$maxIterations = 4207550;
$currentIteration = 0;
while (!$this->cron->hit() && $maxIterations>$currentIteration) {
$deltaTimestampObj->applyToTimestamp($this->cron->timestampObj);
$currentIteration++;
}
$this->cron->timestampObj->zeroSeconds();
return $this->cron->getTimestamp();
}

function previous() {
$deltaTimestampObj = new DeltaTimestampObj();
$deltaTimestampObj->minutes(-1);
$timestampObj = $this->_iterate($deltaTimestampObj);
return $timestampObj;
}

function next() {
$deltaTimestampObj = new DeltaTimestampObj();
$deltaTimestampObj->minutes(1);
$timestampObj = $this->_iterate($deltaTimestampObj);
return $timestampObj;
}
}


class EliminateSearcher extends Searcher {
/*
* This Searcher checks for each field, if it hits.
* If not, it asks this field for the amount of time we could
* add/substract until it possibly would hit the next time.
*
* This looks very performant. Even in cases like this:
* 0 0 29 2 3
*/

function EliminateSearcher($cron) {
parent::Searcher($cron);
}

function _iterate() {
/*
* Idea: Maybe this could perform a little better, if we tried the
* algo always from start when successfully applying a delta to
* anything except month. Because a delta so big until it hits the
* next superior boundery is possible.
* But this becomes academical here. My worst cpu-time for this
* was below 0.25s when hopping over 20 years.
*/
for ($i=0; $i<100000; $i++) {
while (!$this->cron->monthField->hit($this->cron->timestampObj))
$this->cron->monthField->timespan->applyToTimestamp($this->cron->timestampObj);
while (!$this->cron->domField->hit($this->cron->timestampObj))
$this->cron->domField->timespan->applyToTimestamp($this->cron->timestampObj);
while (!$this->cron->dowField->hit($this->cron->timestampObj))
$this->cron->dowField->timespan->applyToTimestamp($this->cron->timestampObj);
while (!$this->cron->hourField->hit($this->cron->timestampObj))
$this->cron->hourField->timespan->applyToTimestamp($this->cron->timestampObj);
while (!$this->cron->minuteField->hit($this->cron->timestampObj))
$this->cron->minuteField->timespan->applyToTimestamp($this->cron->timestampObj);
if ($this->cron->hit()) return;
}
new EException("No hit was found. Max iterations reached.",1);
}

function previous() {
$this->cron->minuteField->timespan->negative();
$this->cron->hourField->timespan->negative();
$this->cron->domField->timespan->negative();
$this->cron->monthField->timespan->negative();
$this->cron->dowField->timespan->negative();

$this->_iterate();

// wipe the seconds to make it reproducable.
$this->cron->timestampObj->zeroSeconds();
return $this->cron->timestampObj;
}

function next() {
$this->cron->minuteField->timespan->positive();
$this->cron->hourField->timespan->positive();
$this->cron->domField->timespan->positive();
$this->cron->monthField->timespan->positive();
$this->cron->dowField->timespan->positive();

$this->_iterate();

// wipe the seconds to make it reproducable.
$this->cron->timestampObj->zeroSeconds();
return $this->cron->timestampObj;
}
}

class Timestamp {
/*
* This class is mainly a wrapper for a integer-timestamp with
* some methods using the date() function. It's more convenient
* and nicer to read if we use that class.
*/

var $stamp;

function Timestamp($stamp) {
/*
* recieves a integer timestamp in unix-format (seconds since 1970 or so).
*/
$this->stamp = $stamp;
}

function copy() {
return new Timestamp($this->stamp);
}

function _date($str) {
return intval(date($str, $this->stamp));
}

function day() {
return $this->_date('j');
}

function hour() {
return $this->_date('G');
}

function minute() {
return $this->_date('i');
}

function second() {
return $this->_date('s');
}

function dom() {
return $this->_date('j');
}

function month() {
return $this->_date('n');
}

function dow() {
return $this->_date('w');
}

function daysInMonth() {
return $this->_date('t');
}

function zeroSeconds() {
/*
* removes the second-information of the timestamp.
*/
$this->stamp -= $this->second();
}

function _moveInTime($seconds) {
$this->stamp += $seconds;
}

function rfc2822() {
return date('r', $this->stamp);
}

function sqlDatetime() {
return date('Y-m-d H:i:s', $this->stamp);
}

function diffToGmt() {
/*
* Returns the number of minutes of delta to gmt.
*/
$o = date('O', $this->stamp);
$sign = substr($o,0,1);
$hours = intval($sign.substr($o,1,2));
$minutes = intval($sign.substr($o,3,2));
$diff = $hours*60+$minutes;
return $diff;
}
}

class DeltaTimestampObj {
/*
* This class represents a delta timstamp which can be positive
* or negative.
* You can set the delta convenient with several methods and after
* you're done, apply the change to your favourite Timestamp object.
*
* This method returns the latest possible value for negative
* calls and the earliest possible value for positive calls.
*/

var $delta = array('seconds'=>0, 'minutes'=>0, 'hours'=>0, 'days'=>0, 'weeks'=>0, 'months'=>0);
var $applyTimeZone = true;

function DeltaTimestampObj($months=0, $days=0, $hours=0, $minutes=0, $seconds=0) {
$this->delta["months"] = $months;
$this->delta["days"] = $days;
$this->delta["hours"] = $hours;
$this->delta["minutes"] = $minutes;
$this->delta["seconds"] = $seconds;
}

function negative() {
/*
* changes the sign of all values to negative.
*/
foreach (array_keys($this->delta) as $index)
if ($this->delta[$index]>0) $this->delta[$index] = -$this->delta[$index];
}

function positive() {
/*
* changes the sign of all values to positive.
*/
foreach (array_keys($this->delta) as $index)
if ($this->delta[$index]<0) $this->delta[$index] = -$this->delta[$index];
}

function applyToTimestamp(&$timestampObj) {
/*
* apply the changes in the following order:
* months, days, hours, minutes, seconds
*
* This is no generic method. It's optimized for cron as it
* returns always the first possible second or the last possible
* second for a question and not the correct delta to a given
* timestamp.
*/

/*
printf ("Applying: %6dm %6dd %6dh %6dm %6ds\n",
$this->delta["months"],
$this->delta["days"],
$this->delta["hours"],
$this->delta["minutes"],
$this->delta["seconds"]);
*/

if ($this->delta["months"] < 0) {
for ($i=0; $i>$this->delta["months"]; $i--) {
$days = -$timestampObj->day();
$dt = new DeltaTimestampObj(0,$days,0,0,0);
$dt->applyToTimestamp($timestampObj);
}
}
if ($this->delta["months"] > 0) {
for ($i=0; $i<$this->delta["months"]; $i++) {
$days = $timestampObj->daysInMonth() - $timestampObj->day() + 1;
$dt = new DeltaTimestampObj(0,$days,0,0,0);
$dt->applyToTimestamp($timestampObj);
}
}

if ($this->delta["days"] < 0) {
$hours = $this->delta["days"]*24 + 23 - $timestampObj->hour();
$dt = new DeltaTimestampObj(0,0,$hours,0,0);
$dt->applyToTimestamp($timestampObj);
}
if ($this->delta["days"] > 0) {
$hours = $this->delta["days"]*24 - $timestampObj->hour();
$dt = new DeltaTimestampObj(0,0,$hours,0,0);
$dt->applyToTimestamp($timestampObj);
}

if ($this->delta["hours"] < 0) {
$minutes = $this->delta["hours"]*60 + 59 - $timestampObj->minute();
$dt = new DeltaTimestampObj(0,0,0,$minutes,0);
$dt->applyToTimestamp($timestampObj);
}
if ($this->delta["hours"] > 0) {
$minutes = $this->delta["hours"]*60 - $timestampObj->minute();
$dt = new DeltaTimestampObj(0,0,0,$minutes,0);
$dt->applyToTimestamp($timestampObj);
}

if ($this->delta["minutes"] < 0) {
$diffToGmtBefore = $timestampObj->diffToGmt();
$seconds = $this->delta["minutes"]*60 + 59 - $timestampObj->second();
$timestampObj->_moveInTime($seconds);
$diffToGmtAfter = $timestampObj->diffToGmt();
$this->applyTimeZone($timestampObj, $diffToGmtBefore, $diffToGmtAfter);
}
if ($this->delta["minutes"] > 0) {
$diffToGmtBefore = $timestampObj->diffToGmt();
$seconds = $this->delta["minutes"]*60 - $timestampObj->second();
$timestampObj->_moveInTime($seconds);
$diffToGmtAfter = $timestampObj->diffToGmt();
$this->applyTimeZone($timestampObj, $diffToGmtBefore, $diffToGmtAfter);
}

$timestampObj->_moveInTime($this->delta["seconds"]);
}

function applyTimeZone(&$timestampObj, $diffToGmtBefore, $diffToGmtAfter) {
// "correct" timezone changes (like summer/wintertime)
if (!$this->applyTimeZone) return;
$deltaMinutes = $diffToGmtBefore - $diffToGmtAfter;
if ($deltaMinutes)
$timestampObj->_moveInTime($deltaMinutes*60);
}

function seconds($val) {
$this->delta["seconds"] += $val;
}

function minutes($val) {
$this->delta["minutes"] += $val;
}

function hours($val) {
$this->delta["hours"] += $val;
}

function days($val) {
$this->delta["days"] += $val;
}

function weeks($val) {
$this->delta["weeks"] += $val;
}

function months($val) {
$this->delta["months"] += $val;
}
}

class CronField {
/*
* Class to parse a cron field. They can contain ranges
* or comma seperated lists of numbers and ranges.
* From the crontab (5) manpage:
*
* A field may be an asterisk (*), which always stands for ‘‘first-last’’.
* Ranges of numbers are allowed. Ranges are two numbers separated with
* a hyphen. The specified range is inclusive. For example, 8-11 for an ‘‘hours’’
* entry specifies execution at hours 8, 9, 10 and 11.
*
* Lists are allowed. A list is a set of numbers (or ranges) separated by commas.
* Examples: ‘‘1,2,5,9’’, ‘‘0-4,8-12’’.
*
* Step values can be used in conjunction with ranges. Following a range with
* ‘‘/<number>’’ specifies skips of the number’s value through the range.
* For example, ‘‘0-23/2’’ can be used in the hours field to specify command
* execution every other hour (the alternative in the V7 standard is ‘‘0,2,4,6,8,10,12,14,16,18,20,22’’).
* Steps are also permitted after an asterisk, so if you want to say ‘‘every two hours’’, just use ‘‘* /2’’
* Names can also be used for the ‘‘month’’ and ‘‘day of week’’ fields. Use the
* first three letters of the particular day or month (case doesn’t matter). Ranges or lists
* of names are not allowed.
*/

var $field = "";
var $activeValues = array();
var $timespan; # contains a DeltaTimestampObj which knows how long one iteration of this field is.

function CronField($field, $possibleValues, $timespan) {
/*
* Constructor.
* Initialize all arrays as needed.
*/

// Just the contents of the field as string.
$this->field = $field;

/*
* create a list with key is value and value is true or false - depending
* on if it's active or not.
*/
foreach ($possibleValues as $possibleValue) {
$this->activeValues[$possibleValue] = false;
}

$this->parseField();

$this->timespan = $timespan;
}

function activate($value) {
/*
* activates a key in $activeValues
*/
$value = $this->correctValue($value);
if (isset($this->activeValues[$value]))
$this->activeValues[$value] = true;
else
new EException('Illegal value: '.$value, 2);
}

function correctValue($val) {
/*
* This is a callback method that should be overwritten if you
* need to support correction of entries like if you need
* 0 or 7 for sunday, or if you want to use names.
* Simply react on $val and return any valid int.
*/

// Default: Do nothing
return $val;
}

function activateRange($range) {
/*
* Activates a range. A range is a string which could contain following values:
* 1-3
* 1-4/2
* *
* * /3 (ignore the whitspace)
* and also following legal values, which wrap over the bounds:
* 5-1
* 5-5
* and also following illegal values.
* 1-1/0.5
*/
$matches = array();
if (strpos($range, '*') === 0) {
$tmpmatches = array();
$valid = preg_match('#^\*/?([0-9]*)$#', $range, $tmpmatches);
# modify it to the other kind of range:
$values = $this->getPossibleValues();
$matches[1] = array_shift($values);
$matches[2] = array_pop($values);
$matches[3] = $tmpmatches[1];
} else {
$valid = preg_match('#^([0-9a-zA-Z]+)-([0-9a-zA-Z]+)/?([0-9]*)$#', $range, $matches);
}

if (!$valid)
new EException("Invalid Range: ".$range, 3);

$begin = $this->correctValue($matches[1]);
$end = $this->correctValue($matches[2]);
$step = $matches[3] ? $matches[3] : 1;

/*
* Algorithm:
* We get the list getPossibleValues twice and search for $begin and
* remember the position.
* Then we search for $end AFTER the position of $begin and also remember
* the position.
* Then we have two indices between which we only have to iterate with $step.
*/

$values = $this->getPossibleValues();

// special case: If step is bigger than the list is long, DON'T activate
// anything. If you don't like this behavour, simply wipe out the following
// line. Then the value in $begin is activated. I consider this a bug though.
if ($step > count($values)) return;

$doubleValues = array_merge($values, $values);
$beginPos = array_search($begin, $doubleValues);
$endPos = array_search($end, $doubleValues);
if ($endPos <= $beginPos)
$endPos += count($values);

// iterate. And include edges.
for ($i = $beginPos; $i <= $endPos; $i += $step) {
$this->activate($doubleValues[$i]);
}
}

function getActiveValues() {
/*
* Returns a list of values where active == true
*/
$ret = array();
foreach ($this->activeValues as $value => $active)
if ($active) $ret[] = $value;
sort($ret);
return $ret;
}

function isActive($value) {
/*
* checks if a given value is active
*/
return $this->activeValues[$value];
}

function getPossibleValues() {
/*
* Returns all possible values in a list.
*/
$ret = array_keys($this->activeValues);
sort($ret);
return $ret;
}

function parseField() {
/*
* Parses the contents of the field and creates active values from it.
* This method handles the following possibilities:
*
* **range**
* Something in the format #^([0-9]+)-([0-9]+)/?([0-9]*)$#
*
* **range with wildcard**
* Something in the format #^* /?([0-9]*)$#
*
* **single value**
* like 4 or Sun
*/
$entries = split(',',$this->field);
foreach ($entries as $entry) {
if (strval(intval($entry)) == $entry || (strpos($entry,"-")===false && strpos($entry,"*")===false)) {
$this->activate($entry);
} else {
$this->activateRange($entry);
}
}
}

function hit($timestampObj) {
/*
* Checks, if the given timestamp-object is a hit for this field.
* PLEASE NOTE: THIS IS NOT AN INTEGER!
* returns true, if hit.
*/
return false;
}

function debug($timestampObj) {
/*
* outputs the state of the object in a nice way
*/
print get_class($this)."\n";
print join(',', $this->getActiveValues());
print "\n";
print "Hit: ".$this->hit($timestampObj)."\n";
print "\n";
}

}

class MinuteField extends CronField {
function MinuteField($field) {
parent::CronField($field, range(0,59), new DeltaTimestampObj(0,0,0,1,0));
}

function hit($timestampObj) {
return $this->isActive($timestampObj->minute());
}
}

class HourField extends CronField {
function HourField($field) {
parent::CronField($field, range(0,23), new DeltaTimestampObj(0,0,1,0,0));
}

function correctValue($val) {
if ($val == 24) return 0;
return $val;
}

function hit($timestampObj) {
return $this->isActive($timestampObj->hour());
}
}

class DomField extends CronField {
function DomField($field) {
parent::CronField($field, range(0,31), new DeltaTimestampObj(0,1,0,0,0));
}

function hit($timestampObj) {
return $this->isActive($timestampObj->dom());
}
}

class MonthField extends CronField {
function MonthField($field) {
parent::CronField($field, range(1,12), new DeltaTimestampObj(1,0,0,0,0));
}

function hit($timestampObj) {
return $this->isActive($timestampObj->month());
}
}

class DowField extends CronField {
function DowField($field) {
parent::CronField($field, range(0,6), new DeltaTimestampObj(0,1,0,0,0));
}

function correctValue($val) {
if ($val == 7) return 0;

$weekdays = array('sun','mon','tue','wed','thu','fri','sat');
if ($pos = array_search(strtolower($val), $weekdays)) return $pos;

return $val;
}

function hit($timestampObj) {
return $this->isActive($timestampObj->dow());
}
}

class ConvenientCron {
/*
* This wraps several methods into single, most-likely-usecases.
*/

var $cron;
var $searcher;
var $error;

function ConvenientCron($cron_entry, $timestamp=null) {
/*
* cron_entry is something like "30 7 0 * *"
* timestamp is a integer unix-timestamp.
*/
if (!$timestamp) $timestamp = time();
$this->cron = new Cron($cron_entry, new Timestamp($timestamp));
if ($ex = ecatch()) {
$this->error = $ex->toString();
}
}

function next() {
/*
* Returns a Timestamp object
* It emulates an iterator so you can call it multiple times.
*/
if ($this->error) return new Timestamp(0); // sorry, w/o exception handling I don't know any better way.
if (!$this->searcher) $this->searcher = new EliminateSearcher($this->cron);
$to = $this->searcher->next();
$ret_to = $to->copy(); // php4, thanks guys!
$this->searcher->cron->moveInTime(new DeltaTimestampObj(0,0,0,1,0));
return $ret_to;
}

function previous() {
/*
* Returns a Timestamp object
* It emulates an iterator so you can call it multiple times.
*/
if ($this->error) return new Timestamp(0);
if (!$this->searcher) $this->searcher = new EliminateSearcher($this->cron);
$to = $this->searcher->previous();
$ret_to = $to->copy(); // php4, thanks guys!
$this->searcher->cron->moveInTime(new DeltaTimestampObj(0,0,0,-1,0));
return $ret_to;
}
}

/*
* Example:
*/

/*
$c = new ConvenientCron("30 27 * * 1");
if ($ex = ecatch()) {
print $ex->toString();
//print $ex->getTraceAsString();
print count($GLOBALS["EXCEPTIONS"][$i]);
exit();
}

for ($i=0; $i<5; $i++) {
$t = $c->next();
print $t->rfc2822()."\n";
}
*/

?>