Location: PHPKode > scripts > PHP_Scheduler > php_scheduler/class.Scheduler.php
<?php
// THIS NEEDS TO BE RUN ON THE PHP CGI, PREFERABLY FROM THE CMDLINE, NOT THE APACHE MODULE
// 4 SPACE TABS. "\t" -> "    "

/************************************************************************************
 * 
 * Scheduler 0.01
 * http://www.phpsex.com/classes/class.Scheduler.php.txt
 * 
 * 
 * Ryan Flynn (hide@address.com | www.parseerror.com | www.phpsex.com)
 * irc.dal.net->#php->pizza_milkshake
 * 10 JUL 2002
 * 
 *
 * I HAVE COMPLETELY REWRITTEN THIS CLASS. THIS VERSION DOES NOT WORK THE
 * SAME AS 0.5.1 FROM JUN 2001. THIS IS MUCH BETTER.
 * 
 * THIS IS A 0.01 RELEASE. ASSUME NOTHING. SUSPECT EVERYTHING. IF YOU'RE
 * HAVING PROBLEMS, SEE IF YOU CAN GET THIS SIMPLE EXAMPLE TO WORK. IF NOT,
 * TRY TO FIGURE OUT WHY, AND EMAIL ME. THE MORE YOU TELL ME THE MORE I CAN
 * HELP YOU.
 * 
 * Update History:
 * 10 JUL 2002 - Version 0.01 done. Took me about 4 hours.
 * So far it parses some sexy date rules, runs stuff and 
 * optionally writes to an uber-cheesey logfile
 *
 * 
 * WHY: This class is for those who are not lucky enough to have access to
 * cron[tab] on *nix systems
 * 
 * 
 * WARNING: this is for running commands on your *nix server.
 * Running just any commands can be very, very dangerous and
 * can do alot of damage. Be careful!
 * 
 * DATE SYNTAX EXAMPLES:
 * 
 *	Remember:
 *		 - Whitespace (space, tab, newline) - delimited fields
 *       - Single values, sets, ranges, wildcards
 * 
 * SECOND	MINUTE				HOUR		DAY		MONTH
 * *		*					*			*		*		(every second)
 * 0,30 	*					*			*		*		(every 30 seconds)
 * 0		0,10,20,30,40,50	*			*		*		(every 10 minutes)
 * 0		0					*			*		*		(beginning of every hour)
 * 0		0					0,6,12,18	*	 	*		(at midnight, 6am, noon, 6pm)
 * 0		0					0			1-7&Fri	*		(midnight, first Fri of the month)
 * 0		0					0			1-7!Fri	*		(midnight, first Mon-Thu,Sat-Sun of the month)
 * 
 * 
 * Example usage:
 * 
 * require "class.Scheduler.php";
 * $bob = new Scheduler();
 * $bob->setLogFile("bob.log", 0755) or die("Don't have access to bob.log");
 * // run this command every 5 minutes
 * $bob->addTask("perl somescript.pl", "0 0,5,10,15,20,25,30,35,40,45,50,55 * * *");
 * // run this command midnight of the first Friday of odd numbered months
 * $bob->addTask("php -q somescript.php", "0 0 0 1-7&Fri 1,3,5,7,9,11");
 * // also run this command midnight of the second Thursday and Saturday of the even numbered months
 * $bob->addTask("php -q somescript.php", "0 0 0 8-15&Thu,8-15&Sat 2,4,6,8,10,12");
 * $bob->run();
 * 
 ***************************************************************************************************/

set_time_limit(0);

class Scheduler {

	var $tasks = array();

	var $uid_counter = 1; // every time a task is added it will get a fresh uid even if immediately removed
	var $logfile = false;
	var $chmod = false;

	function Scheduler(){ }

	function setLogFile($file, $chmod = 0700){
	// returns false if can't touch or chmod
		$this->logfile = $file;
		$this->chmod = $chmod;
		return (touch($file) && chmod($file, $chmod));
	}

	function addTask($cmd, $rules){

		$ds = new SchedulerDate($rules);

		//print_r($ds);

		$this->uid_counter++;

		$this->tasks[] =
			array(
				"uid" => $this->uid_counter,
				"rules" => $ds,
				"cmd" => $cmd
			);

		return $this->uid_counter;
	}

	function removeTask($uid){

		$found = 0;
		for ($i = 0; $i < sizeof($this->tasks); $i++)
			if ($this->tasks["uid"] == $uid){
				$found = $i;
				array_splice($this->tasks, $found); // nuke entry
				break;
			}
		return $found;
	}

	function run(){
		
		if (!sizeof($this->tasks))
			die("Give me some tasks with the ->addTask() method before you ask me to run anything.");

		while (1){
			
			$t = time();

			// check each task's candidacy
			foreach ($this->tasks as $task)
				if ($task['rules']->nowMatches())
					$this->runcmd($task);

			// wait til the next second
			while (time() == $t)
				usleep(100000);
		}
	}


