#!/usr/bin/php
<?php
/* vim: set expandtab tabstop=4 softtabstop=4 shiftwidth=4:
  Codificación: UTF-8
  +----------------------------------------------------------------------+
  | Issabel version 1.2-2                                                |
  | http://www.issabel.org                                               |
  +----------------------------------------------------------------------+
  | Copyright (c) 2006 Palosanto Solutions S. A.                         |
  +----------------------------------------------------------------------+
  | The contents of this file are subject to the General Public License  |
  | (GPL) Version 2 (the "License"); you may not use this file except in |
  | compliance with the License. You may obtain a copy of the License at |
  | http://www.opensource.org/licenses/gpl-license.php                   |
  |                                                                      |
  | Software distributed under the License is distributed on an "AS IS"  |
  | basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See  |
  | the License for the specific language governing rights and           |
  | limitations under the License.                                       |
  +----------------------------------------------------------------------+
  | The Initial Developer of the Original Code is PaloSanto Solutions    |
  +----------------------------------------------------------------------+
  $Id: dialerd,v 1.2 2008/09/08 18:29:36 alex Exp $ */
declare(ticks=8);
ini_set('memory_limit', -1);        // Permitir al programa usar toda la memoria que necesite
ini_set('display_errors', 0);       // Impedir que los errores de STDERR se copien en STDOUT
ini_set('error_reporting', E_ALL & ~E_STRICT);  // Mostrar advertencias además de errores

// Agregar directorio de script a la lista de rutas a buscar para require()
ini_set('include_path', dirname($argv[0]).":".ini_get('include_path'));

// Auto-cargado de las clases en base al nombre de archivo
function __autoload($sNombreClase) {
    $sNombreBase = $sNombreClase.'.class.php';
    foreach (explode(':', ini_get('include_path')) as $sDirInclude) {
        if (file_exists($sDirInclude.'/'.$sNombreBase)) {
            require_once($sNombreBase);
            return;
        }
    }
}

require_once('Console/Getopt.php');     // Parseo de opciones de línea de comando

$gsNombreSignal = NULL; // Número de la señal atrapada
$goAppLog = NULL;

$opt = Console_Getopt::getopt($argv, 'd');
if (PEAR::isError($opt)) {
    // Se ha especificado una operación no reconocida
    fwrite(STDERR, $opt->getMessage()."\n");
    $opt = NULL;
/*
} else if (count($opt[0]) == 0) {
    $opt = NULL;
*/
}
$iRes = main($opt,
    'Marcador predictivo en PHP', 
    'gvdialerd',
    'HubProcess');
exit($iRes);

function load_default_timezone()
{
    $sDefaultTimezone = @date_default_timezone_get();
    if ($sDefaultTimezone == 'UTC') {
        $sDefaultTimezone = 'America/New_York';
        $regs = NULL;
        if (is_link("/etc/localtime") && preg_match("|/usr/share/zoneinfo/(.+)|", readlink("/etc/localtime"), $regs)) {
            $sDefaultTimezone = $regs[1];
        } elseif (file_exists('/etc/sysconfig/clock')) {
            foreach (file('/etc/sysconfig/clock') as $s) {
                $regs = NULL;
                if (preg_match('/^ZONE\s*=\s*"(.+)"/', $s, $regs)) {
                    $sDefaultTimezone = $regs[1];
                }
            }
        }
    }
    date_default_timezone_set($sDefaultTimezone);
}

