<?php

namespace xmvc\daemon\lib;

/**
 * 守护进程类基类
 * - 一个子类只能启动一个服务, 但可设置多个进程
 *
 * -d 守护进程
 * -f 强制关闭
 * -log 启用 log
 * Y 开头为日志文件日期部份的格式 例 Y-m-d H 为每小时一个日志文件, 默认 Y-m-d
 * M数字 以M开头的后面的数字(中间无空格) 为最大保留多少天的日志文件, 例 M30 保留30天, 默认 M7 保留 7 天
 * P数字 以P开头的后面的数字(中间无空格) 开多少个进程来处理业务逻辑, 默认为1个进程, 例 P3 为 开三个进程
 *
 * demoWorker.service
 * 安装服务：将服务文件放置在 /etc/systemd/system/ 目录下，
 * 然后运行 sudo systemctl daemon-reload 来重新加载 systemd 配置，
 * 并且使用 sudo systemctl enable demoWorker 和 sudo systemctl start demoWorker 启动和启用该服务
 * start, stop, restart
 *
 * [Unit]
 * Description=demoWorker
 * After=network.target
 * [Service]
 * Type=forking
 * ExecStart=/usr/bin/php /home/wwwroot/project/yiparts/worker/demoWorker.php start -d -log
 * ExecReload=/usr/bin/php /home/wwwroot/project/yiparts/worker/demoWorker.php restart -d -log
 * ExecStop=/usr/bin/php /home/wwwroot/project/yiparts/worker/demoWorker.php stop
 * PrivateTmp=true
 * RemainAfterExit=yes
 *
 * [Install]
 * WantedBy=multi-user.target
 *
 * 为了能支持更大的并发连接数，必须安装event扩展，并且优化Linux内核
 * 优化Linux内核 https://www.workerman.net/doc/workerman/appendices/kernel-optimization.html
 */
class BaseDaemon
{
    protected static $logHandle = null; // 日志文件句柄
    protected static $hasLog = false; // 是否记录日志 来自 -log 参数
    protected static $logDir; // 日志文件目录 DIR_LOG/daemon
    protected static $logDateFormat = 'Y-m-d'; // 日志文件名日期部份格式, 来自 Yxxx 参数
    protected static $maxProcessQty = 64; // 最大允许的进程数
    protected static $processQty = 1; // 需要启动的进程数 来自 Px 参数
    protected static $logMaxFile = 7; // 最大日志文件数量, 来自 Mx 参数, 不影响 error 日志
    protected static $pidFile; // 记录守护进程的进程 ID 的文件名
    protected static $stopRequested = false; // 停止服务请求, 默认优雅关闭
    protected static $isForce = false; // 是否是强制关闭
    protected static $forceFilename = ''; // 强制关闭时的通信标记文件名
    protected static $childrenProcess = []; // 子进程列表

    /**
     * 析构函数
     */
    public function __destruct()
    {
        static::log("PID:" . getmypid() . ' __destruct');
        $this->cleanup();
    }

    /**
     * 清理资源
     */
    protected function cleanup()
    {
        if (static::$logHandle) {
            fclose(static::$logHandle);
        }
    }

