<?php

namespace Api\PhpServices\Daemon;

use Api\PhpUtils\Cache\ApcuUtil;
use Api\PhpUtils\Log\DaemoLog;
use Api\PhpUtils\Message\Email;
use Yaf\Application;

class Daemon
{
    /**
     * 任务名
     * @var string
     */
    protected $task_name;
    protected $task_class;

    const MAX_WORKER_PROCESS_NUMBER = 16;
    const MAX_WORKER_EXECUTE_TIMES  = 1000;
    protected $process_list         = false;
    protected $max_process_num      = 0;
    protected $current_process_num  = 0;
    protected $matser_process_id    = false;
    protected $output               = false;
    protected $terminate            = false;
    protected $consumer_name;
    protected $log_signature        = "daemon";
    protected $env                  = "dev";
    protected $exception_time_cycle = 120;      // 脚本异常记录时间周期
    protected $exception_save_time  = 10;        // 异常时间周期内，记录前 n 条到错误日志
    protected $exception_alert_time = 100;      // 异常时间周期内，超过 n 次异常则邮件报警



    public function __construct(string $task_name, $task_class, int $worker_num = 4)
    {
        $this->checkEnv();

        if( $worker_num > self::MAX_WORKER_PROCESS_NUMBER )
        {
            die("worker进程最大限制为".self::MAX_WORKER_PROCESS_NUMBER."\n");
        }

        if( !class_exists($task_class) ){
            die("任务不存在"."\n");
        }

        $this->task_name        = $task_name;
        $this->task_class       = $task_class;
        $this->max_process_num  = $worker_num;
        $this->env              = Application::app()->environ() ?? "dev";

        $this->set_base_config();

        if(function_exists('gc_enable')) gc_enable();
    }

    public static function run(){
        throw new \Exception("bbb");
    }


    /**
     * 启动守护进程,并在master进行中监听子进程状态，当子进程退出后重新启动新的子进程( 保持worker进程数为max_process_num )
     * 若要结束守护进程可向主进程发送kill -SIGUSR1的信号,子进程完成当前工作后会逐个退出
     */
    public function start(){
        DaemoLog::info($this->log_signature,'process start now');

        $this->daemon();

        for($i=0; $i<$this->max_process_num; $i++) {
            $this->worker();
        }

        while(1)
        {
            //注册信号处理,master进程中捕获SIGUSR1后通知worker进程退出
            if( pcntl_signal(SIGUSR1, array(__CLASS__, "signal_handler"),false) )
            {
                DaemoLog::info($this->log_signature,'master pcntl SIGUSR1 success');
            }else
            {
                DaemoLog::error($this->log_signature,'master pcntl SIGUSR1 failed');
            }
            pcntl_signal_dispatch();

            $pid = pcntl_wait($status, WUNTRACED); //阻塞取得子进程结束状态

            if( ($key = array_search($pid, $this->process_list)) !==false )
            {
                $this->current_process_num--;
                unset($this->process_list[$key]);
                $this->process_list = array_values($this->process_list);

                DaemoLog::info($this->log_signature,"worker process: {".$pid."} exited with {$status}");
                if( $this->terminate == false )
                {
                    if( $this->current_process_num < $this->max_process_num )
                    {
                        $this->worker();
                    }
                }else
                {
                    if( $this->current_process_num == 0 )
                    {
                        break;
                    }
                }

            }
        }
        DaemoLog::info($this->log_signature,"shut down success");
        exit(0);
    }

    protected function daemon()
    {
        set_time_limit(0);
        $pid = pcntl_fork();
        if($pid == -1)
        {
            die('Could not fork 1');
        }
        if ($pid != 0){ //父进程
            exit(0);
        }
        posix_setsid(); //设置新会话组长，脱离终端
        $pid = pcntl_fork();
        if($pid == -1)
        {
            die('Could not fork 2');
        }
        if ($pid != 0)  //第一子进程
        {
            DaemoLog::info($this->log_signature,'master process pid:'.$pid);
            exit(0);
        }
        chdir(APPLICATION_PATH);

        umask(0);

        fclose(STDIN);
        fclose(STDOUT);
        fclose(STDERR);

        $this->matser_process_id = posix_getgid();

        // mac 系统不支持此函数，调试时关闭
        cli_set_process_title($this->task_name.' '.$this->env.' master process');
    }

