<?php
/* vim: set noai expandtab tabstop=4 softtabstop=4 shiftwidth=4: */
/**
 * System_Daemon turns PHP-CLI scripts into daemons.
 * 
 * PHP version 5
 *
 * @category  System
 * @package   System_Daemon
 * @author    Kevin van Zonneveld <kevin@vanzonneveld.net>
 * @copyright 2008 Kevin van Zonneveld (http://kevin.vanzonneveld.net)
 * @license   http://www.opensource.org/licenses/bsd-license.php New BSD Licence
 * @version   SVN: Release: $Id$
 * @link      http://trac.plutonia.nl/projects/system_daemon
 */

/**
 * Operating System focussed functionality.
 *
 * @category  System
 * @package   System_Daemon
 * @author    Kevin van Zonneveld <kevin@vanzonneveld.net>
 * @copyright 2008 Kevin van Zonneveld (http://kevin.vanzonneveld.net)
 * @license   http://www.opensource.org/licenses/bsd-license.php New BSD Licence
 * @version   SVN: Release: $Id$
 * @link      http://trac.plutonia.nl/projects/system_daemon
 * 
 */
class System_Daemon_OS
{
    /**
     * Holds errors
     *
     * @var array
     */
    public $errors = array();
    
    
        
    /**
     * Template path
     *
     * @var string
     */
    protected $autoRunTemplatePath = "";    
        
    /**
     * Replace the following keys with values to convert a template into
     * a read autorun script
     *
     * @var array
     */
    protected $autoRunTemplateReplace = array();
    
    
    
    /**
     * Hold OS information
     *
     * @var array
     */
    private $_osDetails = array();
    
    
    
    /**
     * Constructor
     * Only run by instantiated OS Drivers
     */
    public function __construct() 
    {
        // Up to date filesystem information
        clearstatcache();

        // Get ancestors
        $ancs = System_Daemon_OS::_getAncestors($this);
        foreach ($ancs as $i=>$anc) {
            $ancs[$i] = System_Daemon_OS::_getShortHand($anc);
        }        
        
        // Set OS Details
        $this->_osDetails["shorthand"] = $this->_getShortHand(get_class($this));
        $this->_osDetails["ancestors"] = $ancs;
    }

    /**
     * Loads all the drivers and returns the one for the most specifc OS
     * 
     * @param mixed   $force_os boolean or string when you want to enforce an OS 
     * for testing purposes. CAN BE VERY DANGEROUS IF WRONG OS IS SPECIFIED! 
     * Will otherwise autodetect OS.
     * @param boolean $retried  used internally to find out wether we are retrying 
     *
     * @return object
     */
    public function &factory($force_os = false, $retried = false)
    {
        $drivers      = array();
        $driversValid = array();
        $class_prefix = "System_Daemon_OS_";
        
        // Load all drivers
        $driver_dir = realpath(dirname(__FILE__)."/OS");
        foreach (glob($driver_dir."/*.php") as $driver_path) {
            // Set names
            $driver = basename($driver_path, ".php");
            $class  = $class_prefix.$driver;
            
            // Only do this for real drivers
            if ($driver == "Exception" || !is_file($driver_path)) {
                continue;
            }
            
            // Let SPL include & load the driver or Report errors
            if (!class_exists($class, true)) {
                $this->errors[] = "Class ".$class." does not exist";
                return false;
            }

            // Save in drivers array
            $drivers[$class] = new $class;            
        }
        
        if ($force_os !== false) {
            // Let's use the Forced OS. This could be dangerous
            $use_name = $class_prefix.$force_os;
        } else {
            // What OSes are valid for this system?
            // e.g. Debian makes Linux valid as well
            foreach ($drivers as $class=>$obj) {
                // Save in Installed container
                if (call_user_func(array($obj, "isInstalled"))) {
                    $driversValid[$class] = $obj;         
                }
            }
            
            // What's the most specific OS?
            // e.g. Ubuntu > Debian > Linux    
            $use_name = System_Daemon_OS::_mostSpecific($driversValid);
        }
        
        // If forced driver wasn't found, retry to autodetect it
        if (!isset($drivers[$use_name])) {
            // Make sure we don't build a loop
            if (!$retried) {
                $obj = System_Daemon_OS::factory(false, true);
                $obj->errors[] = "Unable to use driver: ".$force_os." falling ".
                    "back to autodetection.";
            } else {
                $obj = false;
            }
        } else {
            $obj = $drivers[$use_name]; 
        }
       
        return $obj;
    }//end &factory()

    
    