    /**
     * 启动
     */
    public function start()
    {
        if (!IS_CLI) {
            echo "仅允许在 CLI 模式下运行";
            exit();
        }
        $args = $_SERVER['argv'] ?? [];
        $isDaemon = false; // 是否是守护进程启动
        $command = '';
        foreach ($args as $i => $v) {
            $v = trim($v);
            $fchar = substr($v, 0, 1);
            if (in_array($v, ['start', 'stop', 'restart'])) {
                $command = $v;
            } elseif ($v === '-d') {
                $isDaemon = true;
            } elseif ($v === '-f' && $command != 'start') {
                static::$isForce = true;
            } elseif ($v === '-log') {
                static::$hasLog = true;
            } elseif ($fchar === 'Y') {
                static::$logDateFormat = $v; // 以 Y 开头的为文件日期格式, 默认 Y-m-d
            } elseif ($fchar === 'M') {
                static::$logMaxFile = substr($v, 1) * 1; // 要保留多少天日志, 默认 M7, M0 为不限
            } elseif ($fchar === 'P') {
                static::$processQty = substr($v, 1) * 1; //  P3 开三个进程来处理任务逻辑, 默认不多开进程
                if (static::$processQty > self::$maxProcessQty) {
                    echo "仅允许最大 " . self::$maxProcessQty . " 个进程";
                    exit();
                }
            }
        }
        static::$logDir = DIR_LOG . '/daemon';
        if (!is_dir(static::$logDir)) {
            mkdir(static::$logDir, 0777, true);
            chmod(static::$logDir, 0777);
        }
        static::$pidFile = static::getLogFilenamePrefix() . '.pid';
        static::$forceFilename = static::getLogFilenamePrefix() . '.force';
        switch ($command) {
            case 'start':
                if (file_exists(static::$pidFile)) {
                    echo ("PID file is exists, " . static::$pidFile . "\n");
                } else {
                    static::log("启动守护进程...");
                    if (file_exists(static::$forceFilename)) {
                        unlink(static::$forceFilename);
                    }
                    $isDaemon ? static::startDaemon() : static::run();
                }
                break;
            case 'stop':
                static::log("停止守护进程...");
                $this->stopDaemon();
                break;
            case 'restart':
                static::log("重启守护进程...");
                $this->stopDaemon();
                $isDaemon ? static::startDaemon() : static::run();
                break;
            default:
                echo "Unknown command: $command\n";
                echo "Usage: {start|stop|restart}\n";
                echo "Usage: {start -d} use Daemon\n";
                echo "Usage: {restart -d -f} use Daemon and Force\n";
                echo "Usage: {stop -f} use Force\n";
                break;
        }
        exit();
    }
    /**
     * 启动守护进程
     */
    protected function startDaemon()
    {
        $pid = pcntl_fork();

        if ($pid == -1) {
            die('创建子进程失败');
        } elseif ($pid) {
            echo get_called_class() . " is runing.\n";
            static::log("入口进程 PID: " . getmypid() . " 准备退出");
            // 父进程退出
            exit;
        } else {
            // 子进程成为守护进程
            if (posix_setsid() == -1) {
                static::error('子进程无法分离成为守护进程, 创建服务失败', __FILE__, __LINE__);
                exit();
            }

            // 关闭标准输入输出
            fclose(STDIN);
            fclose(STDOUT);
            fclose(STDERR);

            // 重定向标准输出和标准错误到 /dev/null
            $STDOUT = fopen('/dev/null', 'wb');
            $STDERR = fopen('/dev/null', 'wb');

            // 写入PID文件
            try {
                file_put_contents(static::$pidFile, getmypid());
            } catch (\Throwable $th) {
                static::error('写入PID文件出错, ' . $th->getMessage(), __FILE__, __LINE__);
                exit();
            }

            // 设置处理终止信号的回调函数
            pcntl_signal(SIGTERM, [static::class, 'requestStop']);

            static::log("守护进程启动成功, PID:" . getmypid());

            // 执行子类中的业务逻辑
            try {
                if (static::$processQty > 1) {
                    $this->startWorkers(static::$processQty); // 多进程
                } else {
                    $this->run(); // 单进程
                }
            } catch (\Throwable $th) {
                static::error('执行业务逻辑出错, ' . $th->getMessage(), __FILE__, __LINE__);
                $this->cleanup();
            }

            // 关闭守护进程
            static::shutdown();
        }
    }

    /** 执行业务逻辑 或者  */
    protected function run()
    {
        # 子类循环中需要调用 pcntl_signal_dispatch();  // 处理信号
        static::error('错误 run() 方法需要在子类中实现', __FILE__, __LINE__);
        exit();
    }

    /**
     * 请求停止子进程进程
     */
    public static function stopProcess()
    {
        static::error('错误 stopProcess() 方法因进程通信原因需要在子类中实现', __FILE__, __LINE__);
        exit();
    }

    /** 停止守护进程 */
    protected function stopDaemon()
    {
        if (!file_exists(static::$pidFile)) {
            echo get_called_class() . " is not running.\n";
        } else {
            // 强制关闭标记
            if (static::$isForce) {
                file_put_contents(static::$forceFilename, '1');
            }

            // 发送 SIGTERM 给守护进程
            $pid = (int)file_get_contents(static::$pidFile);
            self::log("发送 SIGTERM 给守护进程 " . $pid);
            if (posix_kill($pid, SIGTERM)) {
                echo get_called_class() . (static::$isForce ? ' force' : '') . " stoping...\n";
            } else {
                echo "Failed to stop " . get_called_class() . " daemon.\n";
            }
        }
    }

    /**
     * 请求停止守护进程
     */
    public static function requestStop()
    {
        static::$stopRequested = true;

        // 发送 SIGTERM 给每个子进程
        if (!empty(static::$childrenProcess)) {
            foreach (static::$childrenProcess as $childPid) {
                self::log("发送 SIGTERM 给子进程 " . $childPid);
                posix_kill($childPid, SIGTERM);
            }
            // 父进程等待所有子进程结束
            while (!empty(static::$childrenProcess)) {
                $pid = pcntl_wait($status); // 等待任意一个子进程结束
                if ($pid > 0) {
                    // 从子进程列表中移除已结束的进程
                    $key = array_search($pid, static::$childrenProcess);
                    if ($key !== false) {
                        unset(static::$childrenProcess[$key]);
                    }
                }
            }
        }


        if (file_exists(static::$forceFilename)) {
            static::log('请求强制关闭');
            static::shutdown();
        } else {
            static::log('请求正常优雅关闭');
        }
    }