// Punto de entrada del programa demonio (plantilla)
function main($paramConfig, $sDescDaemon, $sNameDaemon /* , ... */) {
    
    global $argv;
    global $gsNameSignal;
    global $goAppLog;

    // Para silenciar avisos de fecha/hora
    if (function_exists('date_default_timezone_get')) {
        load_default_timezone();
    }

    // Esta es la tabla de procesos a monitorear
    $tableProcess = array();

    $iNumParams = func_num_args();
    for ($i = 3; $i < $iNumParams; $i++) {
        $sNameProcess = func_get_arg($i);
        $tableProcess[$sNameProcess] = NULL;
    }

    $infoConfig = array();
    $sNameConfig = $sNameDaemon . '.conf';
    $sNameMainLog = $sNameDaemon . '.log';
    $sNamePID = $sNameDaemon . '.pid';

    // El script se rehusa a ejecutarse como root en Unix/Linux.
    if (posix_geteuid() == 0)
        die(
                "\nSECURITY WARNING: Sorry, I STRONGLY OBJECT to run as root.\n\n" .
                "This program requires only network access and therefore does not require root privileges.\n\n");

    // Construir el nombre del archivo de configuración
    $sRutaTrabajo = dirname($argv[0]);
    $sArchivoConfig = "$sRutaTrabajo/$sNameConfig";
    $sArchivoPID = "$sRutaTrabajo/$sNamePID";

    // Intentar cargar el archivo de configuración
    $infoConfig = parse_ini_file($sArchivoConfig, TRUE);
    if (count($infoConfig) == 0) {
        if (function_exists('error_get_last'))
            $e = error_get_last();
        else
            $e = array('message' => 'Failed to open file, error_get_last() not available.');
        die("Could not load configuration file: $e[message]\n");
    }
    if (!isset($infoConfig['basedir']))
        $infoConfig['basedir'] = $sRutaTrabajo;

    // Esperar hasta que finalice la instancia previa
    finalizarInstanciaPrevia($sArchivoPID);

    // Si no se indica depuración del programa, se vuelve un demonio
    $bDepuracion = FALSE;
    if (is_array($paramConfig) && isset($paramConfig[0]) && is_array($paramConfig[0])) {
        foreach ($paramConfig[0] as $param) {
            if ($param[0] === 'd')
                $bDepuracion = TRUE;
        }
    }
    if (!$bDepuracion)
        daemon(TRUE, FALSE);
    //$PROGRAM_NAME = $sNombreDemonio.' - MASTER MONITOR';
    // Escribir el PID del proceso actual
    escribirPID($sArchivoPID);

    // Crear los logs de aplicación, de Harris, y de llamadas no escritas
    $bExitoAbrirLogs = FALSE;
    openlog($sNameDaemon, LOG_PID, LOG_USER);
    $oMainLog = new AppLogger();
    try {
        $oMainLog->open("$sRutaTrabajo/$sNameMainLog");
        $bExitoAbrirLogs = TRUE;
    } catch (Exception $e) {
        syslog(LOG_WARNING, "Unable to open applogs properly: " . $e->getMessage());
        $oMainLog = NULL;
    }
    closelog();

    if ($bExitoAbrirLogs && !is_null($oMainLog)) {
        $bContinuar = TRUE;
        $goAppLog = $oMainLog;

        // Conectar sistema de mensajes de error de PHP al log
        $old_errorHandler = set_error_handler('daemonErrorHandler');

        $sBannerLog = str_pad($sDescDaemon, 40, ' ', STR_PAD_BOTH);
        $sBanner = <<<BANNEREND
Main log opened correctly
----------------------------------------
$sBannerLog
      (PHP implementation, v1.0)
         Powered by Issabel

     Please report bugs/issues at:
  https://github.com/IssabelFoundation
----------------------------------------
BANNEREND;
        $oMainLog->output($sBanner);
        $oMainLog->output("Main log located at $sRutaTrabajo/$sNameMainLog");

        $oMainLog->prefijo("ProcessMonitor");
        $oMainLog->output("PID = " . posix_getpid() . ", monitor started normally");

        // Instalar manejadores de señal para demonio (SIGTERM, SIGQUIT, SIGINT, SIGHUP)
        pcntl_signal(SIGTERM, 'manejadorPrimarioSignal');
        pcntl_signal(SIGQUIT, 'manejadorPrimarioSignal');
        pcntl_signal(SIGINT, 'manejadorPrimarioSignal');
        pcntl_signal(SIGHUP, 'manejadorPrimarioSignal');

        // Iniciar procesos mientras se deba continuar
        while ($bContinuar) {
            // Si la tarea ha finalizado o no existe, se debe iniciar
            foreach (array_keys($tableProcess) as $sTask) {
                // Si está definido el PID del proceso, se verifica si se ejecuta.
                if (!is_null($tableProcess[$sTask])) {
                    $iStatus = NULL;
                    $iPidDevuelto = pcntl_waitpid($tableProcess[$sTask], $iStatus, WNOHANG);
                    if ($iPidDevuelto > 0) {
                        $oMainLog->output("WARNING: $sTask (PID=$iPidDevuelto) ended unexpectedly (status=$iStatus), scheduling restart...");
                        $iErrCode = pcntl_wifexited($iStatus) ? pcntl_wexitstatus($iStatus) : 255;
                        $iRcvSignal = pcntl_wifsignaled($iStatus) ? pcntl_wtermsig($iStatus) : 0;
                        if ($iRcvSignal != 0) {
                            $oMainLog->output("WARNING: $sTask terminated due to signal $iRcvSignal...");
                        }
                        if ($iErrCode != 0) {
                            $oMainLog->output("WARNING: $sTask returned error code $iErrCode...");
                        }
                        $tableProcess[$sTask] = NULL;
                    }
                }

                // Si no está definido el PID del proceso, se intenta iniciar
                if (is_null($tableProcess[$sTask])) {
                    $tableProcess[$sTask] = iniciarTarea($sNameDaemon, $sTask, $infoConfig, $oMainLog);
                }
            }

            // Revisar si existe señal que indique finalización del programa
            if (!is_null($gsNameSignal) && in_array($gsNameSignal, array(SIGTERM, SIGINT, SIGQUIT, SIGHUP))) {

                // Mandar la señal a todos los procesos controlados
                $oMainLog->output("PID = " . posix_getpid() . ", $sNameDaemon received signal #$gsNameSignal, " .
                        (($gsNameSignal == SIGHUP) ? 'switching logs' : 'terminating') . "...");
                foreach (array_keys($tableProcess) as $sTask) {

                    if (!is_null($tableProcess[$sTask])) {
                        $oMainLog->output("Forwarding signal #$gsNameSignal to $sTask...");
                        posix_kill($tableProcess[$sTask], $gsNameSignal);
                        //posix_kill(-1 * getpgrp($tablaProcesos[$sTarea]), $gsNombreSignal);
                        $oMainLog->output("Completed signal forwarding to $sTask");
                    }
                }

                if ($gsNameSignal != SIGHUP) {
                    // Esperar a que todos los procesos controlados terminen
                    foreach (array_keys($tableProcess) as $sTask) {
                        if (!is_null($tableProcess[$sTask])) {
                            $iStatus = NULL;
                            pcntl_waitpid($tableProcess[$sTask], $iStatus, 0);
                            $tableProcess[$sTask] = NULL;
                        }
                    }
                    $bContinuar = 0;
                } else {
                    $oMainLog->reopen();
                    $oMainLog->output("PID = " . posix_getpid() . ", $sNameDaemon received signal #$gsNameSignal, using new log.");
                    $gsNameSignal = NULL;
                }
            } else {
                // Esperar medio segundo entre verificaciones de actividad
                usleep(500000);
            }
        }

        // Restaurar manejador anterior de errores
        // set_error_handler($old_errorHandler);
    }

    // Cerrar los logs una vez indicada la finalización del programa
    if (!is_null($oMainLog)) {
        $oMainLog->output("PID = " . posix_getpid() . ", process terminated normally.");
        $oMainLog->close();
        $oMainLog = NULL;
    }

    // Borrar el PID del proceso actual
    borrarPID($sArchivoPID);
}

