Page 1 of 1

PHPCronEmulator

Posted: Tue Aug 21, 2007 1:39 pm
by s.dot
So, I've rewrote this, and changed the name, because the ones I tried were already taken. I also just edited an old topic to avoid creating a new one.

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);
        }
    }
}

Posted: Tue Aug 21, 2007 10:19 pm
by s.dot
I'm thinking this would be much better suited with a GUI. For example, a web page telling the status, buttons to start/stop, view/clear the log file, edit the config task file, etc.

As it stands, it's way too confusing.

Does anyone know of an existing php code library like the one i've written? (or does something similar) I'd hate to "re-invent the wheel".

Posted: Wed Aug 22, 2007 6:17 am
by VladSun

Posted: Thu Aug 30, 2007 2:20 am
by s.dot
I've edited the initial post, to spare a new topic. New critiques/comments/suggestions are welcome.

I'm now working on a web administration panel to create/edit config/tasks/log files, and to start/stop the script.

Posted: Thu Aug 30, 2007 5:00 pm
by s.dot
I'm really starting to think there is no market for this kind of script. :lol: Perhaps I should've researched that a little better before I went gung-ho on it.