PHPCronEmulator

Coding Critique is the place to post source code for peer review by other members of DevNetwork. Any kind of code can be posted. Code posted does not have to be limited to PHP. All members are invited to contribute constructive criticism with the goal of improving the code. Posted code should include some background information about it and what areas you specifically would like help with.

Popular code excerpts may be moved to "Code Snippets" by the moderators.

Moderator: General Moderators

Post Reply
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

PHPCronEmulator

Post 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);
        }
    }
}
Last edited by s.dot on Thu Aug 30, 2007 2:39 am, edited 4 times in total.
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post 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".
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
VladSun
DevNet Master
Posts: 4313
Joined: Wed Jun 27, 2007 9:44 am
Location: Sofia, Bulgaria

Post by VladSun »

There are 10 types of people in this world, those who understand binary and those who don't
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post 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.
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post 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.
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
Post Reply