[
  {
    "path": ".gitignore",
    "content": "vendor/*\n.DS_Store*\nlog/*\ncomposer.lock\nsrc/master.pid\n"
  },
  {
    "path": ".php_cs",
    "content": "<?php\n$header = <<<'EOF'\nThis file is part of PHP CS Fixer.\n(c) kcloze <pei.greet@qq.com>\nThis source file is subject to the MIT license that is bundled\nwith this source code in the file LICENSE.\nEOF;\n\nreturn PhpCsFixer\\Config::create()\n    ->setRiskyAllowed(true)\n    ->setRules([\n        '@Symfony'                   => true,\n        '@Symfony:risky'             => true,\n        'array_syntax'               => ['syntax' => 'short'],\n        'combine_consecutive_unsets' => true,\n        // one should use PHPUnit methods to set up expected exception instead of annotations\n        'general_phpdoc_annotation_remove'      => ['expectedException', 'expectedExceptionMessage', 'expectedExceptionMessageRegExp'],\n        'header_comment'                        => ['header' => $header],\n        'heredoc_to_nowdoc'                     => true,\n        'no_extra_consecutive_blank_lines'      => ['break', 'continue', 'extra', 'return', 'throw', 'use', 'parenthesis_brace_block', 'square_brace_block', 'curly_brace_block'],\n        'no_unreachable_default_argument_value' => true,\n        'no_useless_else'                       => true,\n        'no_useless_return'                     => true,\n        'ordered_class_elements'                => true,\n        'ordered_imports'                       => true,\n        'php_unit_strict'                       => true,\n        'phpdoc_add_missing_param_annotation'   => true,\n        'phpdoc_order'                          => true,\n        'psr4'                                  => true,\n        'strict_comparison'                     => false,\n        'strict_param'                          => true,\n        'binary_operator_spaces'                => ['align_double_arrow' => true, 'align_equals' => true],\n        'concat_space'                          => ['spacing' => 'one'],\n        'no_empty_statement'                    => true,\n        'simplified_null_return'                => true,\n        'no_extra_consecutive_blank_lines'      => true,\n        'pre_increment'                         => false\n    ])\n    ->setFinder(\n        PhpCsFixer\\Finder::create()\n            ->exclude('vendor')\n            ->in(__DIR__)\n    )\n    ->setUsingCache(false)\n;"
  },
  {
    "path": "README.en.md",
    "content": "# multiprocess [[中文文档]](README.md)\n\n* Based on swoole script management, for multi-process and daemon management\n* Easy to make the common PHP script change daemon and multi-process execution\n* The number of processes can be configured and multiple commands can be executed at once\n* Automatic restart when the child process exits in an abnormal way\n* When the main process exits in an abnormal way, the sub-process exits (smoothing out) after the work is done.\n* Not limited programming language, PHP/Python/Java/Golang/C# and other scripts can be managed\n\n\n\n## Scenario\n\n* PHP requires running one or more cli script consumption queues (resident)\n* The implementation of the script automatically pulls up after the exit, preventing the consumption queue from working, affecting the business\n* In fact, the supervisor can easily do something, this is just another implementation of PHP, no need to change the technology stack\n\n## Flow\n![流程图](flow.png)\n\n\n## Installation\n* git clone https://github.com/kcloze/multiprocess.git\n* composer install\n* modify config.php based on your business configuration\n\n\n## Configure the instance\n* Execute multiple commands at once\n```\n    'logPath'   => __DIR__ . '/log',\n    'exec'      => [\n        [\n            'name'      => 'kcloze-test-1',\n            'bin'       => '/usr/bin/php',\n            'binArgs'   => [__DIR__ . '/test/test.php', 'oop', '123'],\n            'workNum'   => 3,\n        ],\n        [\n            'name'      => 'kcloze-test-2',\n            'bin'       => '/usr/bin/php',\n            'binArgs'   => [__DIR__ . '/test/test2.php', 'oop', '456'],\n            'workNum'   => 5,\n        ],\n        [\n            'name'      => 'kcloze-test-3',\n            'bin'       => '/usr/bin/python',\n            'binArgs'   => [__DIR__ . '/test/test3.py', 'oop', '369'],\n            'workNum'   => 2,\n        ],\n    ],\n\n```\n## Running\n\n### 1.start\n* chmod -R u+r log/\n* php multiprocess.php start >> log/worker.log 2>&1\n### 2.stop\n* php multiprocess.php stop\n### 3.exit\n* php multiprocess.php exit\n### 4.restart\n* php multiprocess.php restart >> log/worker.log 2>&1\n### 5.monitor\n* ps -ef|grep 'multi-process'\n\n### Command\n```\nNAME\n      php multiprocess - manage multiprocess\n\nSYNOPSIS\n      php multiprocess -s command [options] -c config file path\n          Manage multiprocess daemons.\n\n\nWORKFLOWS\n\n\n      help [command]\n      Show this help, or workflow help for command.\n\n      -s restart\n      Stop, then start multiprocess master and workers.\n\n      -s start \n      Start multiprocess master and workers.\n      -s start -c ./config\n      Start multiprocess with specail config file.\n\n\n\n      -s stop\n      Wait all running workers smooth exit, please check multiprocess status for a while.\n\n      -s exit\n      Kill all running workers and master PIDs.\n\n```\n\n\n\n## Monitor\n\n![monitor img](monitor.png)\n\n## Change log\n\n#### 2017-11-30\n* Refactor mercilessly v2 version \n* Add exit param，waiting for the child processes run over\n\n\n## Thanks\n\n* [swoole](http://www.swoole.com/)\n\n## Contact\n\nQQ group：141059677\n\n"
  },
  {
    "path": "README.md",
    "content": "# multiprocess\n\n\n* [[readme in english]](README.en.md)\n* 基于swoole的脚本管理，用于多进程和守护进程管理；\n* 可轻松让普通脚本变守护进程和多进程执行；\n* 进程个数可配置，可以根据配置一次性执行多条命令；\n* 子进程异常退出时,主进程收到信号，自动拉起重新执行；\n* 支持子进程平滑退出，防止重启服务对业务造成影响；\n* 不限定编程语言，PHP/Python/Java/Golang/C#等脚本都可以管理\n\n## 1. 场景\n\n* PHP/python/js等脚本需要跑一个或多个脚本消费队列/计算等任务\n* 实现脚本退出后自动拉起，防止消费队列不工作，影响业务\n* 其实supervisor可以轻松做个事情，这个只是PHP的另一种实现，不需要换技术栈\n\n## 2. 流程图\n![流程图](flow.png)\n\n\n## 3. 安装\n* git clone https://github.com/kcloze/multiprocess.git\n* composer install\n* 根据自己业务配置,修改config.php\n\n\n## 4. 配置实例\n* 一次性执行多个命令\n```\n    'logPath'   => __DIR__ . '/log',\n    'exec'      => [\n        [\n            'name'      => 'kcloze-test-1',\n            'bin'       => '/usr/bin/php',\n            'binArgs'   => [__DIR__ . '/test/test.php', 'oop', '123'],\n            'workNum'   => 3,\n        ],\n        [\n            'name'      => 'kcloze-test-2',\n            'bin'       => '/usr/bin/php',\n            'binArgs'   => [__DIR__ . '/test/test2.php', 'oop', '456'],\n            'workNum'   => 5,\n        ],\n        [\n            'name'      => 'kcloze-test-3',\n            'bin'       => '/usr/bin/python',\n            'binArgs'   => [__DIR__ . '/test/test3.py', 'oop', '369'],\n            'workNum'   => 2,\n        ],\n    ],\n\n```\n## 5. 运行\n\n### 5.1 启动\n* chmod -R u+r log/\n* php multiprocess.php start >> log/system.log 2>&1\n### 5.2 平滑停止服务，根据子进程执行时间等待所有服务停止\n* php multiprocess.php stop\n### 5.3 强制停止服务[慎用]\n* php multiprocess.php exit\n### 5.4 强制重启\n* php multiprocess.php restart >> log/system.log 2>&1\n### 5.5 监控\n* ps -ef|grep 'multi-process'\n\n### 5.6 启动参数说明\n```\nNAME\n      php multiprocess - manage multiprocess\n\nSYNOPSIS\n      php multiprocess -s command [options] -c config file path\n          Manage multiprocess daemons.\n\n\nWORKFLOWS\n\n\n      help [command]\n      Show this help, or workflow help for command.\n\n      -s restart\n      Stop, then start multiprocess master and workers.\n\n      -s start \n      Start multiprocess master and workers.\n      -s start -c ./config\n      Start multiprocess with specail config file.\n\n\n      -s stop\n      Wait all running workers smooth exit, please check multiprocess status for a while.\n\n      -s exit\n      Kill all running workers and master PIDs.\n\n```\n## 6. 服务管理\n### 启动和关闭服务,有两种方式:\n\n#### 6.1 php脚本(主进程挂了之后,需要手动启动)\n```\n./multiprocess.php start|stop|exit|restart\n\n```\n\n\n\n#### 6.2 使用systemd管理(故障重启、开机自启动)\n[更多systemd介绍](https://www.swoole.com/wiki/page/699.html)\n\n```\n1. 根据自己项目路径,修改 systemd/multiprocess.service\n2. sudo cp -f systemd/multiprocess.service /etc/systemd/system/\n3. sudo systemctl --system daemon-reload\n4. 服务管理\n#启动服务\nsudo systemctl start multiprocess.service\n#reload服务\nsudo systemctl reload multiprocess.service\n#关闭服务\nsudo systemctl stop multiprocess.service\n```\n\n\n## 7. 系统状态\n\n![监控图](monitor.png)\n\n## 8. change log\n\n#### 2017-11-30\n* 彻底重构v2版本\n* 增加exit启动参数，默认stop等待子进程平滑退出\n\n\n## 9. 感谢\n\n* [swoole](http://www.swoole.com/)\n\n## 10. 联系\n\nqq群：141059677\n\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"kcloze/multiprocess\",\n    \"description\": \"基于swoole的cli多进程管理\",\n    \"keywords\": [\n        \"swoole\",\n        \"多进程\",\n        \"multi-process\"\n    ],\n    \"homepage\": \"https://github.com/kcloze/multiprocess\",\n    \"license\": \"MIT\",\n    \"require\": {\n        \"php\": \">=7.0\",\n        \"ext-swoole\": \">=1.8.9\"\n    },\n    \"authors\": [\n        {\n            \"name\": \"kcloze\",\n            \"email\": \"pei.greet@qq.com\"\n        }\n    ],\n    \"autoload\": {\n        \"psr-4\": {\n            \"Kcloze\\\\MultiProcess\\\\\": \"src\"\n        }\n    },\n    \"bin\": [\n        \"multiProcess\"\n    ]\n}\n"
  },
  {
    "path": "config.php",
    "content": "<?php\n\n/*\n * This file is part of PHP CS Fixer.\n * (c) kcloze <pei.greet@qq.com>\n * This source file is subject to the MIT license that is bundled\n * with this source code in the file LICENSE.\n */\n\nreturn $config = [\n    //log目录\n    'logPath'          => __DIR__ . '/log',\n    'logSaveFileApp'   => 'application.log', //默认log存储名字\n    'logSaveFileWorker'=> 'workers.log', // 进程启动相关log存储名字\n    'pidPath'          => __DIR__ . '/log',\n    'processName'      => ':swooleMultiProcess', // 设置进程名, 方便管理, 默认值 swooleTopicQueue\n    'sleepTime'        => 3000, // 子进程退出之后，自动拉起暂停毫秒数\n    'redis'            => [\n        'host'  => '192.168.10.129',\n        'port'  => '6379',\n        'preKey'=> 'SwooleMultiProcess-',\n        //'password'=>'',\n        'select' => 0, // 操作库(可选参数，默认0)\n        'serialize' => true, // 是否序列化(可选参数，默认true)\n    ],\n\n    //exec任务相关,name的名字不能相同\n    'exec'      => [\n        [\n            'name'             => 'kcloze-test-1',\n            'bin'              => '/usr/local/php7/bin/php',\n            'binArgs'          => ['/mnt/hgfs/www/saletool/think', 'testAmqp', '0'],\n            'workNum'          => 1, // 外部程序进程数(固定)\n            'queueNumCacheKey' => 'test_mq_queue', // 控制动态进程数队列长度缓存key，注意缓存数据为[\"total\" => 10000,\"update_time\" => 15812345678](可选参数,不设置或为空时只有固定进程)\n            'dynamicWorkNum'   => 2, // 外部程序动态进程数,总进程数=固定+动态(可选参数，设置参数queueNumCacheKey时为必填参数)\n            \n        ],\n        /* [\n            'name'      => 'kcloze-test-1',\n            'bin'       => '/usr/local/bin/php',\n            'binArgs'   => [__DIR__ . '/test/cli/test.php', 'oop', '123'],\n            'workNum'   => 3,\n        ], */\n        /* [\n            'name'      => 'kcloze-test-2',\n            'bin'       => '/usr/local/bin/php',\n            'binArgs'   => [__DIR__ . '/test/cli/test.php', 'oop', '123'],\n            'workNum'   => 2,\n        ], */\n       /*  [\n            'name'      => 'kcloze-test-3',\n            'bin'       => '/usr/local/bin/php',\n            'binArgs'   => [__DIR__ . '/test/cli/test2.php', 'oop', '456'],\n            'workNum'   => 5,\n        ], */\n        // [\n        //     'name'      => 'kcloze-test-3',\n        //     'bin'       => '/usr/bin/python',\n        //     'binArgs'   => [__DIR__ . '/test/cli/test3.py', 'oop', '369'],\n        //     'workNum'   => 2,\n        // ],\n    ],\n];\n"
  },
  {
    "path": "multiprocess",
    "content": "#!/usr/bin/env php\n<?php\n\n/*\n * This file is part of PHP CS Fixer.\n * (c) kcloze <pei.greet@qq.com>\n * This source file is subject to the MIT license that is bundled\n * with this source code in the file LICENSE.\n */\n\ndefine('APP_PATH', __DIR__);\ndate_default_timezone_set('Asia/Shanghai');\n\nrequire APP_PATH . '/vendor/autoload.php';\n\n$param                  = getopt('s:c:');\n$opt                    =$param['s'] ?? '';\n$configFile             =$param['c'] ?? APP_PATH . '/config.php';\nif ($configFile && file_exists($configFile)) {\n    $config = require_once $configFile;\n} else {\n    die('config file can not find!');\n}\n\n$console = new Kcloze\\MultiProcess\\Console($opt, $config);\n$console->run();\n"
  },
  {
    "path": "multiprocess.php",
    "content": "<?php\n\n/*\n * This file is part of PHP CS Fixer.\n * (c) kcloze <pei.greet@qq.com>\n * This source file is subject to the MIT license that is bundled\n * with this source code in the file LICENSE.\n */\n\ndefine('APP_PATH', __DIR__);\ndate_default_timezone_set('Asia/Shanghai');\n\nrequire APP_PATH . '/vendor/autoload.php';\n\n$param                  = getopt('s:c::');\n$opt                    =$param['s'] ?? '';\n$configFile             =$param['c'] ?? APP_PATH . '/config.php';\nif ($configFile && file_exists($configFile)) {\n    $config = require_once $configFile;\n} else {\n    die('config file can not find!');\n}\n\n$console = new Kcloze\\MultiProcess\\Console($opt, $config);\n$console->run();\n"
  },
  {
    "path": "src/Config.php",
    "content": "<?php\n\n/*\n * This file is part of PHP CS Fixer.\n * (c) kcloze <pei.greet@qq.com>\n * This source file is subject to the MIT license that is bundled\n * with this source code in the file LICENSE.\n */\n\nnamespace Kcloze\\MultiProcess;\n\nclass Config\n{\n    private static $config=[];\n\n    public static function setConfig($config)\n    {\n        self::$config=$config;\n    }\n\n    public static function getConfig()\n    {\n        return self::$config;\n    }\n\n    public static function hasRepeatingName($config=[], $chckKey='name')\n    {\n        $nameList=[];\n        foreach ($config as $key => $value) {\n            if (isset($nameList[$value[$chckKey]])) {\n                return true;\n            }\n            $nameList[$value[$chckKey]]=$value[$chckKey];\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/Console.php",
    "content": "<?php\n\n/*\n * This file is part of PHP CS Fixer.\n * (c) kcloze <pei.greet@qq.com>\n * This source file is subject to the MIT license that is bundled\n * with this source code in the file LICENSE.\n */\n\nnamespace Kcloze\\MultiProcess;\n\nclass Console\n{\n    public $logger    = null;\n    private $config   = [];\n    private $opt      = [];\n    private $redis    = null;\n\n    public function __construct($opt, $config)\n    {\n        $this->opt=$opt;\n        if (empty($this->opt)) {\n            $this->printHelpMessage();\n            exit(1);\n        }\n        Config::setConfig($config);\n        $this->config = Config::getConfig();\n        $this->logger = new Logs(Config::getConfig()['logPath'] ?? '', $this->config['logSaveFileApp'] ?? '');\n    }\n\n    public function run()\n    {\n        $this->runOpt();\n    }\n\n    public function start()\n    {\n        //启动\n        $process = new Process();\n        $process->start();\n    }\n\n    /**\n     * 给主进程发送信号：\n     *  SIGUSR1 自定义信号，让子进程平滑退出\n     *  SIGTERM 程序终止，让子进程强制退出.\n     *\n     * @param [type] $signal\n     */\n    public function stop($signal=SIGUSR1)\n    {\n        $this->logger->log(($signal == SIGUSR1) ? 'smooth to exit...' : 'force to exit...');\n\n        if (isset($this->config['pidPath']) && !empty($this->config['pidPath'])) {\n            $masterPidFile=$this->config['pidPath'] . '/master.pid';\n        } else {\n            die('config pidPath must be set!');\n        }\n\n        if (file_exists($masterPidFile)) {\n            $ppid=file_get_contents($masterPidFile);\n            if (empty($ppid)) {\n                exit('service is not running' . PHP_EOL);\n            }\n            //给主进程发送信号\n            if (@\\Swoole\\Process::kill($ppid, $signal)) {\n                $this->logger->log('[pid: ' . $ppid . '] has been stopped success');\n            } else {\n                $this->logger->log('[pid: ' . $ppid . '] has been stopped fail');\n            }\n            $this->getRedis()->set(Process::REDIS_MASTER_KEY, Process::STATUS_WAIT);\n        } else {\n            exit('service is not running' . PHP_EOL);\n        }\n    }\n\n    public function restart()\n    {\n        $this->logger->log('restarting...');\n        $this->exit();\n        sleep(3);\n        $this->start();\n    }\n\n    public function exit()\n    {\n        $this->stop(SIGTERM);\n    }\n\n    public function runOpt()\n    {\n        switch ($this->opt) {\n            case 'start':\n                $this->start();\n                break;\n            case 'stop':\n                $this->stop();\n                break;\n            case 'exit':\n                $this->exit();\n                break;\n            case 'restart':\n                $this->restart();\n                break;\n            case 'help':\n                $this->printHelpMessage();\n                break;\n\n            default:\n                $this->printHelpMessage();\n                break;\n        }\n    }\n\n    public function printHelpMessage()\n    {\n        $msg=<<<'EOF'\nNAME\n      php multiprocess - manage multiprocess\n\nSYNOPSIS\n      php multiprocess command [options]\n          Manage multiprocess daemons.\n\n\nWORKFLOWS\n\n\n      help [command]\n      Show this help, or workflow help for command.\n\n\n      -s restart\n      Stop, then start multiprocess master and workers.\n\n      -s start \n      Start multiprocess master and workers.\n      -s start -c=./config\n      Start multiprocess with specail config file.\n\n      -s stop\n      Wait all running workers smooth exit, please check multiprocess status for a while.\n\n      -s exit\n      Kill all running workers and master PIDs.\n\n\nEOF;\n        echo $msg;\n    }\n\n    private function getRedis()\n    {\n        if ($this->redis && $this->redis->ping()) {\n            return $this->redis;\n        }\n        $this->redis   = new XRedis($this->config['redis']);\n\n        return $this->redis;\n    }\n}\n"
  },
  {
    "path": "src/Logs.php",
    "content": "<?php\n\n/*\n * This file is part of PHP CS Fixer.\n * (c) kcloze <pei.greet@qq.com>\n * This source file is subject to the MIT license that is bundled\n * with this source code in the file LICENSE.\n */\n\nnamespace Kcloze\\MultiProcess;\n\nclass Logs\n{\n    const LEVEL_TRACE          = 'trace';\n    const LEVEL_WARNING        = 'warning';\n    const LEVEL_ERROR          = 'error';\n    const LEVEL_INFO           = 'info';\n    const LEVEL_PROFILE        = 'profile';\n    const MAX_LOGS             = 10000;\n\n    public $rotateByCopy       = true;\n    public $maxLogFiles        = 5;\n    public $maxFileSize        = 100; // in MB\n\n    private $logPath      = '';\n    //单个类型log\n    private $logs                 = [];\n    private $logCount             = 0;\n    //默认log文件存储名\n    private $logSaveFileApp       = 'application.log';\n\n    private static $instance=null;\n\n    public function __construct($logPath, $logSaveFileApp='')\n    {\n        if (empty($logPath)) {\n            die('config logPath must be set!' . PHP_EOL);\n        }\n        Utils::mkdir($logPath);\n        $this->logPath = $logPath;\n        if ($logSaveFileApp) {\n            $this->logSaveFileApp = $logSaveFileApp;\n        }\n    }\n\n    /**\n     * 获取日志实例.\n     *\n     * @$logPath\n     *\n     * @param mixed $logPath\n     * @param mixed $logSaveFileApp\n     */\n    public static function getLogger($logPath='', $logSaveFileApp='')\n    {\n        if (isset(self::$instance) && self::$instance !== null) {\n            return self::$instance;\n        }\n        self::$instance=new self($logPath, $logSaveFileApp);\n\n        return self::$instance;\n    }\n\n    /**\n     * 格式化日志信息.\n     *\n     * @param mixed $message\n     * @param mixed $level\n     * @param mixed $category\n     * @param mixed $time\n     */\n    public function formatLogMessage($message, $level, $category, $time)\n    {\n        return @date('Y/m/d H:i:s', $time) . \" [$level] [$category] $message\\n\";\n    }\n\n    /**\n     * 日志分类处理.\n     *\n     * @param mixed $message\n     * @param mixed $level\n     * @param mixed $category\n     * @param mixed $flush\n     */\n    public function log($message, $level = 'info', $category = '', $flush = true)\n    {\n        if (empty($category)) {\n            $category=$this->logSaveFileApp;\n        }\n        $this->logs[$category][]      = [$message, $level, $category, microtime(true)];\n        $this->logCount++;\n        if ($this->logCount >= self::MAX_LOGS || true == $flush) {\n            $this->flush($category);\n        }\n    }\n\n    /**\n     * 日志分类处理.\n     */\n    public function processLogs()\n    {\n        $logsAll=[];\n        foreach ((array) $this->logs as $key => $logs) {\n            $logsAll[$key] = '';\n            foreach ((array) $logs as $log) {\n                $logsAll[$key] .= $this->formatLogMessage($log[0], $log[1], $log[2], $log[3]);\n            }\n        }\n\n        return $logsAll;\n    }\n\n    /**\n     * 写日志到文件.\n     */\n    public function flush()\n    {\n        if ($this->logCount <= 0) {\n            return false;\n        }\n        $logsAll = $this->processLogs();\n        $this->write($logsAll);\n        $this->logs     = [];\n        $this->logCount = 0;\n    }\n\n    /**\n     * [write 根据日志类型写到不同的日志文件].\n     *\n     * @param $logsAll\n     *\n     * @throws \\Exception\n     */\n    public function write($logsAll)\n    {\n        if (empty($logsAll)) {\n            return;\n        }\n        //$this->logPath = ROOT_PATH . 'src/runtime/';\n        if (!is_dir($this->logPath)) {\n            self::mkdir($this->logPath, [], true);\n        }\n        foreach ($logsAll as $key => $value) {\n            if (empty($key)) {\n                continue;\n            }\n            $fileName = $this->logPath . '/' . $key;\n\n            if (($fp = @fopen($fileName, 'a')) === false) {\n                throw new \\Exception(\"Unable to append to log file: {$fileName}\");\n            }\n            @flock($fp, LOCK_EX);\n\n            if (@filesize($fileName) > $this->maxFileSize * 1024 * 1024) {\n                $this->rotateFiles($fileName);\n            }\n            @fwrite($fp, $value);\n            @flock($fp, LOCK_UN);\n            @fclose($fp);\n        }\n    }\n\n    /**\n     * Rotates log files.\n     *\n     * @param mixed $file\n     */\n    protected function rotateFiles($file)\n    {\n        for ($i = $this->maxLogFiles; $i >= 0; --$i) {\n            // $i == 0 is the original log file\n            $rotateFile = $file . ($i === 0 ? '' : '.' . $i);\n            //var_dump($rotateFile);\n            if (is_file($rotateFile)) {\n                // suppress errors because it's possible multiple processes enter into this section\n                if ($i === $this->maxLogFiles) {\n                    @unlink($rotateFile);\n                } else {\n                    if ($this->rotateByCopy) {\n                        @copy($rotateFile, $file . '.' . ($i + 1));\n                        if ($fp = @fopen($rotateFile, 'a')) {\n                            @ftruncate($fp, 0);\n                            @fclose($fp);\n                        }\n                    } else {\n                        @rename($rotateFile, $file . '.' . ($i + 1));\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Shared environment safe version of mkdir. Supports recursive creation.\n     * For avoidance of umask side-effects chmod is used.\n     *\n     * @param string $dst       path to be created\n     * @param array  $options   newDirMode element used, must contain access bitmask\n     * @param bool   $recursive whether to create directory structure recursive if parent dirs do not exist\n     *\n     * @return bool result of mkdir\n     *\n     * @see mkdir\n     */\n    private static function mkdir($dst, array $options, $recursive)\n    {\n        $prevDir = dirname($dst);\n        if ($recursive && !is_dir($dst) && !is_dir($prevDir)) {\n            self::mkdir(dirname($dst), $options, true);\n        }\n        $mode = isset($options['newDirMode']) ? $options['newDirMode'] : 0777;\n        $res  = mkdir($dst, $mode);\n        @chmod($dst, $mode);\n\n        return $res;\n    }\n}\n"
  },
  {
    "path": "src/Process.php",
    "content": "<?php\n\n/*\n * This file is part of PHP CS Fixer.\n * (c) kcloze <pei.greet@qq.com>\n * This source file is subject to the MIT license that is bundled\n * with this source code in the file LICENSE.\n */\n\nnamespace Kcloze\\MultiProcess;\n\nclass Process\n{\n    const CHILD_PROCESS_CAN_RESTART        ='staticWorker'; //子进程可以重启,进程个数固定\n    const CHILD_PROCESS_CAN_NOT_RESTART    ='dynamicWorker'; //子进程不可以重启，进程个数根据队列堵塞情况动态分配\n\n    const STATUS_START                     ='start'; //主进程启动中状态\n    const STATUS_RUNNING                   ='runnning'; //主进程正常running状态\n    const STATUS_WAIT                      ='wait'; //主进程wait状态\n    const STATUS_STOP                      ='stop'; //主进程stop状态\n    const STATUS_RECOVER                   ='recover'; //主进程recover状态\n    const REDIS_MASTER_KEY                 ='Status'; //Redis主进程状态key\n    const REDIS_WORKER_STATUS_KEY          ='Status-'; //Redis 子进程状态key\n    const REDIS_WORKER_MEMBER_KEY          ='Members-'; //主进程recover状态\n\n    public $processName    = ':swooleMultiProcess'; // 进程重命名, 方便 shell 脚本管理\n    private $workers;\n    private $workersByPidName;\n    private $ppid;\n    private $configWorkersByNameNum;\n    private $checkTickTimer       = 5000; //检查服务是否正常定时器,单位ms\n    private $sleepTime            = 2000; //子进程退出之后，自动拉起暂停毫秒数\n    private $config               = [];\n    private $pidFile              = 'master.pid';\n    private $status               =''; //主进程状态\n    private $timer                =''; //定时器id\n    private $redis                =null; //redis连接\n    private $logSaveFileWorker    = 'workers.log';\n\n    private $queueMaxNum          = 1000; //队列达到一定长度，增加子进程个数\n    private $workersInfoList      = []; // 子进程队列\n    private $dynamicWorkerNum     = []; //动态（不能重启）子进程计数，最大数为每个脚本配置dynamicWorkNum，它的个数是动态变化的\n\n    public function __construct()\n    {\n        $this->config  =  Config::getConfig();\n\n        if (Config::hasRepeatingName($this->config['exec'], 'name')) {\n            die('exec name has repeating name,fetal error!');\n        }\n        $this->logger = new Logs(Config::getConfig()['logPath'] ?? '', $this->config['logSaveFileApp'] ?? '');\n\n        if (isset($this->config['pidPath']) && !empty($this->config['pidPath'])) {\n            Utils::mkdir($this->config['pidPath']);\n            $this->pidFile    =$this->config['pidPath'] . '/' . $this->pidFile;\n        } else {\n            die('config pidPath must be set!');\n        }\n        if (isset($this->config['processName']) && !empty($this->config['processName'])) {\n            $this->processName = $this->config['processName'];\n        }\n        if (isset($this->config['sleepTime']) && !empty($this->config['sleepTime'])) {\n            $this->sleepTime = $this->config['sleepTime'];\n        }\n        if (isset($this->config['logSaveFileWorker']) && !empty($this->config['logSaveFileWorker'])) {\n            $this->logSaveFileWorker = $this->config['logSaveFileWorker'];\n        }\n\n        /*\n         * master.pid 文件记录 master 进程 pid, 方便之后进程管理\n         * 请管理好此文件位置, 使用 systemd 管理进程时会用到此文件\n         * 判断文件是否存在，并判断进程是否在运行\n         */\n\n        if (file_exists($this->pidFile)) {\n            $pid=$this->getMasterPid();\n            if ($pid && @\\Swoole\\Process::kill($pid, 0)) {\n                die('已有进程运行中,请先结束或重启' . PHP_EOL);\n            }\n        }\n\n        \\Swoole\\Process::daemon();\n        $this->ppid    = getmypid();\n        $this->saveMasterPid();\n        $this->setProcessName('multiprocess master ' . $this->ppid . $this->processName);\n    }\n\n    /**\n     * 启动主进程.\n     */\n    public function start()\n    {\n        $this->saveMasterData([self::REDIS_MASTER_KEY =>self::STATUS_START]);\n        if (!isset($this->config['exec'])) {\n            die('config exec must be not null!');\n        }\n        $this->logger->log('process start pid: ' . $this->ppid, 'info', $this->logSaveFileWorker);\n\n        $this->configWorkersByNameNum=[];\n        foreach ($this->config['exec'] as $key => $value) {\n            if (!isset($value['bin']) || !isset($value['binArgs'])) {\n                $this->logger->log('config bin/binArgs must be not null!', 'error', $this->logSaveFileWorker);\n            }\n\n            $workOne['bin']     = $value['bin'];\n            $workOne['name']    = $value['name'];\n            $workOne['binArgs'] = $value['binArgs'];\n            //开启多个子进程\n            for ($i = 0; $i < $value['workNum']; ++$i) {\n                $this->reserveExec($i, $workOne, self::CHILD_PROCESS_CAN_RESTART);\n            }\n            $this->configWorkersByNameNum[$value['name']] = $value['workNum'];\n        }\n\n        if (empty($this->timer)) {\n            $this->registSignal();\n            $this->registTimer();\n        }//启动成功，修改状态\n\n        $this->saveMasterData([self::REDIS_MASTER_KEY=>self::STATUS_RUNNING]);\n    }\n\n    public function startByWorkerName($workName)\n    {\n        $this->saveMasterData([self::REDIS_WORKER_STATUS_KEY . $workName=>self::STATUS_START]);\n        foreach ($this->config['exec'] as $key => $value) {\n            if ($value['name'] != $workName) {\n                continue;\n            }\n            if (!isset($value['bin']) || !isset($value['binArgs'])) {\n                $this->logger->log('config bin/binArgs must be not null!', 'error', $this->logSaveFileWorker);\n            }\n\n            $workOne['bin']     = $value['bin'];\n            $workOne['name']    = $value['name'];\n            $workOne['binArgs'] = $value['binArgs'];\n            //开启多个子进程\n            for ($i = 0; $i < $value['workNum']; ++$i) {\n                $this->reserveExec($i, $workOne);\n            }\n        }\n\n        $this->saveMasterData([self::REDIS_WORKER_STATUS_KEY . $workName=>self::STATUS_RUNNING]);\n    }\n\n    /**\n     * 启动子进程，跑业务代码\n     *\n     * @param int    $workNum\n     * @param mixed  $workOne\n     * @param string $workerType 是否会重启 canRestart|unRestart\n     */\n    public function reserveExec($workNum, $workOne, $workerType=self::CHILD_PROCESS_CAN_RESTART)\n    {\n        $reserveProcess = new \\Swoole\\Process(function ($worker) use ($workNum, $workOne) {\n            usleep($this->sleepTime * 1000); // usleep单位是微妙，$this->sleepTime * 1000 转为毫秒\n            $this->checkMpid($worker);\n            try {\n                $this->logger->log('Worker exec: ' . $workOne['bin'] . ' ' . implode(' ', $workOne['binArgs']), 'info', $this->logSaveFileWorker);\n                //执行一个外部程序\n                $worker->exec($workOne['bin'], $workOne['binArgs']);\n            } catch (\\Throwable $e) {\n                Utils::catchError($this->logger, $e);\n            } catch (\\Exception $e) {\n                Utils::catchError($this->logger, $e);\n            }\n            $this->logger->log('worker id: ' . $workNum . ' is done!!!', 'info', $this->logSaveFileWorker);\n            $worker->exit(0);\n        });\n        $pid                                       = $reserveProcess->start();\n        $this->workers[$pid]                       = $reserveProcess;\n        $this->workersInfoList[$pid]['type']       = $workerType;\n        $this->workersInfoList[$pid]['workOne']    = $workOne;\n        $this->setWorkerList(self::REDIS_WORKER_MEMBER_KEY . $workOne['name'], $pid, 'add');\n        $this->workersByPidName[$pid]              = $workOne['name'];\n        $this->saveMasterData([self::REDIS_WORKER_STATUS_KEY . $workOne['name'] =>self::STATUS_RUNNING]);\n        $this->logger->log('worker id: ' . $workNum . ' pid: ' . $pid . ' is start... ' . $workerType, 'info', $this->logSaveFileWorker);\n    }\n\n    /**\n     * 注册信号.\n     */\n    public function registSignal()\n    {\n        \\Swoole\\Process::signal(SIGTERM, function ($signo) {\n            $this->killWorkersAndExitMaster();\n        });\n        \\Swoole\\Process::signal(SIGKILL, function ($signo) {\n            $this->killWorkersAndExitMaster();\n        });\n        \\Swoole\\Process::signal(SIGUSR1, function ($signo) {\n            $this->waitWorkers();\n        });\n        \\Swoole\\Process::signal(SIGCHLD, function ($signo) {\n            while (true) {\n                // 捕获回收子进程异常\n                try {\n                    $ret = \\Swoole\\Process::wait(false);\n                } catch (\\Exception $e) {\n                    $this->logger->log('signoError: ' . $signo . $e->getMessage(), 'error', Logs::LOG_SAVE_FILE_WORKER);\n                }\n                if ($ret) {\n                    $pid           = $ret['pid'];\n                    $childProcess = $this->workers[$pid];\n                    $workName = $this->workersByPidName[$pid];\n                    $workerType = $this->workersInfoList[$pid]['type'];\n                    $this->status=$this->getMasterData(self::REDIS_MASTER_KEY);\n                    //根据wokerName，获取其运行状态\n                    $workNameStatus=$this->getMasterData(self::REDIS_WORKER_STATUS_KEY . $workName);\n                    //子进程为可重启进程，主进程状态为start,running且子进程组不是recover状态才需要拉起子进程\n                    if (self::CHILD_PROCESS_CAN_RESTART == $workerType && self::STATUS_RECOVER != $workNameStatus && (self::STATUS_RUNNING == $this->status || self::STATUS_START == $this->status)) {\n                        try {\n                            $i=0;\n                            //重启有可能失败，最多尝试10次\n                            while ($i <= 10) {\n                                $newPid  = $childProcess->start();\n                                if ($newPid > 0) {\n                                    break;\n                                }\n                                $this->logger->log($workName . '子进程重启失败，子进程尝试' . $i . '次重启', 'info', $this->logSaveFileWorker);\n\n                                ++$i;\n                            }\n                        } catch (\\Throwable $e) {\n                            Utils::catchError($this->logger, $e, 'error: woker restart fail...');\n                        } catch (\\Exception $e) {\n                            Utils::catchError($this->logger, $e, 'error: woker restart fail...');\n                        }\n                        if ($newPid > 0) {\n                            $this->logger->log(\"Worker Restart, kill_signal={$ret['signal']} PID=\" . $newPid, 'info', $this->logSaveFileWorker);\n                            $this->workers[$newPid] = $childProcess;\n                            $this->workersInfoList[$newPid]['type']      = $workerType;\n                            $this->workersInfoList[$newPid]['workOne']   = $this->workersInfoList[$pid]['workOne'];\n                            $this->setWorkerList(self::REDIS_WORKER_MEMBER_KEY . $workName, $newPid, 'add');\n                            $this->workersByPidName[$newPid]        = $workName;\n                            $this->saveMasterData([self::REDIS_WORKER_STATUS_KEY . $workName=>self::STATUS_RUNNING]);\n                        } else {\n                            $this->saveMasterData([self::REDIS_WORKER_STATUS_KEY . $workName=>self::STATUS_RECOVER]);\n                            $this->logger->log($workName . '子进程重启失败，该组子进程进入recover状态', 'info', $this->logSaveFileWorker);\n                        }\n                    }\n                    // 动态子进程\n                    if (self::CHILD_PROCESS_CAN_NOT_RESTART == $workerType) {\n                        --$this->dynamicWorkerNum[$workName];\n                    }\n                    $this->logger->log(\"Worker Exit, kill_signal={$ret['signal']} PID=\" . $pid, 'info', $this->logSaveFileWorker);\n                    unset($this->workers[$pid], $this->workersByPidName[$pid], $this->workersInfoList[$pid]);\n                    $this->setWorkerList(self::REDIS_WORKER_MEMBER_KEY . $workName, $pid, 'del');\n                    $this->logger->log('Worker count: ' . \\count($this->workers) . '  [' . $workName . ']  ' . $this->configWorkersByNameNum[$workName], 'info', $this->logSaveFileWorker);\n                    //如果$this->workers为空，且主进程状态为wait，说明所有子进程安全退出，这个时候主进程退出\n                    if (empty($this->workers) && self::STATUS_WAIT == $this->status) {\n                        $this->logger->log('主进程收到所有信号子进程的退出信号，子进程安全退出完成', 'info', $this->logSaveFileWorker);\n                        $this->exitMaster();\n                    }\n                } else {\n                    break;\n                }\n            }\n        });\n    }\n\n    /**\n     * 注册定时器.\n     */\n    public function registTimer()\n    {\n        $this->timer=\\Swoole\\Timer::tick($this->checkTickTimer, function ($timerId) {\n            $workNameStatus = '';\n            foreach ($this->configWorkersByNameNum as $workName => $value) {\n                $this->status  =$this->getMasterData(self::REDIS_MASTER_KEY);\n                $workNameStatus=$this->getMasterData(self::REDIS_WORKER_STATUS_KEY . $workName);\n                $workNameMembers=$this->getWorkerList(self::REDIS_WORKER_MEMBER_KEY . $workName);\n                $this->checkChildProcess($workName, $workNameMembers);\n                $count=\\count($workNameMembers);\n                if ($count <= 0) {\n                    $this->saveMasterData([self::REDIS_WORKER_STATUS_KEY . $workName=>self::STATUS_START]);\n                    $this->startByWorkerName($workName);\n                    $this->logger->log('主进程 recover 子进程：' . $workName, 'info', $this->logSaveFileWorker);\n                }\n                $this->logger->log('主进程状态：' . $this->status . ' 数量：' . \\count($this->workers), 'info', $this->logSaveFileWorker);\n                $this->logger->log('[' . $workName . ']子进程状态：' . $workNameStatus . ' 数量：' . $count . ' pids:' . serialize($workNameMembers), 'info', $this->logSaveFileWorker);\n            }\n            // 动态进程控制todo\n            foreach ($this->config['exec'] as $key => $value) {\n                if (!isset($value['dynamicWorkNum']) || $value['dynamicWorkNum'] < 1 || !isset($value['queueNumCacheKey']) || !$value['queueNumCacheKey']) {\n                    continue;\n                }\n                if (!isset($value['bin']) || !isset($value['binArgs'])) {\n                    $this->logger->log('config bin/binArgs must be not null!', 'error', $this->logSaveFileWorker);\n                }\n                // 获取队列的缓存数据，根据入队列数量控制动态进程\n                $queueCacheData = $this->getCacheData($value['queueNumCacheKey']);\n                if(!$queueCacheData || !isset($queueCacheData[\"total\"]) || !isset($queueCacheData[\"update_time\"])) {\n                    continue;\n                }\n                \n                $this->dynamicWorkerNum[$value['name']] = isset($this->dynamicWorkerNum[$value['name']]) ? $this->dynamicWorkerNum[$value['name']] : 0;\n                if ($queueCacheData[\"total\"] < $this->queueMaxNum || $this->dynamicWorkerNum[$value['name']] >= $value['dynamicWorkNum']) {\n                    continue;\n                }\n                // 由缓存的total和update_time组成的key控制是否需要启动进程(只运行一次)\n                $runOneTimeKey = \"sw_process_\".$value['name'].\"_\".$queueCacheData[\"total\"].\"_\".$queueCacheData[\"update_time\"];\n                $runOneTimeRes = $this->getCacheData($runOneTimeKey);\n                if($runOneTimeRes) {\n                    continue;\n                }\n                $workOne['bin']   = $value['bin'];\n                $workOne['name']  = $value['name'];\n                $workOne['binArgs']= $value['binArgs'];\n                $canStartNum = $value['dynamicWorkNum'] - $this->dynamicWorkerNum[$value['name']];\n                //开启多个子进程\n                for ($i = 0; $i < $canStartNum; ++$i) {\n                    $this->reserveExec($i, $workOne, self::CHILD_PROCESS_CAN_NOT_RESTART);\n                    ++$this->dynamicWorkerNum[$value['name']];\n                }\n\n                $this->setCacheData($runOneTimeKey,1,7200);\n            }\n        });\n    }\n\n    //检查子进程是否还活着\n    private function checkChildProcess($workName, $members)\n    {\n        foreach ($members as $key => $pid) {\n            if ($pid) {\n                if (!@\\Swoole\\Process::kill($pid, 0)) {\n                    unset($this->workers[$pid], $this->workersByPidName[$pid]);\n                    $this->setWorkerList(self::REDIS_WORKER_MEMBER_KEY . $workName, $pid, 'del');\n                    $this->logger->log('子进程异常退出：' . $pid . ' name：' . $workName, 'error', $this->logSaveFileWorker);\n                } else {\n                    $this->logger->log('子进程正常：' . $pid . ' name：' . $workName, 'info', $this->logSaveFileWorker);\n                }\n            }\n        }\n    }\n\n    //平滑等待子进程退出之后，再退出主进程\n    private function killWorkersAndExitMaster()\n    {\n        //修改主进程状态为stop\n        $this->status              =self::STATUS_STOP;\n        $this->saveMasterData([self::REDIS_MASTER_KEY=>self::STATUS_STOP]);\n\n        if ($this->workers) {\n            foreach ($this->workers as $pid => $worker) {\n                //强制杀workers子进程\n                if (true == \\Swoole\\Process::kill($pid)) {\n                    unset($this->workers[$pid]);\n                    $this->logger->log('子进程[' . $pid . ']收到强制退出信号,退出成功', 'info', $this->logSaveFileWorker);\n                } else {\n                    $this->logger->log('子进程[' . $pid . ']收到强制退出信号,但退出失败', 'info', $this->logSaveFileWorker);\n                }\n\n                $this->logger->log('Worker count: ' . \\count($this->workers), 'info', $this->logSaveFileWorker);\n            }\n        }\n        $this->exitMaster();\n    }\n\n    //强制杀死子进程并退出主进程\n    private function waitWorkers()\n    {\n        //修改主进程状态为wait\n\n        $this->saveMasterData([self::REDIS_MASTER_KEY=>self::STATUS_WAIT]);\n        $this->status = self::STATUS_WAIT;\n        foreach ($this->configWorkersByNameNum as $key => $value) {\n            $workName                  =$key;\n            $this->saveMasterData([self::REDIS_WORKER_STATUS_KEY . $workName=>self::STATUS_WAIT]);\n        }\n    }\n\n    //退出主进程\n    private function exitMaster()\n    {\n        @unlink($this->pidFile);\n        $this->clearMasterData();\n        $this->logger->log('Time: ' . microtime(true) . '主进程' . $this->ppid . '退出', 'info', $this->logSaveFileWorker);\n        sleep(1);\n        exit();\n    }\n\n    /**\n     * 设置进程名.\n     *\n     * @param mixed $name\n     */\n    private function setProcessName($name)\n    {\n        //mac os不支持进程重命名\n        if (\\function_exists('swoole_set_process_name') && PHP_OS != 'Darwin') {\n            swoole_set_process_name($name);\n        }\n    }\n\n    //主进程如果不存在了，子进程退出\n    private function checkMpid(&$worker)\n    {\n        if (!@\\Swoole\\Process::kill($this->ppid, 0)) {\n            $worker->exit();\n            $this->logger->log(\"Master process exited, I [{$worker['pid']}] also quit\");\n        }\n    }\n\n    private function saveMasterPid()\n    {\n        file_put_contents($this->pidFile, $this->ppid);\n    }\n\n    private function getMasterPid()\n    {\n        return file_get_contents($this->pidFile);\n    }\n\n    private function saveMasterData($data=[])\n    {\n        $this->redis   = $this->getRedis();\n        foreach ((array) $data as $key => $value) {\n            $key && $this->redis->set($key, $value);\n        }\n    }\n\n    private function clearMasterData()\n    {\n        $this->redis = $this->getRedis();\n\n        $data=$this->configWorkersByNameNum;\n        foreach ((array) $data as $key => $value) {\n            $value && $this->redis->del(self::REDIS_WORKER_STATUS_KEY . $key);\n            $value && $this->redis->del(self::REDIS_WORKER_MEMBER_KEY . $key);\n            $this->logger->log('主进程退出前删除woker redis key： ' . $key, 'info', $this->logSaveFileWorker);\n        }\n        $this->redis->del(self::REDIS_MASTER_KEY);\n\n        $this->logger->log('主进程退出前删除master redis key： status', 'info', $this->logSaveFileWorker);\n    }\n\n    private function setWorkerList($key, $member, $opt='add')\n    {\n        $this->redis = $this->getRedis();\n        if ('add' == $opt) {\n            return $this->redis->sAdd($key, $member);\n        } elseif ('del' == $opt) {\n            return $this->redis->sRemove($key, $member);\n        }\n    }\n\n    /**\n     * 获取子进程列表.\n     *\n     * @param string $key\n     *\n     * @return mixed\n     */\n    private function getWorkerList($key)\n    {\n        $this->redis = $this->getRedis();\n\n        return $this->redis->sMembers($key);\n    }\n\n    /**\n     * 获取主进程数据.\n     *\n     * @param string $key\n     *\n     * @return mixed\n     */\n    private function getMasterData($key)\n    {\n        $this->redis = $this->getRedis();\n        if ($key) {\n            return $this->redis->get($key);\n        }\n    }\n\n    /**\n     * 获取缓存数据.\n     *\n     * @param string $key\n     *\n     * @return mixed\n     */\n    private function getCacheData($key)\n    {\n        $this->redis = $this->getRedis();\n        if ($key) {\n            return $this->redis->get($key);\n        }\n\n        return false;\n    }\n\n    /**\n     * 设置缓存数据.\n     *\n     * @param string $key\n     *\n     * @return mixed\n     */\n    private function setCacheData($key,$value,$timeout = 3600)\n    {\n        $this->redis = $this->getRedis();\n        return $this->redis->set($key,$value,$timeout);\n    }\n\n    /**\n     * 获取redis实例.\n     */\n    private function getRedis()\n    {\n        if ($this->redis && $this->redis->ping()) {\n            return $this->redis;\n        }\n        $this->redis   = new XRedis($this->config['redis']);\n\n        return $this->redis;\n    }\n}\n"
  },
  {
    "path": "src/Utils.php",
    "content": "<?php\n\n/*\n * This file is part of PHP CS Fixer.\n * (c) kcloze <pei.greet@qq.com>\n * This source file is subject to the MIT license that is bundled\n * with this source code in the file LICENSE.\n */\n\nnamespace Kcloze\\MultiProcess;\n\nclass Utils\n{\n    /**\n     * 循环创建目录.\n     *\n     * @param mixed $path\n     * @param mixed $recursive\n     * @param mixed $mode\n     */\n    public static function mkdir($path, $mode=0777, $recursive=true)\n    {\n        if (!is_dir($path)) {\n            mkdir($path, $mode, $recursive);\n        }\n    }\n\n    public static function catchError($logger, $exception, $error='')\n    {\n        $error .= '错误类型：' . get_class($exception) . PHP_EOL;\n        $error .= '错误代码：' . $exception->getCode() . PHP_EOL;\n        $error .= '错误信息：' . $exception->getMessage() . PHP_EOL;\n        $error .= '错误堆栈：' . $exception->getTraceAsString() . PHP_EOL;\n\n        $logger->log($error, 'error');\n    }\n}\n"
  },
  {
    "path": "src/XRedis.php",
    "content": "<?php\n\n/*\n * This file is part of PHP CS Fixer.\n * (c) kcloze <pei.greet@qq.com>\n * This source file is subject to the MIT license that is bundled\n * with this source code in the file LICENSE.\n */\n\nnamespace Kcloze\\MultiProcess;\n\nuse Exception;\nuse Redis;\n\nclass XRedis\n{\n    /**\n     * @var Redis\n     */\n    private $handler;\n    private $config;\n\n    /**\n     * @param $config\n     */\n    public function __construct($config)\n    {\n        $this->config = $config;\n        $this->connect();\n    }\n\n    /**\n     * 调用redis.\n     *\n     * @param $method\n     * @param $arguments\n     *\n     * @return mixed\n     */\n    public function __call($method, $arguments)\n    {\n        if (!$this->handler) {\n            $this->connect();\n        }\n\n        return call_user_func_array([$this->handler, $method], $arguments);\n    }\n\n    public function get($key, $serialize = false)\n    {\n        if (!$this->handler) {\n            $this->connect();\n        }\n        if ($serialize === false) {\n            isset($this->config['serialize']) && $serialize = $this->config['serialize'];\n        }\n\n        return $serialize ? unserialize($this->handler->get($key)) : $this->handler->get($key);\n    }\n\n    public function set($key, $value, $timeout = 0, $serialize = false)\n    {\n        if (!$this->handler) {\n            $this->connect();\n        }\n        if ($serialize === false) {\n            isset($this->config['serialize']) && $serialize = $this->config['serialize'];\n        }\n        $value = $serialize ? serialize($value) : $value;\n        if ($timeout > 0) {\n            return $this->handler->set($key, $value, $timeout);\n        }\n\n        return $this->handler->set($key, $value);\n    }\n\n    public function hget($key, $hash, $serialize = false)\n    {\n        if (!$this->handler) {\n            $this->connect();\n        }\n        if ($serialize === false) {\n            isset($this->config['serialize']) && $serialize = $this->config['serialize'];\n        }\n\n        return $serialize ? unserialize($this->handler->hget($key, $hash)) : $this->handler->hget($key, $hash);\n    }\n\n    public function hset($key, $hash, $value, $serialize = false)\n    {\n        if (!$this->handler) {\n            $this->connect();\n        }\n        if ($serialize === false) {\n            isset($this->config['serialize']) && $serialize = $this->config['serialize'];\n        }\n        $value = $serialize ? serialize($value) : $value;\n\n        return $this->handler->hset($key, $hash, $value);\n    }\n\n    /**\n     * 创建handler.\n     *\n     * @throws Exception\n     */\n    private function connect()\n    {\n        $this->handler = new Redis();\n        if (isset($this->config['keep-alive']) && $this->config['keep-alive']) {\n            $fd = $this->handler->pconnect($this->config['host'], $this->config['port'], 60);\n        } else {\n            $fd = $this->handler->connect($this->config['host'], $this->config['port']);\n        }\n        if (isset($this->config['password'])) {\n            $this->handler->auth($this->config['password']);\n        }\n        if (!$fd) {\n            throw new Exception(\"Unable to connect to redis host: {$this->config['host']},port: {$this->config['port']}\");\n        }\n        // 选择数据库0-15\n        if (isset($this->config['select']) && 0 <= $this->config['select'] && $this->config['select'] <= 15) {\n            $this->handler->select($this->config['select']);\n        }\n        //统一key前缀\n        if (isset($this->config['preKey']) && !empty($this->config['preKey'])) {\n            $this->handler->setOption(Redis::OPT_PREFIX, $this->config['preKey']);\n        }\n    }\n}\n"
  },
  {
    "path": "systemd/multiprocess.service",
    "content": "[Unit]\nDescription=Multiprocess Server\nAfter=network.target\nAfter=syslog.target\n\n[Service]\nType=forking\nPIDFile=/media/kcloze/8685937c-af42-4319-aa9b-bb123ccd18ba/data/www/kcloze/multiprocess/log/master.pid\nExecStart=/usr/bin/php7.0 /media/kcloze/8685937c-af42-4319-aa9b-bb123ccd18ba/data/www/kcloze/multiprocess/multiprocess.php start >> /media/kcloze/8685937c-af42-4319-aa9b-bb123ccd18ba/data/www/kcloze/multiprocess/log/server.log 2>&1\nExecStop=/bin/kill $MAINPID\nExecReload=/bin/kill -USR1 $MAINPID\nRestart=always\n\n[Install]\nWantedBy=multi-user.target graphical.target"
  },
  {
    "path": "test/cli/test.php",
    "content": "<?php\n\n/*\n * This file is part of PHP CS Fixer.\n * (c) kcloze <pei.greet@qq.com>\n * This source file is subject to the MIT license that is bundled\n * with this source code in the file LICENSE.\n */\n\nini_set('date.timezone', 'Asia/Shanghai');\n\necho 'test1 time: ' . date('Y-m-d H:i:s');\n\nsleep(15);\n\n$i= mt_rand(1, 5);\nvar_dump($i);\n// if ($i == 3) {\n//     NotExit();\n// }\n"
  },
  {
    "path": "test/cli/test2.php",
    "content": "<?php\n\n/*\n * This file is part of PHP CS Fixer.\n * (c) kcloze <pei.greet@qq.com>\n * This source file is subject to the MIT license that is bundled\n * with this source code in the file LICENSE.\n */\n\nini_set('date.timezone', 'Asia/Shanghai');\nsleep(10);\necho 'test2 time: ' . date('Y-m-d H:i:s');\n// while (true) {\n//     echo '123' . PHP_EOL;\n//     sleep(1);\n// }\n// sleep(10);\n\n// $i= mt_rand(1, 5);\n// var_dump($i);\n// if ($i == 3) {\n//     NotExit();\n// }\n"
  },
  {
    "path": "test/cli/test3.py",
    "content": "#coding=utf-8\n\nimport time\ndef main():\n    print \"Hello,Python！\"\n    time.sleep(5)\nmain()"
  },
  {
    "path": "test/testConfig.php",
    "content": "<?php\n\n/*\n * This file is part of PHP CS Fixer.\n * (c) kcloze <pei.greet@qq.com>\n * This source file is subject to the MIT license that is bundled\n * with this source code in the file LICENSE.\n */\n\ndefine('APP_PATH', dirname(__DIR__));\ndate_default_timezone_set('Asia/Shanghai');\n\nrequire APP_PATH . '/vendor/autoload.php';\n$config = require_once APP_PATH . '/config.php';\n\nvar_dump($config);\n\nuse Kcloze\\MultiProcess\\Config;\n\n$result=Config::hasRepeatingName($config['exec'], 'name');\nvar_dump($result);\n"
  }
]