diff --git a/README.md b/README.md index 53a14319..346b6628 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![作者QQ](https://img.shields.io/badge/作者QQ-627577391-orange.svg)]() [![zhamao License](https://img.shields.io/hexpm/l/plug.svg?maxAge=2592000)](https://github.com/zhamao-robot/zhamao-framework/blob/master/LICENSE) -[![版本](https://img.shields.io/badge/version-1.3-green.svg)]() +[![版本](https://img.shields.io/badge/version-1.4-green.svg)]() [![stupid counter](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/stupid.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=stupid) [![TODO counter](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/TODO.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=TODO) @@ -31,6 +31,7 @@ Pages托管:[https://framework.zhamao.xin/](https://framework.zhamao.xin/) ## 特点 - 支持多账号 - 灵活的注解事件绑定机制 +- 支持下断点调试(Psysh) - 易用的上下文,模块内随处可用 - 采用模块化编写,功能之间高内聚低耦合 - 常驻内存,全局缓存变量随处使用 @@ -47,11 +48,10 @@ Pages托管:[https://framework.zhamao.xin/](https://framework.zhamao.xin/) | 通用模块 | 图片上传和下载模块 | [zhamao-general-tools](https://github.com/zhamao-robot/zhamao-general-tools) | ## 计划开发内容 -- [ ] WebSocket测试脚本(客户端) +- [X] WebSocket测试脚本(客户端) - [X] Session 和中间层管理模块 -- [ ] 支持本地和远程两种方式的定时器(计划任务) - [X] 常驻服务脚本 -- [ ] 一些常用的通用 API 例如经济(用户积分、亲密度等)的模块 +- [X] 一些常用的通用 API 例如经济(用户积分、亲密度等)的模块 - [ ] 图灵机器人/腾讯AI 聊天模块 - [ ] 分词模块(可能会放弃计划,因为目前好用的分词都是其他语言的) - [ ] HTTP 过滤器、Auth 模块、完整的 MVC 兼容(可能会放弃计划,因为框架主打机器人开发) diff --git a/composer.json b/composer.json index 5aa2a07b..969fc952 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "high-performance intelligent assistant", "minimum-stability": "stable", "license": "Apache-2.0", - "version": "1.3.2", + "version": "1.4.0", "authors": [ { "name": "whale", @@ -15,6 +15,7 @@ } ], "require": { + "php": ">=7.2", "swoole/ide-helper": "@dev", "ext-mbstring": "*", "swlib/saber": "^1.0", @@ -22,7 +23,8 @@ "ext-json": "*", "ext-posix": "*", "ext-ctype": "*", - "ext-pdo": "*" + "ext-pdo": "*", + "psy/psysh": "@stable" }, "repositories": { "packagist": { diff --git a/config/global.php b/config/global.php index f95ac1aa..7fe76507 100644 --- a/config/global.php +++ b/config/global.php @@ -10,6 +10,9 @@ $config['port'] = 20001; /** 框架开到公网或外部的HTTP访问链接,通过 DataProvider::getFrameworkLink() 获取 */ $config['http_reverse_link'] = "http://127.0.0.1:".$config['port']; +/** 框架是否启动debug模式 */ +$config['debug_mode'] = false; + /** 存放框架内文件数据的目录 */ $config['zm_data'] = WORKING_DIR.'/zm_data/'; diff --git a/config/motd.txt b/config/motd.txt new file mode 100644 index 00000000..45145957 --- /dev/null +++ b/config/motd.txt @@ -0,0 +1,6 @@ + ______ +|__ / |__ __ _ _ __ ___ __ _ ___ + / /| '_ \ / _` | '_ ` _ \ / _` |/ _ \ + / /_| | | | (_| | | | | | | (_| | (_) | +/____|_| |_|\__,_|_| |_| |_|\__,_|\___/ + diff --git a/phar-starter.php b/phar-starter.php index 333ad6d0..f019dd7e 100644 --- a/phar-starter.php +++ b/phar-starter.php @@ -41,6 +41,7 @@ function loadPhp($dir) { loadPhp($path); } else { if (pathinfo($dir . '/' . $v)['extension'] == 'php') { + if(pathinfo($dir . '/' . $v)['basename'] == 'terminal_listener.php') continue; //echo 'loading '.$path.PHP_EOL; require_once $path; } diff --git a/src/Framework/Console.php b/src/Framework/Console.php index e38bd0d9..8dcf72e7 100755 --- a/src/Framework/Console.php +++ b/src/Framework/Console.php @@ -8,12 +8,19 @@ namespace Framework; -use co; +use ZM\Annotation\Swoole\SwooleEventAt; +use ZM\Connection\WSConnection; use ZM\Utils\ZMUtil; use Exception; class Console { + /** + * @var false|resource + */ + public static $console_proc = null; + public static $pipes = []; + static function setColor($string, $color = "") { switch ($color) { case "red": @@ -119,8 +126,8 @@ class Console } } - static function debug($obj) { - debug($obj); + static function debug($msg) { + if (ZMBuf::$atomics["info_level"]->get() >= 4) Console::log(date("[H:i:s] ") . "[D] " . $msg, 'gray'); } static function log($obj, $color = "") { @@ -157,11 +164,51 @@ class Console self::info("ConsoleCommand disabled."); return; } - go(function () { - while (true) { - $cmd = trim(co::fread(STDIN)); - if (self::executeCommand($cmd) === false) break; + global $terminal_id; + global $port; + $port = ZMBuf::globals("port"); + $vss = new SwooleEventAt(); + $vss->type = "open"; + $vss->level = 256; + $vss->rule = "connectType:terminal"; + $terminal_id = call_user_func(function () { + try { + $data = random_bytes(16); + } catch (Exception $e) { + return ""; } + $data[6] = chr(ord($data[6]) & 0x0f | 0x40); + $data[8] = chr(ord($data[8]) & 0x3f | 0x80); + return strtoupper(vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4))); + }); + $vss->callback = function(?WSConnection $conn) use ($terminal_id){ + $req = ctx()->getRequest(); + if($conn->getType() != "terminal") return false; + if(($req->header["x-terminal-id"] ?? "") != $terminal_id || ($req->header["x-pid"] ?? "") != posix_getpid()) { + $conn->close(); + return false; + } + return false; + }; + ZMBuf::$events[SwooleEventAt::class][] = $vss; + $vss2 = new SwooleEventAt(); + $vss2->type = "message"; + $vss2->rule = "connectType:terminal"; + $vss2->callback = function(?WSConnection $conn){ + if($conn->getType() != "terminal") return false; + $cmd = ctx()->getFrame()->data; + self::executeCommand($cmd); + return false; + }; + ZMBuf::$events[SwooleEventAt::class][] = $vss2; + go(function () { + global $terminal_id, $port; + $descriptorspec = array( + 0 => STDIN, + 1 => STDOUT, + 2 => STDERR + ); + self::$console_proc = proc_open('php -r \'$terminal_id = "'.$terminal_id.'";$port = '.$port.';require "'.__DIR__.'/terminal_listener.php";\'', $descriptorspec, $pipes); }); } @@ -209,14 +256,14 @@ class Console return false; case 'save': $origin = ZMBuf::$atomics["info_level"]->get(); - ZMBuf::$atomics["info_level"]->set(3); + //ZMBuf::$atomics["info_level"]->set(3); DataProvider::saveBuffer(); - ZMBuf::$atomics["info_level"]->set($origin); + //ZMBuf::$atomics["info_level"]->set($origin); return true; case '': return true; default: - Console::info("Command not found: " . $it[0]); + Console::info("Command not found: " . $cmd); return true; } } diff --git a/src/Framework/DataProvider.php b/src/Framework/DataProvider.php index 5b324966..2fa99c8d 100644 --- a/src/Framework/DataProvider.php +++ b/src/Framework/DataProvider.php @@ -4,6 +4,8 @@ namespace Framework; +use ZM\Annotation\Swoole\OnSave; + class DataProvider { public static $buffer_list = []; @@ -40,6 +42,13 @@ class DataProvider Console::debug("Saving " . $k . " to " . $v); self::setJsonData($v, ZMBuf::get($k)); } + foreach (ZMBuf::$events[OnSave::class] ?? [] as $v) { + $c = $v->class; + $method = $v->method; + $class = new $c(); + Console::debug("Calling @OnSave: $c -> $method"); + $class->$method(); + } if (ZMBuf::$atomics["info_level"]->get() >= 3) echo Console::setColor("saved", "blue") . PHP_EOL; } @@ -53,7 +62,7 @@ class DataProvider return json_decode(file_get_contents(self::getDataConfig() . $string), true); } - private static function setJsonData($filename, array $args) { + public static function setJsonData($filename, array $args) { $pathinfo = pathinfo($filename); if (!is_dir(self::getDataConfig() . $pathinfo["dirname"])) { Console::debug("Making Directory: " . self::getDataConfig() . $pathinfo["dirname"]); diff --git a/src/Framework/FrameworkLoader.php b/src/Framework/FrameworkLoader.php index bdc9476a..db06a085 100644 --- a/src/Framework/FrameworkLoader.php +++ b/src/Framework/FrameworkLoader.php @@ -39,7 +39,8 @@ class FrameworkLoader public function __construct($args = []) { if (self::$instance !== null) die("Cannot run two FrameworkLoader in one process!"); self::$instance = $this; - self::$argv = $args; + + $this->requireGlobalFunctions(); if (!isPharMode()) { define('WORKING_DIR', getcwd()); @@ -47,7 +48,15 @@ class FrameworkLoader echo "Phar mode: " . WORKING_DIR . PHP_EOL; } $this->registerAutoloader('classLoader'); - Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL); + self::$settings = new GlobalConfig(); + if (self::$settings->get("debug_mode") === true) { + $args[] = "--debug-mode"; + $args[] = "--disable-console-input"; + } + self::$argv = $args; + if (!in_array("--debug-mode", self::$argv)) { + Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL); + } self::$settings = new GlobalConfig(); ZMBuf::$globals = self::$settings; if (!self::$settings->success) die("Failed to load global config. Please check config/global.php file"); @@ -97,7 +106,13 @@ class FrameworkLoader "\nworking_dir: " . (isPharMode() ? realpath('.') : WORKING_DIR) ); global $motd; - echo $motd . PHP_EOL; + if (!file_exists(DataProvider::getWorkingDir() . "/config/motd.txt")) { + echo $motd; + } else { + echo file_get_contents(DataProvider::getWorkingDir() . "/config/motd.txt"); + } + if (in_array("--debug-mode", self::$argv)) + Console::warning("You are in debug mode, do not use in production!"); $this->server->start(); } catch (Exception $e) { Console::error("Framework初始化出现错误,请检查!"); @@ -126,12 +141,15 @@ class FrameworkLoader define("ZM_MATCH_FIRST", 1); define("ZM_MATCH_NUMBER", 2); define("ZM_MATCH_SECOND", 3); + define("ZM_BREAKPOINT", 'if(in_array("--debug-mode", \Framework\FrameworkLoader::$argv)) extract(\Psy\debug(get_defined_vars(), isset($this) ? $this : @get_called_class()));'); } private function selfCheck() { if (!extension_loaded("swoole")) die("Can not find swoole extension.\n"); + if (version_compare(SWOOLE_VERSION, "4.4.13") == -1) die("You must install swoole version >= 4.4.13 !"); //if (!extension_loaded("gd")) die("Can not find gd extension.\n"); if (!extension_loaded("sockets")) die("Can not find sockets extension.\n"); + if (!extension_loaded("ctype")) die("Can not find ctype extension.\n"); if (!function_exists("mb_substr")) die("Can not find mbstring extension.\n"); if (substr(PHP_VERSION, 0, 1) != "7") die("PHP >=7 required.\n"); //if (!function_exists("curl_exec")) die("Can not find curl extension.\n"); @@ -160,5 +178,6 @@ $motd = << $v) { - if (!Co::exists($c)) unset(ZMBuf::$context[$c]); + if (!Co::exists($c)) unset(ZMBuf::$context[$c], ZMBuf::$context_class[$c]); } } @@ -173,23 +175,52 @@ function set_coroutine_params($array) { * @return ContextInterface|null */ function context() { + return ctx(); +} + +/** + * @return ContextInterface|null + */ +function ctx() { $cid = Co::getCid(); $c_class = ZMBuf::globals("context_class"); if (isset(ZMBuf::$context[$cid])) { - return new $c_class($cid); + return ZMBuf::$context_class[$cid] ?? (ZMBuf::$context_class[$cid] = new $c_class($cid)); } else { Console::debug("未找到当前协程的上下文($cid),正在找父进程的上下文"); while (($pcid = Co::getPcid($cid)) !== -1) { $cid = $pcid; - if (isset(ZMBuf::$context[$cid])) return new $c_class($cid); + if (isset(ZMBuf::$context[$cid])) return ZMBuf::$context_class[$cid] ?? (ZMBuf::$context_class[$cid] = new $c_class($cid)); } return null; } } -function ctx() { return context(); } +function debug($msg) { Console::debug($msg); } -function debug($msg) { - if (ZMBuf::$atomics["info_level"]->get() >= 4) - Console::log(date("[H:i:s] ") . "[D] " . $msg, 'gray'); +function zm_sleep($s = 1) { Co::sleep($s); } + +function zm_exec($cmd): array { return System::exec($cmd); } + +function zm_cid() { return Co::getCid(); } + +function zm_yield() { Co::yield(); } + +function zm_resume(int $cid) { Co::resume($cid); } + +function zm_timer_after($ms, callable $callable) { + go(function () use ($ms, $callable) { + ZMUtil::checkWait(); + Swoole\Timer::after($ms, $callable); + }); } + +function zm_timer_tick($ms, callable $callable) { + go(function () use ($ms, $callable) { + ZMUtil::checkWait(); + Console::debug("Adding extra timer tick of " . $ms . " ms"); + Swoole\Timer::tick($ms, $callable); + }); +} + + diff --git a/src/Framework/terminal_listener.php b/src/Framework/terminal_listener.php new file mode 100644 index 00000000..a5d8bc96 --- /dev/null +++ b/src/Framework/terminal_listener.php @@ -0,0 +1,31 @@ +set(['websocket_mask' => true]); + $client->setHeaders(["x-terminal-id" => $terminal_id, 'x-pid' => posix_getppid()]); + $ret = $client->upgrade("/?type=terminal"); + if ($ret) { + while (true) { + $line = fgets(STDIN); + if ($line !== false) { + $r = $client->push(trim($line)); + if (trim($line) == "reload" || trim($line) == "r" || trim($line) == "stop") { + break; + } + if($r === false) { + echo "Unable to connect framework terminal, connection closed.\n"; + break; + } + } else { + break; + } + } + } else { + echo "Unable to connect framework terminal. port: $port\n"; + } +}); + diff --git a/src/ZM/API/CQAPI.php b/src/ZM/API/CQAPI.php index 37bb9648..b715a595 100644 --- a/src/ZM/API/CQAPI.php +++ b/src/ZM/API/CQAPI.php @@ -201,7 +201,7 @@ class CQAPI * @param CQConnection $connection * @param $reply * @param |null $function - * @return bool + * @return bool|array */ public static function processAPI($connection, $reply, $function = null) { $api_id = ZMBuf::$atomics["wait_msg_id"]->get(); @@ -243,7 +243,7 @@ class CQAPI Console::warning("CQAPI send failed, websocket push error."); $response = [ "status" => "failed", - "retcode" => 999, + "retcode" => -1000, "data" => null, "self_id" => $connection->getQQ() ]; @@ -251,7 +251,7 @@ class CQAPI if (($s["func"] ?? null) !== null) call_user_func($s["func"], $response, $reply); ZMBuf::unsetByValue("sent_api", $reply["echo"]); - if ($function === true) return null; + if ($function === true) return $response; return false; } } diff --git a/src/ZM/Annotation/AnnotationParser.php b/src/ZM/Annotation/AnnotationParser.php index f387fc89..f6e8fc52 100644 --- a/src/ZM/Annotation/AnnotationParser.php +++ b/src/ZM/Annotation/AnnotationParser.php @@ -4,7 +4,10 @@ namespace ZM\Annotation; use Doctrine\Common\Annotations\{AnnotationException, AnnotationReader}; +use Co; use Framework\{Console, ZMBuf}; +use Error; +use Exception; use ReflectionClass; use ReflectionException; use ReflectionMethod; @@ -22,15 +25,14 @@ use ZM\Annotation\Http\{After, Before, Controller, HandleException, Middleware, use Swoole\Timer; use ZM\Annotation\Interfaces\CustomAnnotation; use ZM\Annotation\Interfaces\Level; -use ZM\Annotation\Module\{Closed, InitBuffer, SaveBuffer}; -use ZM\Annotation\Swoole\{OnStart, OnTick, SwooleEventAfter, SwooleEventAt}; +use ZM\Annotation\Module\{Closed, InitBuffer, LoadBuffer, SaveBuffer}; +use ZM\Annotation\Swoole\{OnSave, OnStart, OnTick, SwooleEventAfter, SwooleEventAt}; use ZM\Annotation\Interfaces\Rule; use ZM\Connection\WSConnection; use ZM\Event\EventHandler; use ZM\Http\MiddlewareInterface; use Framework\DataProvider; use ZM\Utils\ZMUtil; -use function foo\func; class AnnotationParser { @@ -64,6 +66,9 @@ class AnnotationParser } elseif ($vs instanceof SaveBuffer) { Console::debug("注册自动保存的缓存变量: " . $vs->buf_name . " (Dir:" . $vs->sub_folder . ")"); DataProvider::addSaveBuffer($vs->buf_name, $vs->sub_folder); + } elseif ($vs instanceof LoadBuffer) { + Console::debug("注册到内存的缓存变量: " . $vs->buf_name . " (Dir:" . $vs->sub_folder . ")"); + ZMBuf::set($vs->buf_name, DataProvider::getJsonData(($vs->sub_folder ?? "") . "/" . $vs->buf_name . ".json")); } elseif ($vs instanceof InitBuffer) { ZMBuf::set($vs->buf_name, []); } elseif ($vs instanceof MiddlewareClass) { @@ -119,6 +124,7 @@ class AnnotationParser elseif ($vss instanceof CQBefore) ZMBuf::$events[CQBefore::class][$vss->cq_event][] = $vss; elseif ($vss instanceof CQAfter) ZMBuf::$events[CQAfter::class][$vss->cq_event][] = $vss; elseif ($vss instanceof OnStart) ZMBuf::$events[OnStart::class][] = $vss; + elseif ($vss instanceof OnSave) ZMBuf::$events[OnSave::class][] = $vss; elseif ($vss instanceof Middleware) ZMBuf::$events[MiddlewareInterface::class][$vss->class][$vss->method][] = $vss->middleware; elseif ($vss instanceof OnTick) self::addTimerTick($vss); elseif ($vss instanceof CQAPISend) ZMBuf::$events[CQAPISend::class][] = $vss; @@ -144,13 +150,14 @@ class AnnotationParser } } } + Console::debug("解析注解完毕!"); if (ZMBuf::isset("timer_count")) { Console::info("Added " . ZMBuf::get("timer_count") . " timer(s)!"); ZMBuf::unsetCache("timer_count"); } } - private static function getRuleCallback($rule_str) { + public static function getRuleCallback($rule_str) { $func = null; $rule = $rule_str; if ($rule != "") { @@ -228,14 +235,14 @@ class AnnotationParser return $func; } - private static function registerRuleEvent(?AnnotationBase $vss, ReflectionMethod $method, ReflectionClass $class) { + public static function registerRuleEvent(?AnnotationBase $vss, ReflectionMethod $method, ReflectionClass $class) { $vss->callback = self::getRuleCallback($vss->getRule()); $vss->method = $method->getName(); $vss->class = $class->getName(); return $vss; } - private static function registerMethod(?AnnotationBase $vss, ReflectionMethod $method, ReflectionClass $class) { + public static function registerMethod(?AnnotationBase $vss, ReflectionMethod $method, ReflectionClass $class) { $vss->method = $method->getName(); $vss->class = $class->getName(); return $vss; @@ -336,10 +343,9 @@ class AnnotationParser ZMBuf::set("timer_count", ZMBuf::get("timer_count", 0) + 1); $class = ZMUtil::getModInstance($vss->class); $method = $vss->method; - $has_middleware = false; $ms = $vss->tick_ms; $cid = go(function () use ($class, $method, $ms) { - \Co::suspend(); + Co::suspend(); $plain_class = get_class($class); if (!isset(ZMBuf::$events[MiddlewareInterface::class][$plain_class][$method])) { Console::debug("Added timer: " . $plain_class . " -> " . $method); @@ -347,9 +353,9 @@ class AnnotationParser set_coroutine_params([]); try { $class->$method(); - } catch (\Exception $e) { + } catch (Exception $e) { Console::error("Uncaught error from TimerTick: " . $e->getMessage() . " at " . $e->getFile() . "({$e->getLine()})"); - } catch (\Error $e) { + } catch (Error $e) { Console::error("Uncaught fatal error from TimerTick: " . $e->getMessage()); echo Console::setColor($e->getTraceAsString(), "gray"); Console::error("Please check your code!"); @@ -361,9 +367,9 @@ class AnnotationParser set_coroutine_params([]); try { EventHandler::callWithMiddleware($class, $method, [], []); - } catch (\Exception $e) { + } catch (Exception $e) { Console::error("Uncaught error from TimerTick: " . $e->getMessage() . " at " . $e->getFile() . "({$e->getLine()})"); - } catch (\Error $e) { + } catch (Error $e) { Console::error("Uncaught fatal error from TimerTick: " . $e->getMessage()); echo Console::setColor($e->getTraceAsString(), "gray"); Console::error("Please check your code!"); diff --git a/src/ZM/Annotation/CQ/CQAPISend.php b/src/ZM/Annotation/CQ/CQAPISend.php index 6b29602d..bf4429c0 100644 --- a/src/ZM/Annotation/CQ/CQAPISend.php +++ b/src/ZM/Annotation/CQ/CQAPISend.php @@ -20,6 +20,11 @@ class CQAPISend extends AnnotationBase implements Level */ public $action = ""; + /** + * @var bool + */ + public $with_result = false; + public $level = 20; /** diff --git a/src/ZM/Annotation/Module/LoadBuffer.php b/src/ZM/Annotation/Module/LoadBuffer.php new file mode 100644 index 00000000..0ad9c299 --- /dev/null +++ b/src/ZM/Annotation/Module/LoadBuffer.php @@ -0,0 +1,24 @@ +cid]["response"] ?? null; } /** @return WSConnection */ - public function getConnection() { return ConnectionManager::get($this->getFrame()->fd); } + public function getConnection() { return ConnectionManager::get($this->getFd()); } /** * @return int|null @@ -95,6 +95,8 @@ class Context implements ContextInterface public function setCache($key, $value) { ZMBuf::$context[$this->cid]["cache"][$key] = $value; } + public function getCQResponse() { return ZMBuf::$context[$this->cid]["cq_response"] ?? null; } + /** * only can used by cq->message event function * @param $msg @@ -106,6 +108,7 @@ class Context implements ContextInterface case "group": case "private": case "discuss": + $this->setCache("has_reply", true); return CQAPI::quick_reply(ConnectionManager::get($this->getFrame()->fd), $this->getData(), $msg, $yield); } return false; diff --git a/src/ZM/Context/ContextInterface.php b/src/ZM/Context/ContextInterface.php index b6df09c6..d736331d 100644 --- a/src/ZM/Context/ContextInterface.php +++ b/src/ZM/Context/ContextInterface.php @@ -72,6 +72,8 @@ interface ContextInterface public function setMessageType($type); + public function getCQResponse(); + /** * @param $msg * @param bool $yield diff --git a/src/ZM/DB/DB.php b/src/ZM/DB/DB.php index f239e0fb..6084e9de 100644 --- a/src/ZM/DB/DB.php +++ b/src/ZM/DB/DB.php @@ -9,6 +9,7 @@ use framework\Console; use framework\ZMBuf; use PDOStatement; use Swoole\Coroutine; +use Swoole\Database\PDOStatementProxy; use ZM\Exception\DbException; class DB @@ -66,9 +67,10 @@ class DB try { $conn = ZMBuf::$sql_pool->get(); if ($conn === false) { + ZMBuf::$sql_pool->put(null); throw new DbException("无法连接SQL!" . $line); } - $result = $conn->query($line) === false ? false : ($conn->errno != 0 ? false : true); + $result = $conn->query($line) === false ? false : true; ZMBuf::$sql_pool->put($conn); return $result; } catch (DBException $e) { @@ -98,25 +100,29 @@ class DB try { $conn = ZMBuf::$sql_pool->get(); if ($conn === false) { + ZMBuf::$sql_pool->put(null); throw new DbException("无法连接SQL!" . $line); } $ps = $conn->prepare($line); if ($ps === false) { - ZMBuf::$sql_pool->connect_cnt -= 1; + ZMBuf::$sql_pool->put(null); throw new DbException("SQL语句查询错误," . $line . ",错误信息:" . $conn->error); } else { - if (!($ps instanceof PDOStatement)) { - throw new DbException("语句查询错误!" . $line); + if (!($ps instanceof PDOStatement) && !($ps instanceof PDOStatementProxy)) { + var_dump($ps); + ZMBuf::$sql_pool->put(null); + throw new DbException("语句查询错误!返回的不是 PDOStatement" . $line); } if ($params == []) $result = $ps->execute(); elseif (!is_array($params)) { $result = $ps->execute([$params]); } else $result = $ps->execute($params); - ZMBuf::$sql_pool->put($conn); if ($result !== true) { + ZMBuf::$sql_pool->put(null); throw new DBException("语句[$line]错误!" . $ps->errorInfo()[2]); //echo json_encode(debug_backtrace(), 128 | 256); } + ZMBuf::$sql_pool->put($conn); if (ZMBuf::get("sql_log") === true) { $log = "[" . date("Y-m-d H:i:s") . @@ -134,8 +140,12 @@ class DB "] " . $line . " " . json_encode($params, JSON_UNESCAPED_UNICODE) . " (Error:" . $e->getMessage() . ")\n"; Coroutine::writeFile(CRASH_DIR . "sql.log", $log, FILE_APPEND); } + if(mb_strpos($e->getMessage(), "has gone away") !== false) { + zm_sleep(0.2); + Console::warning("Gone away of MySQL! retrying!"); + return self::rawQuery($line, $params); + } Console::warning($e->getMessage()); - throw $e; } } diff --git a/src/ZM/DB/WhereBody.php b/src/ZM/DB/WhereBody.php index eb2b651f..ec5ca4de 100644 --- a/src/ZM/DB/WhereBody.php +++ b/src/ZM/DB/WhereBody.php @@ -28,6 +28,7 @@ trait WhereBody $param []=$vs; } } + if ($msg == '') $msg = 1; return [$msg, $param]; } -} \ No newline at end of file +} diff --git a/src/ZM/Event/CQ/MessageEvent.php b/src/ZM/Event/CQ/MessageEvent.php index 9fce23e6..0d6603c9 100644 --- a/src/ZM/Event/CQ/MessageEvent.php +++ b/src/ZM/Event/CQ/MessageEvent.php @@ -38,18 +38,19 @@ class MessageEvent * @throws AnnotationException */ public function onBefore() { - foreach (ZMBuf::$events[CQBefore::class]["message"] ?? [] as $v) { - $c = $v->class; + $obj_list = ZMBuf::$events[CQBefore::class]["message"]; + foreach ($obj_list as $v) { + if($v->level < 200) break; EventHandler::callWithMiddleware( - $c, + $v->class, $v->method, ["data" => context()->getData(), "connection" => $this->connection], [], function ($r) { - if(!$r) context()->setCache("block_continue", true); + if (!$r) context()->setCache("block_continue", true); } ); - if(context()->getCache("block_continue") === true) return false; + if (context()->getCache("block_continue") === true) return false; } foreach (ZMBuf::get("wait_api", []) as $k => $v) { if (context()->getData()["user_id"] == $v["user_id"] && @@ -63,6 +64,21 @@ class MessageEvent return false; } } + foreach (ZMBuf::$events[CQBefore::class]["message"] ?? [] as $v) { + if($v->level >= 200) continue; + $c = $v->class; + if (ctx()->getCache("level") != 0) continue; + EventHandler::callWithMiddleware( + $c, + $v->method, + ["data" => context()->getData(), "connection" => $this->connection], + [], + function ($r) { + if (!$r) context()->setCache("block_continue", true); + } + ); + if (context()->getCache("block_continue") === true) return false; + } return true; } @@ -91,7 +107,7 @@ class MessageEvent "data" => context()->getData(), "connection" => context()->getConnection() ]; - if(!isset($obj[$c])) { + if (!isset($obj[$c])) { $obj[$c] = new $c($class_construct); } if ($word[0] != "" && $v->match == $word[0]) { @@ -102,7 +118,8 @@ class MessageEvent }); return; } elseif ($v->regexMatch != "" && ($args = matchArgs($v->regexMatch, context()->getMessage())) !== false) { - $this->function_call = EventHandler::callWithMiddleware($obj[$c], $v->method, $class_construct, [$args], function ($r){ + Console::debug("Calling $c -> {$v->method}"); + $this->function_call = EventHandler::callWithMiddleware($obj[$c], $v->method, $class_construct, [$args], function ($r) { if (is_string($r)) context()->reply($r); return true; }); @@ -120,13 +137,14 @@ class MessageEvent ($v->message_type == '' || ($v->message_type != '' && $v->message_type == context()->getData()["message_type"])) && ($v->raw_message == '' || ($v->raw_message != '' && $v->raw_message == context()->getData()["raw_message"]))) { $c = $v->class; + Console::debug("Calling CQMessage: $c -> {$v->method}"); if (!isset($obj[$c])) $obj[$c] = new $c([ "data" => context()->getData(), "connection" => $this->connection ], ModHandleType::CQ_MESSAGE); - EventHandler::callWithMiddleware($obj[$c], $v->method, [], [context()->getData()["message"]], function($r) { - if(is_string($r)) context()->reply($r); + EventHandler::callWithMiddleware($obj[$c], $v->method, [], [context()->getData()["message"]], function ($r) { + if (is_string($r)) context()->reply($r); }); if (context()->getCache("block_continue") === true) return; } @@ -150,10 +168,10 @@ class MessageEvent ["data" => context()->getData(), "connection" => $this->connection], [], function ($r) { - if(!$r) context()->setCache("block_continue", true); + if (!$r) context()->setCache("block_continue", true); } ); - if(context()->getCache("block_continue") === true) return false; + if (context()->getCache("block_continue") === true) return false; } return true; } diff --git a/src/ZM/Event/EventHandler.php b/src/ZM/Event/EventHandler.php index f41efc6a..b6d33656 100644 --- a/src/ZM/Event/EventHandler.php +++ b/src/ZM/Event/EventHandler.php @@ -11,6 +11,7 @@ use Exception; use Framework\Console; use Framework\ZMBuf; use ZM\Event\Swoole\{MessageEvent, RequestEvent, WorkerStartEvent, WSCloseEvent, WSOpenEvent}; +use Swoole\Http\Request; use Swoole\Server; use Swoole\WebSocket\Frame; use ZM\Annotation\CQ\CQAPIResponse; @@ -46,8 +47,9 @@ class EventHandler DataProvider::saveBuffer(); ZMBuf::$server->shutdown(); }); - (new WorkerStartEvent($param0, $param1))->onActivate()->onAfter(); + $r = (new WorkerStartEvent($param0, $param1))->onActivate(); Console::log("\n=== Worker #" . $param0->worker_id . " 已启动 ===\n", "gold"); + $r->onAfter(); self::startTick(); } catch (Exception $e) { Console::error("Worker加载出错!停止服务!"); @@ -69,6 +71,7 @@ class EventHandler } catch (Error $e) { $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; Console::error("Fatal error when calling $event_name: " . $error_msg); + Console::stackTrace(); } break; case "request": @@ -99,12 +102,14 @@ class EventHandler } break; case "open": - set_coroutine_params(["server" => $param0, "request" => $param1]); + /** @var Request $param1 */ + set_coroutine_params(["server" => $param0, "request" => $param1, "fd" => $param1->fd]); try { (new WSOpenEvent($param0, $param1))->onActivate()->onAfter(); } catch (Error $e) { $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; Console::error("Fatal error when calling $event_name: " . $error_msg); + Console::stackTrace(); } break; case "close": @@ -114,6 +119,7 @@ class EventHandler } catch (Error $e) { $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; Console::error("Fatal error when calling $event_name: " . $error_msg); + Console::stackTrace(); } break; } @@ -128,6 +134,7 @@ class EventHandler * @throws AnnotationException */ public static function callCQEvent($event_data, $conn_or_response, int $level = 0) { + ctx()->setCache("level",$level); if ($level >= 5) { Console::warning("Recursive call reached " . $level . " times"); Console::stackTrace(); @@ -160,8 +167,12 @@ class EventHandler return false; } + /** + * @param $req + * @throws AnnotationException + */ public static function callCQResponse($req) { - //Console::info("收到来自API连接的回复:".json_encode($req, 128|256)); + Console::debug("收到来自API连接的回复:".json_encode($req, 128|256)); $status = $req["status"]; $retcode = $req["retcode"]; $data = $req["data"]; @@ -172,13 +183,26 @@ class EventHandler "status" => $status, "retcode" => $retcode, "data" => $data, - "self_id" => $self_id + "self_id" => $self_id, + "echo" => $req["echo"] ]; + set_coroutine_params(["cq_response" => $response]); if (isset(ZMBuf::$events[CQAPIResponse::class][$req["retcode"]])) { list($c, $method) = ZMBuf::$events[CQAPIResponse::class][$req["retcode"]]; $class = new $c(["data" => $origin["data"]]); call_user_func_array([$class, $method], [$origin["data"], $req]); } + $origin_ctx = ctx()->copy(); + ctx()->setCache("action", $origin["data"]["action"] ?? "unknown"); + ctx()->setData($origin["data"]); + foreach (ZMBuf::$events[CQAPISend::class] ?? [] as $k => $v) { + if (($v->action == "" || $v->action == ctx()->getCache("action")) && $v->with_result) { + $c = $v->class; + self::callWithMiddleware($c, $v->method, context()->copy(), [ctx()->getCache("action"), $origin["data"]["params"] ?? [], ctx()->getRobotId()]); + if (context()->getCache("block_continue") === true) break; + } + } + set_coroutine_params($origin_ctx); if (($origin["func"] ?? null) !== null) { call_user_func($origin["func"], $response, $origin["data"]); } elseif (($origin["coroutine"] ?? false) !== false) { @@ -204,7 +228,7 @@ class EventHandler context()->setCache("action", $action); context()->setCache("reply", $reply); foreach (ZMBuf::$events[CQAPISend::class] ?? [] as $k => $v) { - if ($v->action == "" || $v->action == $action) { + if (($v->action == "" || $v->action == $action) && !$v->with_result) { $c = $v->class; self::callWithMiddleware($c, $v->method, context()->copy(), [$reply["action"], $reply["params"] ?? [], $connection->getQQ()]); if (context()->getCache("block_continue") === true) break; @@ -286,5 +310,6 @@ class EventHandler foreach (ZMBuf::get("paused_tick", []) as $cid) { Co::resume($cid); } + } } diff --git a/src/ZM/Event/Swoole/MessageEvent.php b/src/ZM/Event/Swoole/MessageEvent.php index abbb8d50..0c6f9298 100644 --- a/src/ZM/Event/Swoole/MessageEvent.php +++ b/src/ZM/Event/Swoole/MessageEvent.php @@ -48,8 +48,10 @@ class MessageEvent implements SwooleEvent ctx()->setCache("level", 0); Console::debug("Calling CQ Event from fd=" . $conn->fd); EventHandler::callCQEvent($data, ConnectionManager::get(context()->getFrame()->fd), 0); - } else + } else{ + set_coroutine_params(["connection" => $conn]); EventHandler::callCQResponse($data); + } } foreach (ZMBuf::$events[SwooleEventAt::class] ?? [] as $v) { if (strtolower($v->type) == "message" && $this->parseSwooleRule($v)) { diff --git a/src/ZM/Event/Swoole/WSOpenEvent.php b/src/ZM/Event/Swoole/WSOpenEvent.php index 5038767b..833749e1 100644 --- a/src/ZM/Event/Swoole/WSOpenEvent.php +++ b/src/ZM/Event/Swoole/WSOpenEvent.php @@ -6,6 +6,7 @@ namespace ZM\Event\Swoole; use Closure; use Doctrine\Common\Annotations\AnnotationException; +use Framework\Console; use Framework\ZMBuf; use Swoole\Http\Request; use Swoole\WebSocket\Server; @@ -51,9 +52,16 @@ class WSOpenEvent implements SwooleEvent if ($type_conn == CQConnection::class) { $qq = $this->request->get["qq"] ?? $this->request->header["x-self-id"] ?? ""; $self_token = ZMBuf::globals("access_token") ?? ""; - $remote_token = $this->request->get["token"] ?? (isset($header["authorization"]) ? explode(" ", $this->request->header["authorization"])[1] : ""); + if(isset($this->request->header["authorization"])) { + Console::debug($this->request->header["authorization"]); + } + $remote_token = $this->request->get["token"] ?? (isset($this->request->header["authorization"]) ? explode(" ", $this->request->header["authorization"])[1] : ""); if ($qq != "" && ($self_token == $remote_token)) $this->conn = new CQConnection($this->server, $this->request->fd, $qq); - else $this->conn = new UnknownConnection($this->server, $this->request->fd); + else { + $this->conn = new UnknownConnection($this->server, $this->request->fd); + Console::warning("connection of CQ has invalid QQ or token!"); + Console::debug("Remote token: ".$remote_token); + } } else { $this->conn = new $type_conn($this->server, $this->request->fd); } diff --git a/src/ZM/Event/Swoole/WorkerStartEvent.php b/src/ZM/Event/Swoole/WorkerStartEvent.php index b8d02c7c..24c7f519 100644 --- a/src/ZM/Event/Swoole/WorkerStartEvent.php +++ b/src/ZM/Event/Swoole/WorkerStartEvent.php @@ -9,6 +9,8 @@ use Doctrine\Common\Annotations\AnnotationException; use Exception; use ReflectionException; use Swoole\Coroutine; +use Swoole\Database\PDOConfig; +use Swoole\Database\PDOPool; use Swoole\Process; use Swoole\Timer; use ZM\Annotation\AnnotationBase; @@ -25,7 +27,6 @@ use Swoole\Server; use ZM\Event\EventHandler; use ZM\Exception\DbException; use Framework\DataProvider; -use ZM\Utils\SQLPool; use ZM\Utils\ZMUtil; class WorkerStartEvent implements SwooleEvent @@ -49,6 +50,9 @@ class WorkerStartEvent implements SwooleEvent */ public function onActivate(): WorkerStartEvent { Console::info("Worker启动中"); + ZMBuf::$server = $this->server; + Console::listenConsole(); //这个方法只能在这里调用,且如果worker_num不为1的话,此功能不可用 + Process::signal(SIGINT, function () { Console::warning("Server interrupted by keyboard."); ZMUtil::stop(true); @@ -76,14 +80,35 @@ class WorkerStartEvent implements SwooleEvent } if (ZMBuf::globals("sql_config")["sql_host"] != "") { Console::info("新建SQL连接池中"); - ZMBuf::$sql_pool = new SQLPool(); + ob_start(); + phpinfo(); + $str = ob_get_clean(); + $str = explode("\n", $str); + foreach($str as $k => $v) { + $v = trim($v); + if($v == "") continue; + if(mb_strpos($v, "API Extensions") === false) continue; + if(mb_strpos($v, "pdo_mysql") === false) { + throw new DbException("未安装 mysqlnd php-mysql扩展。"); + } + } + $sql = ZMBuf::globals("sql_config"); + ZMBuf::$sql_pool = new PDOPool((new PDOConfig()) + ->withHost($sql["sql_host"]) + ->withPort($sql["sql_port"]) + // ->withUnixSocket('/tmp/mysql.sock') + ->withDbName($sql["sql_database"]) + ->withCharset('utf8mb4') + ->withUsername($sql["sql_username"]) + ->withPassword($sql["sql_password"]) + ); DB::initTableList(); } - ZMBuf::$server = $this->server; + ZMBuf::$atomics['reload_time']->add(1); Console::info("监听console输入"); - Console::listenConsole(); //这个方法只能在这里调用,且如果worker_num不为1的话,此功能不可用 + $this->setAutosaveTimer(ZMBuf::globals("auto_save_interval")); $this->loadAllClass(); //加载composer资源、phar外置包、注解解析注册等 return $this; @@ -99,18 +124,22 @@ class WorkerStartEvent implements SwooleEvent } ZMBuf::unsetCache("wait_start"); set_coroutine_params(["server" => $this->server, "worker_id" => $this->worker_id]); + foreach (ZMBuf::$events[OnStart::class] ?? [] as $v) { $class_name = $v->class; + Console::debug("正在调用启动时函数: " . $class_name . " -> " . $v->method); EventHandler::callWithMiddleware($class_name, $v->method, ["server" => $this->server, "worker_id" => $this->worker_id], []); } foreach (ZMBuf::$events[SwooleEventAfter::class] ?? [] as $v) { /** @var AnnotationBase $v */ if (strtolower($v->type) == "workerstart") { $class_name = $v->class; + Console::debug("正在调用启动时函数after: " . $class_name . " -> " . $v->method); EventHandler::callWithMiddleware($class_name, $v->method, ["server" => $this->server, "worker_id" => $this->worker_id], []); if (context()->getCache("block_continue") === true) break; } } + Console::debug("调用完毕!"); return $this; } @@ -118,8 +147,8 @@ class WorkerStartEvent implements SwooleEvent foreach ($this->server->connections as $v) { $this->server->close($v); } - if (ZMBuf::$sql_pool instanceof SqlPool) { - ZMBuf::$sql_pool->destruct(); + if (ZMBuf::$sql_pool !== null) { + ZMBuf::$sql_pool->close(); ZMBuf::$sql_pool = null; } } @@ -147,12 +176,11 @@ class WorkerStartEvent implements SwooleEvent } //加载composer类 + Console::info("加载composer资源中"); if (file_exists(DataProvider::getWorkingDir() . "/vendor/autoload.php")) { - Console::info("加载composer资源中"); require_once DataProvider::getWorkingDir() . "/vendor/autoload.php"; - } else { - if (isPharMode()) require_once WORKING_DIR . "/vendor/autoload.php"; } + if (isPharMode()) require_once WORKING_DIR . "/vendor/autoload.php"; //加载各个模块的注解类,以及反射 Console::info("检索Module中"); @@ -162,6 +190,7 @@ class WorkerStartEvent implements SwooleEvent ConnectionManager::registerCustomClass(); //加载自定义的全局函数 + Console::debug("加载自定义的全局函数中"); if (file_exists(DataProvider::getWorkingDir() . "/src/Custom/global_function.php")) require_once DataProvider::getWorkingDir() . "/src/Custom/global_function.php"; $this->afterCheck(); @@ -169,7 +198,7 @@ class WorkerStartEvent implements SwooleEvent private function setAutosaveTimer($globals) { DataProvider::$buffer_list = []; - Timer::tick($globals * 1000, function () { + zm_timer_tick($globals * 1000, function () { DataProvider::saveBuffer(); }); } diff --git a/src/ZM/Http/StaticFileHandler.php b/src/ZM/Http/StaticFileHandler.php new file mode 100644 index 00000000..fec943bc --- /dev/null +++ b/src/ZM/Http/StaticFileHandler.php @@ -0,0 +1,35 @@ +getResponse(); + Console::debug("Full path: ".$full_path); + if ($full_path !== false) { + if (strpos($full_path, $path) !== 0) { + $response->status(403); + $response->end("403 Forbidden"); + return true; + } else { + if(is_file($full_path)) { + $exp = strtolower(pathinfo($full_path)['extension'] ?? "unknown"); + $response->setHeader("Content-Type", ZMBuf::config("file_header")[$exp] ?? "application/octet-stream"); + $response->end(file_get_contents($full_path)); + return true; + } + } + } + $response->status(404); + $response->end(ZMUtil::getHttpCodePage(404)); + return true; + } +} diff --git a/src/ZM/Utils/SQLPool.php b/src/ZM/Utils/SQLPool.php index d813b6f9..9fb66280 100644 --- a/src/ZM/Utils/SQLPool.php +++ b/src/ZM/Utils/SQLPool.php @@ -14,7 +14,6 @@ use PDO; use PDOException; use SplQueue; use Swoole\Coroutine; -use Swoole\Coroutine\Mysql; class SQLPool { @@ -34,6 +33,26 @@ class SQLPool "password" => ZMBuf::globals("sql_config")["sql_password"], "database" => ZMBuf::globals("sql_config")["sql_database"] ]; + Console::debug("新建检测 MySQL 连接的计时器"); + zm_timer_tick(10000, function () { + //Console::debug("正在检测是否有坏死的MySQL连接,当前连接池有 ".count($this->pool) . " 个连接"); + if (count($this->pool) > 0) { + /** @var PDO $cnn */ + $cnn = $this->pool->pop(); + $this->connect_cnt -= 1; + try { + $cnn->getAttribute(PDO::ATTR_SERVER_INFO); + } catch (PDOException $e) { + if (strpos($e->getMessage(), 'MySQL server has gone away') !== false) { + Console::info("MySQL 长连接丢失,取消连接"); + unset($cnn); + return; + } + } + $this->pool->push($cnn); + $this->connect_cnt += 1; + } + }); } /** @@ -90,8 +109,7 @@ class SQLPool private function newConnect() { //无空闲连接,创建新连接 - $mysql = new Mysql(); - $dsn = "mysql:host=" . $this->info["host"] . ";dbname=" . $this->info["database"]; + $dsn = "mysql:host=" . $this->info["host"] . ";dbname=" . $this->info["database"] . ";charset=utf8"; try { $mysql = new PDO($dsn, $this->info["user"], $this->info["password"], array(PDO::ATTR_PERSISTENT => true)); } catch (PDOException $e) {