    /**
     * Determines wether the system is compatible with this OS
     *
     * @return boolean
     */    
    public function isInstalled() 
    {
        $this->errors[] = "Not implemented for OS";
        return false;
    }//end isInstalled
    
    /**
     * Returns array with all the specific details of the loaded OS
     *
     * @return array
     */
    public function getDetails()
    {
        return $this->_osDetails;
    }//end getDetails

    /**
     * Returns a template path to base the autuRun script on.
     * Uses $autoRunTemplatePath if possible. 
     *
     * @return unknown
     * @see autoRunTemplatePath
     */
    public function getAutoRunTemplatePath() 
    {
        if (!$this->autoRunTemplatePath) {
            $this->errors[] = "No autoRunTemplatePath found";
            return false;
        }
        
        return $this->autoRunTemplatePath; 
    }//end getAutoRunTemplatePath        
    
    /**
     * Returns OS specific path to autoRun file
     * 
     * @param string $appName Unix-proof name of daemon
     *
     * @return string
     */
    public function getAutoRunPath($appName) 
    {
        if (!$this->autoRunDir) {
            $this->errors[] = "autoRunDir is not set";
            return false;
        }
        
        $path = $this->autoRunDir."/".$appName;
        
        // Path exists
        if (!is_dir($dir = dirname($path))) {
            $this->errors[] = "Directory: '".$dir."' does not exist. ".
                "How can this be a correct path?";
            return false;
        }
        
        // Is writable?
        if (!is_writable($dir)) {
            $this->errors[] = "Directory: '".$dir."' is not writable. ".
                "Maybe run as root?";
            return false;
        }
        
        return $path;
    }//end getAutoRunPath    
    
    /**
     * Returns a template to base the autuRun script on.
     * Uses $autoRunTemplatePath if possible. 
     *
     * @return unknown
     * @see autoRunTemplatePath
     */
    public function getAutoRunTemplate() 
    {
        if (($path = $this->getAutoRunTemplatePath()) === false) {
            return false;
        }
        
        if (!file_exists($path)) {
            $this->errors[] = "autoRunTemplatePath: ".
                $path." does not exist";
            return false;
        }
        
        return file_get_contents($this->autoRunTemplatePath);
    }//end getAutoRunTemplate    
         
    /**
     * Uses properties to enrich the autuRun Template
     *
     * @param array $properties Contains the daemon properties
     * 
     * @return mixed string or boolean on failure
     */
    public function getAutoRunScript($properties)    
    {
        
        // All data in place? 
        if (($template = $this->getAutoRunTemplate()) === false) {
            return false;
        }
        if (!$this->autoRunTemplateReplace 
            || !is_array($this->autoRunTemplateReplace)
            || !count($this->autoRunTemplateReplace)) {
            return false;
        }
        
        // Replace System specific keywords with Universal placeholder keywords
        $script = str_replace(array_keys($this->autoRunTemplateReplace), 
            array_values($this->autoRunTemplateReplace), 
            $template);
        
        // Replace Universal placeholder keywords with Daemon specific properties
        if (!preg_match_all('/(\{PROPERTIES([^\}]+)\})/is', $script, $r)) {
            $this->errors[] = "No PROPERTIES found in autoRun template";
            return false;
        }
        
        $placeholders = $r[1];
        array_unique($placeholders);
        foreach ($placeholders as $placeholder) {
            // Get var
            $var = str_replace(array("{PROPERTIES.", "}"), "", $placeholder);
            
            // Replace placeholder with actual daemon property
            $script = str_replace($placeholder, $properties[$var], $script);
        }
        
        return $script;        
    }//end getAutoRunScript()   
    
