<?php

/**
 * FastIm v1.0.0
 * FastAdmin企业IM客服系统
 * https://www.fastadmin.net/store/fastim.html
 *
 * Copyright 2021 FastAdmin:thinkphp
 *
 * FastAdmin企业IM客服系统不是开源产品，所有文字、图片、样式、风格等版权归企业IM客服系统作者所有，如有复制、仿冒、抄袭、盗用，FastAdmin和企业IM客服系统作者将追究法律责任
 *
 * Released on: November 8, 2021
 */

namespace addons\fastim\command;

use Swoole\Process;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
use think\Config;
use think\Exception;
use addons\fastim\library\swoole\WebSocket as WebSocketServer;

/**
 * Swoole WebSocket 命令行，支持操作：start|stop|restart|reload
 * 此文件的修改不支持热更新，请于更新后重启swoole-websocket服务
 */
class Swoole extends Command
{
    /**
     * 支持Swoole的所有配置参数
     * 若需使用Swoole的常量,请添加到`init`方法的"检查扩展"之后
     * @config array
     */
    protected $config = [
        'dispatch_mode'            => 2,// 固定模式
        'pid_file'                 => RUNTIME_PATH . 'swoole/fastim-swoole.pid',
        'log_file'                 => RUNTIME_PATH . 'swoole/fastim-swoole.log',
        'heartbeat_check_interval' => 30
    ];

    /**
     * 文件修改监听重启服务
     * @monitor array
     */
    protected $monitor = [
        'file_monitor'          => false, // 是否开启PHP文件更改监控（正式环境勿开，很占cpu）
        'file_monitor_interval' => 3, // 文件变化监控检测时间间隔（秒）
        'file_monitor_path'     => [
            ROOT_PATH . 'addons/fastim',
        ], // 文件监控目录
    ];

    public function configure()
    {
        $this->setName('fastim')
            ->addArgument('action', Argument::OPTIONAL, "start|stop|restart|reload", 'start')
            ->addOption('daemon', 'd', Option::VALUE_NONE, 'Run the swoole server in daemon mode.')
            ->setDescription('Swoole WebSocket Server for FastIm');
    }

    public function initialize(Input $input, Output $output)
    {
        $config       = get_addon_config('fastim');
        $this->config = array_merge($this->config, $config);
    }

    public function execute(Input $input, Output $output)
    {
        $action = $input->getArgument('action');

        $this->init();

        if (in_array($action, ['start', 'stop', 'reload', 'restart'])) {
            $this->$action();
        } else {
            $output->writeln("<error>Invalid argument action:{$action}, Expected start|stop|restart|reload .</error>");
        }
    }

    protected function init()
    {

        // 检查swoole扩展
        if (!extension_loaded('swoole')) {
            throw new Exception('Please install swoole extension. See https://wiki.swoole.com/#/environment');
        }

        // 涉及swoole常量的配置
        $this->config = array_merge($this->config, [
            'log_rotation' => SWOOLE_LOG_ROTATION_DAILY, // 按日期分隔日志
            'log_level'    => SWOOLE_LOG_DEBUG,// 日志等级:调试=SWOOLE_LOG_DEBUG,线上=SWOOLE_LOG_WARNING
        ]);

        if (!is_dir(RUNTIME_PATH . 'swoole')) {
            @mkdir(RUNTIME_PATH . 'swoole', 0755);
        }
        if (empty($this->config['pid_file'])) {
            $this->config['pid_file'] = RUNTIME_PATH . 'swoole/fastim-swoole.pid';
        }

        // 文件名加端口号，避免pid混乱
        $this->config['pid_file'] .= '_' . $this->config['websocket_port'];

        // 线上仅记录警告以上日志
        if (!Config::get('app_debug')) {
            $this->config['log_level'] = SWOOLE_LOG_WARNING;
        }
    }

    /**
     * 启动server
     * @access protected
     * @return void
     */
    protected function start()
    {
        $pid = $this->getMasterPid();

        if ($this->isRunning($pid)) {
            $this->output->writeln('<error>swoole WebSocket server process is already running.</error>');
            return false;
        }

        $this->output->writeln('Starting swoole WebSocket server...');

        $server = new WebSocketServer();

        // 开启守护进程模式
        $exitTips = 'You can exit with <info>`CTRL-C`</info>';
        if ($this->input->hasOption('daemon')) {
            $this->config['daemonize'] = true;
            $exitTips                  = 'Stop swoole WebSocket server command <info>`php think fastim stop`</info>';
        }

        // 设置文件监控
        if ($this->monitor['file_monitor']) {
            $interval = $this->monitor['file_monitor_interval'] ?? 2;
            $paths    = $this->monitor['file_monitor_path'] ?? [];
            $server->setMonitor($interval, $paths);
        }

        $server->option($this->config);

        $agreement = $this->config['wss_switch'] ? 'wss://' : 'ws://';
        $address   = $agreement . "0.0.0.0:" . $this->config['websocket_port'];

        $this->output->writeln("Swoole WebSocket server started: <" . $address . ">");
        $this->output->writeln($exitTips);

        $server->start();
    }

    /**
     * 柔性重启server
     * @access protected
     * @return void
     */
    protected function reload()
    {
        $pid = $this->getMasterPid();

        if (!$this->isRunning($pid)) {
            $this->output->writeln('<error>no swoole WebSocket server process running.</error>');
            return false;
        }

        $this->output->writeln('Reloading swoole WebSocket server...');
        Process::kill($pid, SIGUSR1);
        $this->output->writeln('> success');
    }


    /**
     * 停止server
     * @access protected
     * @return void
     */
    protected function stop()
    {
        $pid = $this->getMasterPid();

        if (!$this->isRunning($pid)) {
            $this->output->writeln('<error>no swoole WebSocket server process running.</error>');
            return false;
        }

        $this->output->writeln('Stopping swoole WebSocket server...');

        Process::kill($pid, SIGTERM);
        $this->removePid();

        $this->output->writeln('> success');
    }


    /**
     * 重启server
     * @access protected
     * @return void
     */
    protected function restart()
    {
        $pid = $this->getMasterPid();

        if ($this->isRunning($pid)) {
            $this->stop();
        }

        $this->start();
    }

    /**
     * 获取主进程PID
     * @access protected
     * @return int
     */
    protected function getMasterPid()
    {
        if (is_file($this->config['pid_file'])) {
            $masterPid = (int)file_get_contents($this->config['pid_file']);
        } else {
            $masterPid = 0;
        }

        return $masterPid;
    }

    /**
     * 删除PID文件
     * @access protected
     * @return void
     */
    protected function removePid()
    {
        if (is_file($this->config['pid_file'])) {
            unlink($this->config['pid_file']);
        }
    }

    /**
     * 判断PID是否在运行
     * @access protected
     * @param int $pid
     * @return bool
     */
    protected function isRunning($pid)
    {
        if (empty($pid)) {
            return false;
        }

        return Process::kill($pid, 0);
    }
}