// Implementación de daemon() usando pcntl_fork()
function daemon($nochdir, $noclose) {
    $iPid = pcntl_fork();
    if ($iPid != -1) {
        if ($iPid != 0)
            exit(0);    // Terminar el proceso padre
        if (!$noclose) {
            fclose(STDIN);
            fclose(STDOUT);
        }
        if (posix_setsid() >= 0) {
            if (!$nochdir)
                chdir('/');
            return 0;
        } else {
            return -1;
        }
    } else {
        return -1;
    }
}

/* Si existe el archivo de PID indicado, se manda una señal SIGINT y se espera
   a que el otro programa finalice
 */
function finalizarInstanciaPrevia($sNombrePID) {
    
    if (file_exists($sNombrePID)) {
        // Abrir el archivo y leer el PID que tiene adentro
        $regs = NULL;
        $contenido = file($sNombrePID);
        if (!is_array($contenido))
            die("Unable to open PID file '$sNombrePID'\n");
        
        if (count($contenido) > 0 && preg_match('/^(\d+)/', $contenido[0], $regs)) {
            $iPid = $regs[1];
            if (posix_kill($iPid, 0)) {
                /* El siguiente método es específico de Linux y no funcionará
                  con otro OS. Se verifica si el proceso fue iniciado con PHP
                  y si contiene como primer parámetro la cadena */
                $bValidoPID = TRUE;
                if (!is_readable("/proc/$iPid/cmdline"))
                    $bValidoPID = FALSE;
                if ($bValidoPID) {
                    $argv_proc = explode("\0", file_get_contents("/proc/$iPid/cmdline"));
                    if (strpos($argv_proc[0], '/php') === FALSE)
                        $bValidoPID = FALSE;
                }
                if (!$bValidoPID) {
                    print "PID $iPid does not look like a PHP daemon. Assuming stale PID file.\n";
                    unlink($sNombrePID);
                } else {
                    print "Signaling termination to PID $iPid...\n";
                    if (!posix_kill($iPid, SIGINT)) {
                        print "Unable to signal PID $iPid (running as different user?). Assuming stale PID file.\n";
                        unlink($sNombrePID);
                    }
                }
                if (file_exists($sNombrePID)) {
                    print "Waiting for PID $iPid to terminate...\n";
                    while (file_exists($sNombrePID))
                        usleep(1000000);
                    print "PID $iPid seems to have terminated, resuming startup...\n";
                }
            }
        }
    }
}