    /**
     * Writes an: 'init.d' script on the filesystem
     * combining
     * 
     * @param array   $properties Contains the daemon properties
     * @param boolean $overwrite  Wether to overwrite when the file exists 
     * 
     * @return mixed string or boolean on failure
     * @see getAutoRunScript()
     * @see getAutoRunPath()
     */
    public function writeAutoRun($properties, $overwrite = false)
    {
        // Check properties
        if ($this->_testAutoRunProperties($properties) === false) {
            // Explaining errors should have been generated by 
            // previous function already
            return false;
        }
        
        // Get script body
        if (($body = $this->getAutoRunScript($properties)) === false) {
            // Explaining errors should have been generated by 
            // previous function already
            return false;
        }
        
        // Get script path
        if (($path = $this->getAutoRunPath($properties["appName"])) === false) {
            // Explaining errors should have been generated by 
            // previous function already            
            return false;
        }
        
        // Overwrite?
        if (file_exists($path) && !$overwrite) {
            return true;
        }
        
        // Write
        if (!file_put_contents($path, $body)) {
            $this->errors[] =  "startup file: '".
                $path."' cannot be ".
                "written to. Check the permissions";
            return false;
        }

        // Chmod
        if (!chmod($path, 0777)) {
            $this->errors[] =  "startup file: '".
                $path."' cannot be ".
                "chmodded. Check the permissions";
            return false;
        } 
        
        
        return $path;
    }//end writeAutoRun() 
            
    
        
    /**
     * Sets daemon specific properties
     *  
     * @param array $properties Contains the daemon properties
     * 
     * @return array
     */       
    private function _testAutoRunProperties($properties = false) 
    {
        $required_props = array("appName", "appExecutable", 
            "appDescription", "appDir", "authorName", "authorEmail");
        
        // Valid array?
        if (!is_array($properties) || !count($properties)) {
            $this->errors[] = "No properties to ".
                "forge init.d script";
            return false; 
        }
                
        // Check if all required properties are available
        $success = true;
        foreach ($required_props as $required_prop) {
            if (!isset($properties[$required_prop])) {
                $this->errors[] = "Cannot forge an ".
                    "init.d script without a valid ".
                    "daemon property: ".$required_prop;
                $success        = false;
                continue;
            }            
        }
        
        // Path to daemon
        $daemon_filepath = $properties["appDir"]."/".$properties["appExecutable"];
        
        // Path to daemon exists?
        if (!file_exists($daemon_filepath)) {
            $this->errors[] = "unable to forge startup script for non existing ".
                "daemon_filepath: ".$daemon_filepath.", try setting a valid ".
                "appDir or appExecutable";
            $success        = false;
        }
        
        // Path to daemon is executable? 
        if (!is_executable($daemon_filepath)) {
            $this->errors[] = "unable to forge startup script. ".
                "daemon_filepath: ".$daemon_filepath.", needs to be executable ".
                "first";
            $success        = false;
        }
        
        return $success;
        
    } //end _testAutoRunProperties    
        
    /**
     * Determines how specific an operating system is.
     * e.g. Ubuntu is more specific than Debian is more 
     * specific than Linux is more specfic than Common.
     * Determined based on class hierarchy.
     *
     * @param array $classes Array with keys with classnames
     * 
     * @return string
     */
    private function _mostSpecific($classes) 
    {
        $weights = array_map(array("System_Daemon_OS", "_getAncestorCount"), 
            $classes);
        arsort($weights);        
        return reset(array_keys($weights));
    }//end _mostSpecific
    
    /**
     * Extracts last part of a classname. e.g. System_Daemon_OS_Ubuntu -> Ubuntu
     *
     * @param string $class Full classname
     * 
     * @return string
     */
    private function _getShortHand($class) 
    {
        if (!is_string($class) || ! $class ) {
            return false;
        }
        $parts = explode("_", $class);
        return end($parts);
    } //end _getShortHand
    
    /**
     * Get the total parent count of a class
     *
     * @param string $class Full classname or instance
     * 
     * @return integer
     */
    private function _getAncestorCount($class) 
    {
        return count(System_Daemon_OS::_getAncestors($class));        
    }//end _getAncestorCount
    
    /**
     * Get an array of parent classes
     *
     * @param string $class Full classname or instance
     * 
     * @return array
     */
    private function _getAncestors($class)
    {
        $classes = array();
        while ($class = get_parent_class($class)) { 
            $classes[] = $class; 
        }
        return $classes;
    }//end _getAncestors
    
}//end class
?>