    /**
     * 启动worker进程
     * @Author   wangdanfeng
     * @return   void
     */
    protected function worker()
    {
        $pid = pcntl_fork();
        if( $pid == -1 ) {
            DaemoLog::error($this->log_signature,'worker Process fork failed');

        }elseif ($pid == 0) {
            //子进程
            cli_set_process_title($this->task_name . ' ' . $this->env . ' worker process');
            DaemoLog::info($this->log_signature,"worker Process {".posix_getpid()."} was created".' master process id:'.posix_getppid());

            //注册子进程信号
            if( pcntl_signal(SIGUSR2, array(__CLASS__, "signal_handler"),false) )
            {
                DaemoLog::info( $this->log_signature ,"worker Process {".posix_getpid()."} pcntl SIGUSR2 success".' master process id:'.posix_getppid() );
            }else
            {
                DaemoLog::error( $this->log_signature ,"worker Process {".posix_getpid()."} pcntl SIGUSR2 failed".' master process id:'.posix_getppid() );
            }

            // 执行指定次数后自动退出
            for ($i=0; $i <self::MAX_WORKER_EXECUTE_TIMES ; $i++)
            {
                pcntl_signal_dispatch();

                try {
                    $task = new $this->task_class();
                    $task->run();
                }catch (\Exception $e){
                    // 默认2分钟内最多记录10条异常日志，超过100次则报警
                    $exception_num = ApcuUtil::apcu_inc($this->log_signature, posix_getppid(),1,$success, $this->exception_time_cycle);
                    if($success && $exception_num){
                        if($exception_num < $this->exception_save_time+1){
                            DaemoLog::error( $this->log_signature ,"worker Process {".posix_getpid()."} throw exception! msg: ".$e->getMessage().' master process id:'.posix_getppid() );
                        }elseif ($exception_num == $this->exception_alert_time){
                            $subject= "Daemon 任务异常";
                            $mail_body = '任务在'.$this->exception_time_cycle.'秒内异常次数达到'.$this->exception_alert_time.'次，请及时处理。'."\r\n" .
                                'time: ' . date('Y-m-d H:i:s') . "\r\n" .
                                'task_name: ' . $this->task_name . "\r\n" .
                                'task_class: ' . $this->task_class . "\r\n" .
                                'host name: ' . gethostname() . "\r\n";

//                            $to = 'api@yidian-inc.com';
                            $to = 'zhangyu1@yidian-inc.com';
                            Email::sendMail('noreply@yidian-inc.com', $to, $subject, $mail_body);
                        }
                    }
                }

                if($this->terminate==true)
                {
                    exit(0);
                }
            }
            exit(0);
        }else
        {
            $this->process_list[] = $pid;
            $this->current_process_num++;
            return;
        }
    }
    protected function quit()
    {
        DaemoLog::info( $this->log_signature ,"process shut down" );
        $this->terminate = true;
        foreach ($this->process_list as $pid)
        {
            if( posix_kill($pid, SIGUSR2) )
            {
                DaemoLog::info( $this->log_signature ,'master process send SIGUSR2 to '.$pid.' success' );
            }else
            {
                DaemoLog::error( $this->log_signature ,'master process send SIGUSR2 to '.$pid.' failed' );
            }
        }
    }
    /**
     * 目前仅支持退出所有进程,后续待完善
     * @Author   wangdanfeng
     * @param    [type]                   $signal [description]
     * @return   [type]                           [description]
     */
    protected function signal_handler($signal)
    {
        switch ($signal)
        {
            case SIGUSR1:                       //父进程监听信号
                $this->quit();
                break;
            case SIGUSR2:                       //子进程监听信号
                $this->terminate = true;
                break;
            default:
                # code...
                break;
        }
    }


    /**
     * 运行环境检查
     * @Author   wangdanfeng
     * @return   void
     */
    protected function checkEnv()
    {
        php_sapi_name() == "cli" or die("只允许在cli下面运行"."\n");
        PHP_VERSION_ID  >= 50330 or die("php版本需大于5.3.3"."\n");
        function_exists('pcntl_signal') or die("请安装pcntl扩展"."\n");
        function_exists('posix_getpid') or die("请安装posix扩展"."\n");
    }


    /**
     * 基本配置
     */
    protected function set_base_config(){
        ini_set("display_errors", 1);
        error_reporting('E_ALL');
        ini_set('date.timezone','Asia/Shanghai');
        ini_set("memory_limit", '2048M');
        error_reporting(E_ALL);

        //创建任务日志文件
        $log_dir  = APPLICATION_PATH.'/logs/daemon/'.$this->task_name;
        $log_path = $log_dir.'/'.$this->task_name.'.log';
        ini_set('error_log',$log_path );
        if( !file_exists($log_path) )
        {
            mkdir($log_dir, 0777, true);
            if( !touch( $log_path ) )
            {
                die($log_path."文件无法创建"."\n");
            }
        }

        $this->log_signature = "daemon:".$this->task_name;
    }
}