Current File : //opt/RZphp80/includes/System/Daemon/OS.php
<?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: OS.php 150 2008-09-05 22:06:05Z kevin $
 * @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: OS.php 150 2008-09-05 22:06:05Z kevin $
 * @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();

    /**
     * Path of init.d scripts
     *
     * @var string
     */
    protected $_autoRunDir;

    /**
     * Hold OS information
     *
     * @var array
     */
    protected $_osDetails = array();



    /**
     * Constructor
     * Only ran 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 static 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;
        }

        // Determine which one to use
        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 = self::_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           = self::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;
    }



    /**
     * Determines wether the system is compatible with this OS
     *
     * @return boolean
     */
    public function isInstalled()
    {
        $this->errors[] = "Not implemented for OS";
        return false;
    }

    /**
     * Returns array with all the specific details of the loaded OS
     *
     * @return array
     */
    public function getDetails()
    {
        return $this->_osDetails;
    }

    /**
     * Returns a template path to base the autuRun script on.
     * Uses $autoRunTemplatePath if possible.
     *
     * @param array $properties Additional properties
     *
     * @return unknown
     * @see autoRunTemplatePath
     */
    public function getAutoRunTemplatePath($properties = array())
    {
        $path = $this->_autoRunTemplatePath;

        if (!empty($properties['runTemplateLocation'])) {
            $path = $properties['runTemplateLocation'];
        }

        if (!$path) {
            $this->errors[] = "No autoRunTemplatePath found";
            return false;
        }

        // Replace variable: #datadir#
        // with actual package datadir
        // this enables predefined templates for e.g. redhat & bsd
        if (false !== strpos($path, '#datadir#')) {
            $dataDir = $this->getDataDir();
            if (false === $dataDir) {
                return false;
            }
            $path = str_replace('#datadir#', $dataDir, $path);
        }

        return $path;
    }

    /**
     * Returns the directory where data is stored (like init.d templates)
     * Could be PEAR's data directory but could also be a local one.
     *
     * @return string
     */
    public function getDataDir()
    {
        $tried_dirs = array();
        $dir        = false;

        if (class_exists('PEAR_Config', true)) {
            $config = PEAR_Config::singleton();
            if (PEAR::isError($config)) {
                $this->errors[] = $config->getMessage();
                return false;
            }

            $try_dir = realpath(
                $config->get('data_dir').
                '/System_Daemon/data'
            );
            if (!is_dir($try_dir)) {
                $tried_dirs[] = $try_dir;
            } else {
                $dir = $try_dir;
            }
        }

        if (!$dir) {
            $try_dir = realpath(dirname(__FILE__).'/../../data');
            if (!is_dir($try_dir)) {
                $tried_dirs[] = $try_dir;
            } else {
                $dir = $try_dir;
            }
        }

        if (!$dir) {
            $this->errors[] = 'No data dir found in either: '.
            implode(' or ', $tried_dirs);
            return false;
        }

        return $dir;
    }

    /**
     * Returns OS specific path to autoRun file
     *
     * @param string $appName Unix-proof name of daemon
     *
     * @return string
     */
    public function getAutoRunPath($appName)
    {
        if (empty($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 (!self::isWritable($dir)) {
            $this->errors[] = "Directory: '".$dir."' is not writable. ".
                "Maybe run as root (now: " . getmyuid() . ")?";
            return false;
        }

        return $path;
    }
    
    /**
     * A 'better' is_writable. Taken from PHP.NET comments:
     * http://nl.php.net/manual/en/function.is-writable.php#73596
     * Will work in despite of Windows ACLs bug
     * NOTE: use a trailing slash for folders!!!
     * see http://bugs.php.net/bug.php?id=27609
     * see http://bugs.php.net/bug.php?id=30931
     *
     * @param string $path Path to test
     * 
     * @return boolean
     */
    public static function isWritable($path)
    {
        if ($path{strlen($path)-1} === '/') {
            //// recursively return a temporary file path
            return self::isWritable($path.uniqid(mt_rand()).'.tmp');
        } else if (is_dir($path)) {
            return self::isWritable($path.'/'.uniqid(mt_rand()).'.tmp');
        }
        // check tmp file for read/write capabilities
        if (($rm = file_exists($path))) {
            $f = fopen($path, 'a');
        } else {
            $f = fopen($path, 'w');
        }
        if ($f === false) {
            print_r($path);
            return false;
        }
        @fclose($f);
        if (!$rm) {
            unlink($path);
        }
        return true;
    }

    /**
     * Returns a template to base the autuRun script on.
     * Uses $autoRunTemplatePath if possible.
     *
     * @param array $properties Contains the daemon properties
     *
     * @return unknown
     * @see autoRunTemplatePath
     */
    public function getAutoRunTemplate($properties)
    {
        if (($path = $this->getAutoRunTemplatePath($properties)) === false) {
            return false;
        }

        if (!file_exists($path)) {
            $this->errors[] = "autoRunTemplatePath: ".
            $path." does not exist";
            return false;
        }

        return file_get_contents($path);
    }

    /**
     * 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($properties)) === false) {
            return false;
        }
        if (!$this->_autoRunTemplateReplace
            || !is_array($this->_autoRunTemplateReplace)
            || !count($this->_autoRunTemplateReplace)
        ) {
            $this->errors[] = "No autoRunTemplateReplace found";
            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;
    }

    /**
     * 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;
    }



    /**
     * Sets daemon specific properties
     *
     * @param array $properties Contains the daemon properties
     *
     * @return array
     */
    protected 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;

    }

    /**
     * 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
     */
    protected static function _mostSpecific($classes)
    {
        $weights = array_map(
            array("System_Daemon_OS", "_getAncestorCount"),
            $classes
        );
        arsort($weights);
        $fattest = reset(array_keys($weights));
        return $fattest;
    }

    /**
     * Extracts last part of a classname. e.g. System_Daemon_OS_Ubuntu -> Ubuntu
     *
     * @param string $class Full classname
     *
     * @return string
     */
    protected function _getShortHand($class)
    {
        if (!is_string($class) || ! $class ) {
            return false;
        }
        $parts = explode("_", $class);
        return end($parts);
    }

    /**
     * Get the total parent count of a class
     *
     * @param string $class Full classname or instance
     *
     * @return integer
     */
    protected function _getAncestorCount($class)
    {
        return count(System_Daemon_OS::_getAncestors($class));
    }

    /**
     * Get an array of parent classes
     *
     * @param string $class Full classname or instance
     *
     * @return array
     */
    protected function _getAncestors($class)
    {
        $classes = array();
        while ($class = get_parent_class($class)) {
            $classes[] = $class;
        }
        return $classes;
    }
}