// Escribir el archivo de PID
function escribirPID($sNombrePID) {
    $hArchivoPID = fopen($sNombrePID, 'w');
    if (!$hArchivoPID)
        die("Unable to create PID file '$sNombrePID'\n");
    fputs($hArchivoPID, sprintf('%d', posix_getpid()));
    fclose($hArchivoPID);
}

// Borrar el archivo de PID
function borrarPID($sNombrePID) {
    while (file_exists($sNombrePID)) {
        unlink($sNombrePID);
    }
}

// Manejador de señales para el proceso demonio principal
function manejadorPrimarioSignal($signo) {
    global $gsNameSignal;

    $gsNameSignal = $signo;
}

/* Iniciar una tarea específica en un proceso separado. Para el proceso padre,
   devuelve el PID del proceso hijo.
 */
function iniciarTarea($sNombreDemonio, $sNombreTarea, $infoConfig, &$oMainLog) {
    
    global $gsNameSignal;

    set_error_handler('daemonErrorHandler');

    // Verificar que el nombre de la clase que implementa el proceso es válido
    if (!class_exists($sNombreTarea)) {
        $oMainLog->output("FATAL: (internal) Invalid process classname '$sNombreTarea'");
        die("(internal) Invalid process classname '$sNombreTarea'\n");
    }

    // Iniciar tarea en proceso separado
    $iPidProceso = pcntl_fork();
    if ($iPidProceso != -1) {
        if ($iPidProceso == 0) {
            //$PROGRAM_NAME = $sNombreDemonio.' - '.$sNombreTarea;
            $oMainLog->prefijo($sNombreTarea);
            $oMainLog->output("starting up process...");

            // Instalar los manejadores de señal para el proceso hijo
            pcntl_signal(SIGTERM, 'manejadorPrimarioSignal');
            pcntl_signal(SIGQUIT, 'manejadorPrimarioSignal');
            pcntl_signal(SIGINT, 'manejadorPrimarioSignal');
            pcntl_signal(SIGHUP, 'manejadorPrimarioSignal');

            // Elegir la tarea que debe de ejecutarse
            $oProceso = NULL;
            try {
                $oProceso = new $sNombreTarea();
                if (!($oProceso instanceof AbstractProcess))
                    throw new Exception('Not a subclass of AbstractProcess!');
            } catch (Exception $ex) {
                $oMainLog->output("ERR: while creating $sNombreTarea - uncaught exception: " . $ex->getMessage());
                die("ERR: while instantiating $sNombreTarea - " . $ex->getMessage() . "\n");
            }

            // Realizar inicialización adicional de la tarea
            try {
                $bContinuar = $oProceso->inicioPostDemonio($infoConfig, $oMainLog);
                if ($bContinuar)
                    $oMainLog->output("PID = " . posix_getpid() . ", process started normally");
            } catch (Exception $ex) {
                $bContinuar = FALSE;
                $oMainLog->output("ERR: while initializing $sNombreTarea - uncaught exception: " . $ex->getMessage());
            }

            // Continuar la tarea hasta que se finalice
            while ($bContinuar) {
                // Ejecutar el procedimiento de trabajo del demonio
                if (is_null($gsNameSignal)) {
                    try {
                        $bContinuar = $oProceso->procedimientoDemonio();
                    } catch (Exception $ex) {
                        $bContinuar = FALSE;
                        $oMainLog->output("ERR: while running $sNombreTarea - uncaught exception: " . $ex->getMessage());
                    }
                }

                // Revisar si existe señal que indique finalización del programa
                if (!is_null($gsNameSignal)) {
                    if (in_array($gsNameSignal, array(SIGTERM, SIGINT, SIGQUIT))) {
                        $oMainLog->output("PID = " . posix_getpid() . ", process received signal $gsNameSignal, terminating...");
                        $bContinuar = FALSE;
                    } elseif ($gsNameSignal == SIGHUP) {
                        $oMainLog->output("PID = " . posix_getpid() . ", process received signal $gsNameSignal, switching logs...");
                        if (method_exists($oProceso, 'propagarSIGHUP')) {
                            $oMainLog->output("PID = " . posix_getpid() . ", process has handler for SIGHUP, calling...");
                            $oProceso->propagarSIGHUP();
                        }
                        $oMainLog->reopen();
                        $oMainLog->output("PID = " . posix_getpid() . ", process received signal $gsNameSignal, using new log.");
                        $gsNameSignal = NULL;
                    }
                }
            }

            // Indicar al módulo de trabajo por qué se está finalizando
            try {
                $oProceso->limpiezaDemonio($gsNameSignal);
            } catch (Exception $ex) {
                $oMainLog->output("ERR: while cleaning up $sNombreTarea - uncaught exception: " . $ex->getMessage());
            }
            $oMainLog->output("PID = " . posix_getpid() . ", process terminated normally.");
            $oMainLog->close();

            exit(0);   // Finalizar el proceso hijo
        }
    } else {
        // Avisar que no se puede iniciar la tarea requerida
        $oMainLog->output("Unable to fork $sNombreTarea - $!");
    }
    return $iPidProceso;
}

// Procedimiento que usa el log abierto para reportar el error que haya ocurrido
function daemonErrorHandler($errno, $errmsg, $filename, $linenum, $vars) {
    
    global $goAppLog;
    $errortype = array(
        E_ERROR => 'Error',
        E_WARNING => 'Warning',
        E_PARSE => 'Parsing Error',
        E_NOTICE => 'Notice',
        E_CORE_ERROR => 'Core Error',
        E_CORE_WARNING => 'Core Warning',
        E_COMPILE_ERROR => 'Compile Error',
        E_COMPILE_WARNING => 'Compile Warning',
        E_USER_ERROR => 'User Error',
        E_USER_WARNING => 'User Warning',
        E_USER_NOTICE => 'User Notice',
        E_STRICT => 'Runtime Notice',
//        E_RECOVERABLE_ERROR  => 'Catchable Fatal Error'
    );
    if (!is_null($goAppLog) && $errno != E_STRICT) {
        $goAppLog->output("$errortype[$errno]: $filename line $linenum - $errmsg");
    }
}