	/***** internal functions ******/

	function runcmd(&$task){

		exec($task["cmd"]);

		if ($this->logfile)
			$this->writelog($task["uid"], $task["cmd"]);
	}

	function writeLog($uid, $msg){
		if (!($f = fopen($this->logfile, 'a'))){
			echo "ERROR: Cannot write to logfile '".$this->logfile."'. Make sure user '".$_ENV["USER"]."' has write permissions on the file.";
			return;
		}
		$stamp = date('m-d-Y H:i:s');
		fwrite($f, "[$stamp] ran #$uid: $msg\n");
		fclose($f);
	}

}


class SchedulerDate {

	var $legalDays = array('MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN');

	var $sec;
	var $min;
	var $hour;
	var $day;
	var $month;

	function SchedulerDate($raw){

		$raw = strtoupper($raw); // this'll work for now, Mon -> MON, tUe -> TUE

		$this->parse($raw);

		print_r($this->day);
	}

	function nowMatches(){
		if (
			$this->monthMatches() &&
			$this->monthMatches() &&
			$this->dayMatches() &&
			$this->hourMatches() &&
			$this->minMatches() &&
			$this->secMatches()
		)
			return true;
		return false;
	}

	function monthMatches(){
		if ($this->month == '*')
			return true;

		$currentmonth = '-'.date('n').'-';

		if(strpos($this->month, $currentmonth) !== false)
			return true;

		return false;
	}

	function dayMatches(){

		if ($this->day["value"] == '*')
			return true;

		$currentdaynum = '-'.date('j').'-';
		$currentdaytxt = '-'.strtoupper(date('D')).'-';

		foreach ($this->day as $day)
			if (strpos($day["not"], $currentdaytxt) !== false){
				// do nothing
			} else {
				$v1 = strpos($day["value"], $currentdaynum);
				$v2 = strpos($day["and"], $currentdaytxt);
	
				if ($day["and"] && ($v1 && $v2))
					return true;
				else if (!$day["and"] && $v1)
					return true;
			}

		return false;

	}

	function hourMatches(){
		if ($this->hour == '*')
			return true;

		$currenthour = '-'.date('G').'-';

		if (strpos($this->hour, $currenthour) !== false)
			return true;

		return false;
	}

	function minMatches(){
		
		if ($this->min == '*')
			return true;

		$currentmin = '-'.intval(date('i')).'-';

		if (strpos($this->min, $currentmin) !== false)
			return true;

		return false;
	}

	function secMatches(){

		if ($this->sec == '*')
			return true;

		$currentsec = '-'.intval(date('s')).'-';

		if (strpos($this->sec, $currentsec) !== false)
			return true;

		return false;
	}


	function parse($str){

		$s = array();

		list($s["sec"], $s["min"], $s["hour"], $s["day"], $s["month"]) = split("[\n\t ]+", $str);

		foreach ($s as $k=>$v)
			if (strpos($v, '*') !== false)
				$s[$k] = array('*');
			else if (!$this->generallyDecentSyntax($v))
				die("Illegal syntax in '$v'\n");
			else
				$s[$k] = explode(",", $s[$k]);

		//print_r($s);

		if ($s["sec"][0] == '*'){
			$this->sec = '*';
		} else {
			for ($i = 0; $i < sizeof($s["sec"]); $i++)
				if ($this->isRange($s["sec"][$i]))
					$s["sec"][$i] = $this->expandRange($this->rangeVals($s["sec"][$i]));
			$this->sec = '-'.join('-', $s["sec"]).'-';
		}

		if ($s["min"][0] == '*'){
			$this->min = '*';
		} else {
			for ($i = 0; $i < sizeof($s["min"]); $i++)
				if ($this->isRange($s["min"][$i]))
					$s["min"][$i] = $this->expandRange($this->rangeVals($s["min"][$i]));
			$this->min = '-'.join('-', $s["min"]).'-';
		}

		if ($s["hour"][0] == '*'){
			$this->hour = '*';
		} else {
			for ($i = 0; $i < sizeof($s["hour"]); $i++)
				if ($this->isRange($s["hour"][$i]))
					$s["hour"][$i] = $this->expandRange($this->rangeVals($s["hour"][$i]));
			$this->hour = '-'.join('-', $s["hour"]).'-';
		}

		// day is gonna be hard
		if ($s["day"][0] == '*'){
			$this->day = '*';
		} else {
			for ($i = 0; $i < sizeof($s["day"]); $i++){
				$tmp = array();
				if (($char = $this->isCond($s["day"][$i])) !== false){
					if ($char == '&'){
						list($tmp["value"], $tmp["and"]) = explode($char, $s["day"][$i]);
						if ($this->isRange($tmp["and"]))
							$tmp["and"] = $this->expandRange($this->rangeVals($tmp["and"]));
					} else {
						list($tmp["value"], $tmp["not"]) = explode($char, $s["day"][$i]);
						if ($this->isRange($tmp["not"]))
							$tmp["not"] = $this->expandRange($this->rangeVals($tmp["not"]));
					}
				}else{
					$tmp = array("value" => $s["day"][$i]);
				}
				
				$s["day"][$i] = $tmp;

				if ($this->isRange($s["day"][$i]["value"]))
					$s["day"][$i]["value"] = $this->expandRange($this->rangeVals($s["day"][$i]["value"]));
			}
			$this->day = $s["day"]; // no join
		}

		if ($s["month"][0] == '*'){
			$this->month = '*';
		} else {
			for ($i = 0; $i < sizeof($s["month"]); $i++)
				if ($this->isRange($s["month"][$i]))
					$s["month"][$i] = $this->expandRange($this->rangeVals($s["month"][$i]));
			$this->month = '-'.join('-', $s["month"]).'-';
		}

	}

