The below script requires a config file, and a tasks file. Along with a running.txt file and a log.txt file. I am in the process of creating a web administration script that will create these files, so it will be easy for the end user and properly parsed by the below script.
What this does is checks (in real time) for updated config/tasks files, and loads them into an array. Then checks against that array every 60 seconds to see if a scheduled task should be exected. If it should, then it optionally emails the output. This will run continuously, however I've unset() all variables so memory usage should be very minimal.
You can add/edit tasks and configuration without stopping the instance of this script. Also I will have a section in the web admin to start/stop this script.
Code: Select all
<?php
/*
** Critique only. Do not use, do not redistribute, do not modify
*/
class PHPCronEmulator
{
/*
** Stores the complete list of tasks to be ran.
*/
private $_completeTaskList = array();
/*
** Stores the md5_file() hash of the configuration file.
*/
private $_configHash;
/*
** Stores the md5_file() hash of the tasks file.
*/
private $_taskHash;
/*
** Whether or not to log errors to the log file.
*/
private $_logErrors = true;
/*
** Whether or not to log successfully ran tasks to the log file.
*/
private $_logSuccess = true;
/*
** Whether or not to log events to the log file.
*/
private $_logEvents = true;
/*
** Email address to send task output to, if desired.
*/
private $_email;
/*
** The constructor method checks if an instance is already running.
** @access public
*/
public function __construct()
{
if ($this->_isRunning())
{
if ($this->_logErrors)
{
$this->_log('Error', 'PHPCronEmulator attempted to start but an instance was already running.', time());
}
trigger_error('PHPCronEmulator attempted to start but an instance was already running.', E_USER_ERROR);
}
}
/*
** Compares two hashes
** @access private
*/
private function _compareHashes($hash1, $hash2)
{
return $hash1 == $hash2;
}
/*
** Loads the configuration file and sets appropriate variables.
** @access private
*/
private function _loadConfigFile()
{
$configFile = trim(file_get_contents('txt/config.txt'));
$this->_checkIfKilled($configFile);
$lines = array_filter(explode("\n", $configFile));
foreach ($lines AS $line)
{
$line = explode('=', trim($line));
if (in_array(trim($line[0]), array('_email', '_logSuccess', '_logErrors', '_logEvents')))
{
$this->$line[0] = trim($line[1]);
}
}
//generate hash
$this->_configHash = md5_file('txt/config.txt');
//log
if ($this->_logEvents)
{
$this->_log('Event', 'Configuration file loaded.', time());
}
//cleanup
unset($configFile, $lines, $line);
}
/*
** Loads the tasks file, checks if the time supplied is valid, and if it is,
** adds the task to $this->_completeTaskList
** @access private
*/
private function _loadTaskFile()
{
$taskFile = trim(file_get_contents('txt/tasks.txt'));
if ($taskFile != '')
{
$taskFile = explode("\n", $taskFile);
foreach ($taskFile AS $task)
{
$task = explode("\t", $task);
if (file_exists(trim($task[0])))
{
if ($this->_isValidTimeFormat($task[1]))
{
$this->_completeTaskList[] = array('script' => trim($task[0]), 'time' => trim($task[1]), 'email' => trim($task[2]));
} else
{
if ($this->_logErrors)
{
$this->_log('Error', 'Could not evaluate time (' . $task[1] . ') given for script (' . $task[0] . ').', time());
}
trigger_error('Could not evaluate time (' . $task[1] . ') given for script (' . $task[0] . ').', E_USER_WARNING);
}
} else
{
if ($this->_logErrors)
{
$this->_log('Error', 'Could not locate script (' . $task[0] . ').', time());
}
trigger_error('Could not locate script (' . $task[0] . ').', E_USER_WARNING);
}
}
}
//generate hash
$this->_taskHash = md5_file('txt/tasks.txt');
if ($this->_logEvents)
{
$this->_log('Event', 'Task file loaded.', time());
}
//cleanup
unset($taskFile, $task);
}
/*
** Method to determine if a supplied crontab style time is a valid crontab
** style time.
** @param string $time
** @access private
*/
private function _isValidTimeFormat($time)
{
//minimum and maximum values for each part
$minValues = array(0, 0, 1, 1, 0);
$maxValues = array(59, 23, 31, 12, 6);
//get each part of the time separated
$timesTemp = array_filter(explode(' ', trim($time)));
//reset key values
$times = array();
foreach ($timesTemp AS $timeTemp)
{
$times[] = trim($timeTemp);
}
if (count($times) != 5)
{
return false;
}
//validate each part
$i=0;
foreach($times AS $time)
{
if (strpos($time, ','))
{
//we have separated values
$eachPart = explode(',', $time);
$count = count($eachPart);
if ($count == 2)
{
if (strpos($eachPart[0], '-') && strpos($eachPart[1], '-'))
{
//range, range
foreach ($eachPart AS $part)
{
$partsOfPart = explode('-', $part);
foreach($partsOfPart AS $partPart)
{
if (($partPart < $minValues[$i]) || ($partPart > $maxValues[$i]))
{
return false;
}
}
}
} elseif (strpos($eachPart[0], '-') && !strpos($eachPart[1], '-'))
{
//range, singlevalue
$partsOfPart = explode('-', $eachPart[0]);
foreach ($partsOfPart AS $partPart)
{
if (($partPart < $minValues[$i]) || ($partPart > $maxValues[$i]))
{
return false;
}
}
if (($eachPart[1]) < $minValues[$i] || ($eachPart[1] > $maxValues[$i]))
{
return false;
}
} elseif (!strpos($eachPart[0], '-') && strpos($eachPart[1], '-'))
{
//singlevalue,range
if (($eachPart[0]) < $minValues[$i] || ($eachPart[0] > $maxValues[$i]))
{
return false;
}
$partsOfPart = explode('-', $eachPart[1]);
foreach ($partsOfPart AS $partPart)
{
if (($partPart < $minValues[$i]) || ($partPart > $maxValues[$i]))
{
return false;
}
}
} else
{
//singlevalue,singlevalue
foreach ($eachPart AS $part)
{
if (($part < $minValues[$i]) || ($part > $maxValues[$i]))
{
return false;
}
}
}
} else
{
//list
foreach ($eachPart AS $part)
{
if (($part < $minValues[$i]) || ($part > $maxValues[$i]))
{
return false;
}
}
}
} elseif (strpos($time, '-'))
{
//we have a single range
$eachPart = explode('-', $time);
foreach ($eachPart AS $part)
{
if (($part < $minValues[$i]) || ($part > $maxValues[$i]))
{
return false;
}
}
} else
{
//we have a single number
if (($time != '*') && (($time < $minValues[$i]) || ($time > $maxValues[$i])))
{
return false;
}
}
$i++;
}
//cleanup
unset($time, $minValues, $maxValues, $timesTemp, $times, $timeTemp, $i);
return true;
}
/*
** Method to evaluate a crontab style time and determine if it is within
** the current minute.
** @param string $time
** @access private
*/
private function _taskTimeIsInMinute($time)
{
$minValues = array(0, 0, 1, 1, 0);
$maxValues = array(59, 23, 31, 12, 6);
$piecesTemp = array_filter(explode(' ', trim($time)));
$pieces = array();
foreach ($piecesTemp AS $piece)
{
$pieces[] = trim($piece);
}
$container = array();
//break each piece down into an array of allowed values
$i = 0;
foreach ($pieces AS $piece)
{
if (strpos($piece, ','))
{
$eachPart = explode(',', $piece);
$count = count($eachPart);
if ($count == 2)
{
if (strpos($eachPart[0], '-') && strpos($eachPart[1], '-'))
{
//range, range
$partsOfPart = explode('-', $eachPart[0] . '-' . $eachPart[1]);
$container[$i] = array_merge(range($partsOfPart[0], $partsOfPart[1]), range($partsOfPart[2], $partsOfPart[3]));
} elseif (strpos($eachPart[0], '-') && !strpos($eachPart[1], '-'))
{
$partsOfPart = explode('-', $eachPart[0]);
$container[$i] = array_merge(range($partsOfPart[0], $partsOfPart[1]), array($eachPart[1]));
} elseif (!strpos($eachPart[0], '-') && strpos($eachPart[1], '-'))
{
$partsOfPart = explode('-', $eachPart[1]);
$container[$i] = array_merge(range($partsOfPart[0], $partsOfPart[1]), array($eachPart[0]));
} else
{
foreach ($eachPart AS $part)
{
$container[$i][] = $part;
}
}
} else
{
foreach ($eachPart AS $part)
{
$container[$i][] = $part;
}
}
} elseif (strpos($piece, '-'))
{
$eachPart = explode('-', $piece);
$container[$i] = range($eachPart[0], $eachPart[1]);
} else
{
if ($piece == '*')
{
$container[$i] = range($minValues[$i], $maxValues[$i]);
} else
{
$container[$i] = array($piece);
}
}
$i++;
}
//remove duplicates
for ($i=0; $i<5; $i++)
{
if(!isset($container[$i]))
{
$container[$i] = array('0');
} else
{
$container[$i] = array_unique($container[$i]);
}
}
//generate valid timestamps for this task
//check if is in minute
if (in_array(date('n'), $container[3]) && (in_array(date('w'), $container[4]) || in_array(date('j'), $container[2])) && in_array(date('G'), $container[1]))
{
//loop through each minute, and generate a timestamp for that minute
//check if it is within the current minute
foreach($container[0] AS $minute)
{
$minute = mktime(date('G'), $minute, 0);
if (($minute > time()) && ($minute <= (time()+60)))
{
//cleanup
unset($time, $minValues, $maxValues, $piecesTemp, $pieces, $piece, $container, $i, $minute);
return true;
}
}
}
//cleanup
unset($time, $minValues, $maxValues, $piecesTemp, $pieces, $piece, $container, $i);
return false;
}
/*
** Sends email with output (if specified) of a task.
** @param string $output
** @param string $script
** @access private
*/
private function _sendOutputEmail($output, $script)
{
if (!@mail(
$this->_email,
'PHPCronEmulator - Output - ' . $script . ' - ' . date("n\d\Y \a\\t g:i A"),
$output
))
{
if ($this->_logErrors)
{
$this->_log('Error', 'Could not email script (' . $script . ') output. Check your server and/or PHP settings for mail().', time());
}
trigger_error('Could not email script (' . $script . ') output. Check your server and/or PHP settings for mail().', E_USER_WARNING);
}
unset($output, $script);
}
/*
** Logs a message to the log file.
** @param string $type - type of log entry
** @param string $msg - value of log entry
** @param integer $time - time of log entry
** @access private
*/
private function _log($type, $msg, $time)
{
$handle = fopen('log/log.txt', 'a');
flock($handle, LOCK_EX);
fwrite($handle, date("H:i:s", $time) . ' ' . $type . ' - ' . $msg . "\n");
flock($handle, LOCK_UN);
fclose($handle);
//cleanup
unset($type, $msg, $time, $handle);
}
/*
** Checks if PHPCronEmulator is currently running.
** @access private
*/
private function _isRunning()
{
$runningTime = (int) trim(file_get_contents('txt/running.txt'));
if ($runningTime >= (time()-60))
{
unset($runningTime);
return true;
}
unset($runningTime);
return false;
}
/*
** Updates the pid file so PHPCronEmulator can be determined as running.
** @access private
*/
private function _updateRunningTime()
{
$handle = fopen('txt/running.txt', "w");
flock($handle, LOCK_EX);
fwrite($handle, time());
flock($handle, LOCK_UN);
fclose($handle);
unset($handle);
}
/*
** Checks if PHPCronEmulator has been killed.
** @param string $configContent - contents of the configuration file.
** @access private
*/
private function _checkIfKilled($configContent)
{
//check if string *kill* was found in the contents
if (strpos($configContent, '*kill*') !== false)
{
//it was found, take it out
$configContent = str_replace('*kill*', '', $configContent);
//rewrite file
$handle = fopen('txt/config.txt', "w");
flock($handle, LOCK_EX);
fwrite($handle, $configContent);
flock($handle, LOCK_UN);
fclose($handle);
//log stoppage
if ($this->_logEvents)
{
$this->_log('Event', 'PHPCronEmulator stopped.', time());
}
//halt the script
exit;
}
unset($configContent);
}
/*
** Starts PHPCronEmulator
*/
public function run()
{
//set script to run "forever"
set_time_limit(0);
ignore_user_abort(true);
//infinite loop
while (true)
{
//update running time
$this->_updateRunningTime();
//if either of these values are not set, instance has just started running
if (($this->_configHash == null) || ($this->_taskHash == null))
{
if ($this->_logEvents)
{
$this->_log('Event', 'PHPCronEmulator started.', time());
}
}
//compare current hash against stored hash of the config file. If they
//don't match, load new configuration file
if (!$this->_compareHashes($this->_configHash, md5_file('txt/config.txt')))
{
$this->_loadConfigFile();
}
//compare current hash against stored hash of the tasks file. If they
//don't match, load new tasks file
if (!$this->_compareHashes($this->_taskHash, md5_file('txt/tasks.txt')))
{
$this->_loadTaskFile();
}
//check if we have any tasks
if (!empty($this->_completeTaskList))
{
//loop through each task
foreach ($this->_completeTaskList AS $task)
{
//determine if the tasks time is within the current minute
if ($this->_taskTimeIsInMinute($task['time']))
{
//include script and capture output
ob_start();
include $task['script'];
$output = ob_get_contents();
ob_end_clean();
//email output
if ($task['email'] == true)
{
if ($this->_email != null)
{
$this->_sendOutputEmail($output, $task['script']);
} else
{
if ($this->_logErrors)
{
$this->_log('Error', 'Could not email output for script (' . $task['script'] . ') because email address is not set.', time());
}
trigger_error('Could not email output for script (' . $task['script'] . ') because email address is not set.', E_USER_WARNING);
}
}
//log success
if ($this->_logSuccess)
{
$this->_log('Success', 'Script (' . $task['script'] . ') successfully executed.', time());
}
//cleanup
foreach (get_defined_vars() AS $k => $v)
{
if ($k != 'this')
{
unset($$k);
}
}
unset($k, $v);
}
}
}
//sleep one minute
sleep(60);
}
}
}