    /**
     * 关闭守护进程
     */
    protected static function shutdown()
    {
        if (file_exists(static::$pidFile)) {
            unlink(static::$pidFile);
        }
        if (file_exists(static::$forceFilename)) {
            unlink(static::$forceFilename);
        }
        static::log("PID:" . getmypid() . ' 守护进程已经退出.');
        exit(0);
    }


    /** 创建与监控处理子进程 */
    protected function startWorkers($numWorkers)
    {
        static::$childrenProcess = [];
        for ($i = 0; $i < $numWorkers; $i++) {
            $pid = pcntl_fork();
            if ($pid == -1) {
                die('无法创建子进程');
            } elseif ($pid) {
                // 守护进程代码
                self::log('创建子进程 ' . ($i + 1) . ' PID:' . $pid);
                static::$childrenProcess[] = $pid; // 保存子进程PID以便稍后检查
            } else {
                // 子进程代码
                // 设置处理终止信号的回调函数
                pcntl_signal(SIGTERM, [static::class, 'stopProcess']);
                $this->run();
                exit(0); // 子进程执行完毕后退出
            }
        }

        // 父进程定期检查子进程
        while (!empty(static::$childrenProcess) && !static::$stopRequested) {
            $newChildren = [];
            foreach (static::$childrenProcess as $pid) {
                if (posix_kill($pid, 0) !== -1) {
                    // 子进程仍然存在
                    $newChildren[] = $pid;
                } else {
                    // 子进程已不存在，需要重启
                    static::log("子进程 {$pid} 异常退出，正在重新创建新子进程...");
                    $newPid = pcntl_fork();
                    if ($newPid == -1) {
                        die('无法重启子进程');
                    } elseif ($newPid) {
                        // 守护进程代码
                        $newChildren[] = $newPid; // 新的子进程PID添加到列表中
                    } else {
                        static::log("新子进程 PID:" . getmypid() . " 创建成功");
                        // 设置处理终止信号的回调函数
                        pcntl_signal(SIGTERM, [static::class, 'stopProcess']);
                        // 新的子进程代码
                        $this->run();
                        exit(0); // 新子进程完成后退出
                    }
                }
            }
            static::$childrenProcess = $newChildren; // 更新子进程列表
            sleep(10); // 休眠一段时间再检查
            pcntl_signal_dispatch();
        }
    }

    /** 获取日志文件前缀 */
    private static function getLogFilenamePrefix()
    {
        return static::$logDir . '/' . str_replace('\\', '_', get_called_class());
    }

    /** 打开日志文件 */
    protected static function logOpenHandle()
    {
        $logFile =  static::getLogFilenamePrefix() . '-' . date(static::$logDateFormat) . '.log';
        if (!is_file($logFile)) {
            if (static::$logHandle) {
                fclose(static::$logHandle);
            }
            static::clearLogFile();
        }
        if (!static::$logHandle) {
            static::$logHandle = fopen($logFile, 'ab');
        }
        if (!static::$logHandle) {
            file_put_contents(static::$logDir . '/error.log', '[' . date('Y-m-d H:i:s') . '] 不能够打开日志文件: ' . $logFile);
        }
    }

    /** 清理过期日志文件 */
    protected static function clearLogFile()
    {
        if (static::$logMaxFile == 0) {
            return;
        }
        try {
            $files = glob(static::getLogFilenamePrefix() . '-*.log');
            $now = time();
            foreach ($files as $file) {
                $fileModifiedTime = filemtime($file);
                if (($now - $fileModifiedTime) > (static::$logMaxFile * 24 * 60 * 60)) {
                    unlink($file);
                }
            }
        } catch (\Throwable $th) {
            static::error('清理过期日志文件出错, ' . $th->getMessage(), __FILE__, __LINE__);
        }
    }

    /** 写日志 */
    protected static function log(string $message)
    {
        if (!static::$hasLog) {
            return;
        }
        static::logOpenHandle();

        // 时间到毫秒
        $microtime = microtime(true);
        $milliseconds = round(($microtime - floor($microtime)) * 1000, 0);
        $formattedDate = date('Y-m-d H:i:s') . '.' . str_pad($milliseconds, 3, '0', STR_PAD_LEFT);

        fwrite(static::$logHandle, '[' . $formattedDate . '] ' . $message . "\n");
    }

    /** 错误处理 */
    protected static function error(string $message, string $filename, int $line)
    {
        $errormsg = 'ERROR: ' . $message . ', in line ' . $line . ' of ' . $filename;
        error($errormsg, null, [[__FILE__,__LINE__]]);
        static::log($errormsg);
    }
}