	function isCond($s){
		if (strpos($s, '&') !== false)
			return '&';
		else if (strpos($s, '!') !== false)
			return '!';
		else
			return false;
	}

	function isRange($s){
		if (preg_match('/^\w+\-\w+/', $s))
			return true;
		else
			return false;
	}

	function isCondRange($s){
		if (isCond($s) && isRange($s))
			return true;
		else
			return false;
	}

	function isCondVal($s){
		if (isCond($s) && !isRange($s))
			return true;
		else
			return false;
	}

	function rangeVals($s){
		//echo "rangeVals: '$s'\n";
		return explode('-', $s);
	}

	function expandRange($l, $h = ""){
		// expand range from M-F -> "-M-T-W-R-F-" and 1-5 -> "-1-2-3-4-5-"

		if (is_array($l))
			list($l, $h) = $l;

		//echo "expandRange: $l $h\n";

		if ($this->isDigit($l)){
			if ($this->isAlpha($h))
				die("Invalid range '$l-$h' ... can't mix letters and numbers.");
			else if(!$this->isDigit($h))
				die("Invalid value '$h' in range '$l-$h'");

			// currently there is no possible reason to need to do a range beyond 0-59 for anything
			if ($l < 0)
				$l = 0;
			else if ($l > 59)
				$l = 59;
			
			if ($h < 0)
				$h = 0;
			else if ($h > 59)
				$h = 59;

			if ($l > $h){
				$tmp = $l;
				$l = $h;
				$h = $tmp;
				unset($tmp);
			}

			// for some reason range() is fucking up w/o the explicit intval()s. weird.
			return '-'.join('-', range(intval($l), intval($h))).'-';

		} else if ($this->isAlpha($l)){
			if ($this->isDigit($h))
				die("Invalid range '$l-$h' ... can't mix letters and numbers.");
			else if (!$this->isAlpha($h))
				die("Invalid value '$h' in range '$l-$h'");

			$d1 = $this->dayValue($l);
			$d2 = $this->dayValue($h);

			if ($d1 > $d2){
				$tmp = $d1;
				$d1 = $d2;
				$d2 = $tmp;
				unset($tmp);
			}

			$r = '-';
			for ($i = $d1; $i <= $d2; $i++)
				$r .= $this->legalDays[$i] . '-';
			
			return $r;

		} else { //invalid
			die("Invalid value '$l' in range '$l-$h'");
		}
	}

	function dayValue($s){
		for ($i = 0; $i < sizeof($this->legalDays); $i++)
			if ($this->legalDays[$i] == $s)
				return $i;

		return -1;
	}

	function dayValue($s){
		for ($i = 0; $i < sizeof($this->legalDays); $i++)
			if ($this->legalDays[$i] == $s)
				return $i;

		return -1;
	}

	function isDigit($s){
		if (preg_match('/^\d+$/', $s))
			return true;
		else
			return false;
	}

	function isAlpha($s){
		if ($this->isLegalDay($s))
			return true;
		else
			return false;
	}

	function isLegalDay($s){
		if (in_array($s, $this->legalDays))
			return true;
		else
			return false;
	}

	function generallyDecentSyntax($s){
		if ($s == '*' || preg_match('/^\d+(-\d+)?([!&][A-Z\*]+(-[A-Z\*]+)?)?(,\d+(-\d+)?([!&][A-Z\*]+(-[A-Z\*]+)?)?)*$/', $s))
			return true;
		return false;
	}

}

	/***
	$bob = new Scheduler();
	$bob->setLogFile("/tmp/bob.log", 0755);
	$bob->addTask("echo \"wazaaaaa\\n\" >> somefile", "0,5,10,15,20,25,30,35,40,45,50,55 * * * *");
	$bob->run();
	***/

?>
Return current item: PHP_Scheduler