diff --git a/composer.json b/composer.json index 00999499..3a4b9d6a 100644 --- a/composer.json +++ b/composer.json @@ -35,8 +35,9 @@ "zhamao/connection-manager": "^1.0" }, "suggest": { - "ext-ctype": "*", - "ext-mbstring": "*" + "ext-ctype": "Use C/C++ extension instead of polyfill will be more efficient", + "ext-mbstring": "Use C/C++ extension instead of polyfill will be more efficient", + "league/climate": "Display columns and status in terminal" }, "autoload": { "psr-4": { diff --git a/config/global.php b/config/global.php index 24bedb09..0441f463 100644 --- a/config/global.php +++ b/config/global.php @@ -29,6 +29,7 @@ $config['swoole'] = [ //'worker_num' => swoole_cpu_num(), //如果你只有一个 OneBot 实例连接到框架并且代码没有复杂的CPU密集计算,则可把这里改为1使用全局变量 'dispatch_mode' => 2, //包分配原则,见 https://wiki.swoole.com/#/server/setting?id=dispatch_mode 'max_coroutine' => 300000, + 'max_wait_time' => 5 //'task_worker_num' => 4, //'task_enable_coroutine' => true ]; diff --git a/docs/FAQ.md b/docs/FAQ.md index a67fdd0e..00cc7066 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -20,4 +20,35 @@ ps aux | grep vendor/bin/start | grep -v grep | awk '{print $2}' | xargs kill -9 # 然后使用下面的这条命令,假设最小的pid是23643 kill -INT 23643 # 如果使用 ps aux 看不到框架相关进程,证明关闭成功,否则需要使用第一条强行杀死 -``` \ No newline at end of file +``` + +## 出现 deadlock 字样 + +一般情况下,如果误操作框架可能会报如下图的错误: + +``` +=================================================================== + [FATAL ERROR]: all coroutines (count: 1) are asleep - deadlock! +=================================================================== + + [Coroutine-1] +-------------------------------------------------------------------- +#0 Swoole\Coroutine\System::sleep() called at [/Users/jerry/project/git-project/zhamao-framework/src/ZM/global_functions.php:232] +#1 zm_sleep() called at [/Users/jerry/project/git-project/zhamao-framework/src/Module/Example/Hello.php:38] +#2 Module\Example\Hello->onStart() called at [/Users/jerry/project/git-project/zhamao-framework/src/ZM/Event/EventDispatcher.php:205] +#3 ZM\Event\EventDispatcher->dispatchEvent() called at [/Users/jerry/project/git-project/zhamao-framework/src/ZM/Event/EventDispatcher.php:89] +#4 ZM\Event\EventDispatcher->dispatchEvents() called at [/Users/jerry/project/git-project/zhamao-framework/src/ZM/Event/SwooleEvent/OnWorkerStart.php:130] +#5 ZM\Event\SwooleEvent\OnWorkerStart->onCall() called at [/Users/jerry/project/git-project/zhamao-framework/src/ZM/Framework.php:336] +``` + +这种错误的出现原因一般是因为协程未结束而 Worker 进程提前退出导致的,这个错误也可手动造成(在任意 Worker 进程内的位置使用 `zm_yield()` 且不使用 `zm_resume()` 恢复,期间使用 reload 或 stop 重启或停止框架就会报错)。 + +还有一种情况是数据库、文件读取或下载上传还没有传送结束,时间已经超时,在关闭或重启框架时不得不强行切断协程的运行。这种情况建议根据下方的打印输出栈进行插错,建议将协程运行时间长的过程缩短或调长 `swoole` 配置项下面的 `max_wait_time` 时间(秒),2.4.3 版本起此参数默认为 5 秒。 + +## 使用 LightCache 关闭时无法正常保存持久化 + +LightCache 因为是跨内存使用的,所以每次重启和关闭框架时,都只会让其中一个进程去保存。因为在 2.4.2 版本开始,持久化的逻辑发生了更改,不再支持 `expire = -2` 进行设置持久化(因为那样会很容易让开发者写错),仅支持使用 `LightCache::addPersistence($key)` 这样的方式进行设置持久化,所以在 2.4.2 版本以后,请使用此方法进行持久化设置,保证数据不丢失。 + +此外,2.4.2 版本起,不再支持用户手动调用 `savePersistence()` 方法,普通用户不可手动调用此方法,否则会导致数据出错。 + +## \ No newline at end of file diff --git a/docs/component/light-cache.md b/docs/component/light-cache.md index 55da74ac..b3aa97a9 100644 --- a/docs/component/light-cache.md +++ b/docs/component/light-cache.md @@ -57,7 +57,9 @@ $config['light_cache'] = [ `$value` 可存入 `bool`、`string`、`int`、`array` 等可被 `json_encode()` 的变量,闭包函数和对象不可存入。 -`$expire` 是 `int`,超时时间(秒)。如果设定了大于 0 的值,则表明是在 `$expire` 秒后自动删除。如果为 -1 则什么都不做,如果框架使用了 `stop` 或 Ctrl+C 或意外退出时数据会丢失。如果为 -2,则会将此数据持久化保存,保存在上方配置文件指定的 json 文件中,待关闭后再次启动框架会自动加载回来,不会丢失。 +`$expire` 是 `int`,超时时间(秒)。如果设定了大于 0 的值,则表明是在 `$expire` 秒后自动删除(框架中途停止不受影响)。如果为 -1 则什么都不做。框架停止后自动被清除。 + +**注意:如果前面使用了 set() ,后面再次使用 set() 会重置 expire 过期时间为 -1(-1 是框架运行时不过期,关闭框架删除的状态),如果只需要更新值,请使用 update()。** ```php // use ZM\Store\LightCache; @@ -88,6 +90,14 @@ public function storeAfterRemove() { ( 内容不存在! +### LightCache::update() + +更新值而不更新状态。如果键值对不存在,则返回 false。 + +定义:`LightCache::update(string $key, $value)` + +参数同 `set()`,可参考。 + ### LightCache::get() 获取内容。 @@ -106,6 +116,20 @@ zm_sleep(10); dump(LightCache::getExpire("test")); // 返回 10 ``` +### LightCache::getExpireTS() + +获取存储项要过期的时间戳。 + +定义:`LightCache::getExpireTS(string $key)` + +```php +$s = LightCache::set("test", "hello", 20); //假设这条代码执行时时间戳是 1616838482 +zm_sleep(10); +dump(LightCache::getExpire("test")); // 返回 1616838502 +zm_sleep(10); +dump(LightCache::getExpire("test")); // 返回 null +``` + ### LightCache::getMemoryUsage() 获取轻量缓存使用的总空间大小(字节) @@ -157,25 +181,34 @@ dump(LightCache::getAll()); */ ``` -### LightCache::savePersistence() +### LightCache::addPersistence() -立刻保存所有被标记为持久化的缓存项到磁盘。 +添加持久化存储的键。 -!!! note "提示" +用法:`LightCache::addPersistence($key)`。 - 在一般情况下,框架定时执行此方法来保存,在停止框架、reload 框架和 Ctrl+C 停止框架的时候,均会执行保存。 +注:只需调用一次即可,无需多次重复调用,也不需要设置 expire 为 -2 了。(2.4.2 起可用此方法)。 + +详见下方 **持久化**。 + +### LightCache::removePersistence() + +删除持久化的键。 + +用法:`LightCache::removePersistence($key)`。 + +注:只需调用一次即可,无需多次重复调用,也不需要设置 expire 为非 -2 了。(2.4.2 起可用此方法)。 ### 持久化 -将 `set()` 的 expire 设置为 -2 即可。 +使用 `LightCache::addPersistence($key)` 添加对应需要持久化的键名即可。 ```php /** - * @CQCommand("store") + * @OnStart() */ -public function store() { - LightCache::set("msg_time", time(), -2); - return "OK!"; +public function onStart() { + LightCache::addPersistence("msg_time"); } /** * @CQCommand("getStore") @@ -187,11 +220,11 @@ public function getStore() { ^ 我在 2021-01-05 15:21:00 发送这条消息 -) store -( OK! +) getStore +( 2021-01-05 15:20:00 ^ 这时我用 Ctrl+C 停止框架,过一会儿再启动 ) getStore -( 存储时间:2021-01-05 15:21:00 +( 存储时间:2021-01-05 15:20:00 ### 数据加锁 diff --git a/docs/guide/basic-config.md b/docs/guide/basic-config.md index 4bbf427a..4e5d6aae 100644 --- a/docs/guide/basic-config.md +++ b/docs/guide/basic-config.md @@ -43,6 +43,7 @@ | `worker_num` | Worker 工作进程数 | 运行框架的主机 CPU 核心数 | | `dispatch_mode` | 数据包分发策略,见 [文档](https://wiki.swoole.com/#/server/setting?id=dispatch_mode) | 2 | | `max_coroutine` | 最大协程并发数 | 300000 | +| `max_wait_time` | 退出进程时等待协程恢复的最长时间(秒) | 5(2.4.3 版本后默认值) | | `task_worker_num` | TaskWorker 工作进程数 | 默认不开启(此参数被注释) | | `task_enable_coroutine` | TaskWorker 工作进程启用协程 | 默认不开启(此参数被注释)或 `bool` | diff --git a/docs/update/v2.md b/docs/update/v2.md index 068c6f74..4386c076 100644 --- a/docs/update/v2.md +++ b/docs/update/v2.md @@ -1,8 +1,14 @@ # 更新日志(v2 版本) -# v2.4.2 (build 402) +## v2.4.3 (build 403) -> 更新时间:202.3.27 +> 更新时间:2021.3.29 + +- + +## v2.4.2 (build 402) + +> 更新时间:2021.3.27 - 更改:`WORKING_DIR` 常量的含义 - 修复:未指定 `--remote-terminal` 参数时还依旧开启远程终端的 bug diff --git a/src/ZM/Annotation/Command/TerminalCommand.php b/src/ZM/Annotation/Command/TerminalCommand.php index 30034cb8..335ac96e 100644 --- a/src/ZM/Annotation/Command/TerminalCommand.php +++ b/src/ZM/Annotation/Command/TerminalCommand.php @@ -5,6 +5,7 @@ namespace ZM\Annotation\Command; use Doctrine\Common\Annotations\Annotation\Required; use Doctrine\Common\Annotations\Annotation\Target; +use ZM\Annotation\AnnotationBase; /** * Class TerminalCommand @@ -12,7 +13,7 @@ use Doctrine\Common\Annotations\Annotation\Target; * @Annotation * @Target("METHOD") */ -class TerminalCommand +class TerminalCommand extends AnnotationBase { /** * @var string @@ -20,6 +21,8 @@ class TerminalCommand */ public $command; + public $alias = ''; + /** * @var string */ diff --git a/src/ZM/Annotation/Swoole/SwooleHandler.php b/src/ZM/Annotation/Swoole/SwooleHandler.php index d70fb7d0..d5f54752 100644 --- a/src/ZM/Annotation/Swoole/SwooleHandler.php +++ b/src/ZM/Annotation/Swoole/SwooleHandler.php @@ -12,7 +12,7 @@ use ZM\Annotation\AnnotationBase; * Class SwooleHandler * @package ZM\Annotation\Swoole * @Annotation - * @Target("METHOD") + * @Target("ALL") */ class SwooleHandler extends AnnotationBase { diff --git a/src/ZM/ConsoleApplication.php b/src/ZM/ConsoleApplication.php index 9ad65294..88bf7fe8 100644 --- a/src/ZM/ConsoleApplication.php +++ b/src/ZM/ConsoleApplication.php @@ -19,8 +19,8 @@ use ZM\Command\SystemdCommand; class ConsoleApplication extends Application { - const VERSION_ID = 402; - const VERSION = "2.4.2"; + const VERSION_ID = 403; + const VERSION = "2.4.3"; public function __construct(string $name = 'UNKNOWN') { define("ZM_VERSION_ID", self::VERSION_ID); @@ -72,13 +72,6 @@ class ConsoleApplication extends Application if (!empty($with_default_cmd)) { $this->setDefaultCommand($with_default_cmd); } - /* - $command_register = ZMConfig::get("global", "command_register_class") ?? []; - foreach ($command_register as $v) { - $obj = new $v(); - if (!($obj instanceof Command)) throw new TypeError("Command register class must be extended by Symfony\\Component\\Console\\Command\\Command"); - $this->add($obj); - }*/ return $this; } diff --git a/src/ZM/Event/ServerEventHandler.php b/src/ZM/Event/ServerEventHandler.php deleted file mode 100644 index c11cce95..00000000 --- a/src/ZM/Event/ServerEventHandler.php +++ /dev/null @@ -1,701 +0,0 @@ -getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"); - } catch (Error $e) { - Console::error("Uncaught error " . get_class($e) . ": " . $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"); - } - }); - }*/ - Process::signal(SIGINT, function () use ($r) { - if (zm_atomic("_int_is_reload")->get() === 1) { - zm_atomic("_int_is_reload")->set(0); - \server()->reload(); - } else { - echo "\r"; - Console::warning("Server interrupted(SIGINT) on Master."); - if ((Framework::$server->inotify ?? null) !== null) - /** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify); - ZMUtil::stop(); - } - }); - if (Framework::$argv["daemon"]) { - $daemon_data = json_encode([ - "pid" => \server()->master_pid, - "stdout" => ZMConfig::get("global")["swoole"]["log_file"] - ], 128 | 256); - file_put_contents(DataProvider::getWorkingDir() . "/.daemon_pid", $daemon_data); - } - if (Framework::$argv["watch"]) { - if (extension_loaded('inotify')) { - Console::warning("Enabled File watcher, do not use in production."); - /** @noinspection PhpUndefinedFieldInspection */ - Framework::$server->inotify = $fd = inotify_init(); - $this->addWatcher(DataProvider::getWorkingDir() . "/src", $fd); - Event::add($fd, function () use ($fd) { - $r = inotify_read($fd); - dump($r); - ZMUtil::reload(); - }); - } else { - Console::warning("You have not loaded \"inotify\" extension, please install first."); - } - } - } - - /** - * @SwooleHandler("shutdown") - */ - public function onShutdown() { - Console::debug("正在关闭 Master 进程,pid=" . posix_getpid()); - } - - /** - * @SwooleHandler("WorkerStop") - * @param $server - * @param $worker_id - * @throws Exception - */ - public function onWorkerStop(Server $server, $worker_id) { - if ($worker_id == (ZMConfig::get("worker_cache")["worker"] ?? 0)) { - LightCache::savePersistence(); - } - Console::verbose(($server->taskworker ? "Task" : "") . "Worker #$worker_id 已停止"); - } - - /** - * @SwooleHandler("WorkerStart") - * @param Server $server - * @param $worker_id - * @throws Exception - */ - public function onWorkerStart(Server $server, $worker_id) { - //if (ZMBuf::atomic("stop_signal")->get() != 0) return; - - Process::signal(SIGINT, function () use ($worker_id, $server) { - Timer::clearAll(); - ProcessManager::resumeAllWorkerCoroutines(); - Console::debug("正在关闭 " . ($server->taskworker ? "Task" : "") . "Worker 进程 " . Console::setColor("#" . \server()->worker_id, "gold") . TermColor::frontColor256(59) . ", pid=" . posix_getpid()); - server()->stop($worker_id); - }); - unset(Context::$context[Coroutine::getCid()]); - if ($server->taskworker === false) { - Process::signal(SIGUSR1, function() use ($worker_id){ - Timer::clearAll(); - ProcessManager::resumeAllWorkerCoroutines(); - }); - zm_atomic("_#worker_".$worker_id)->set($server->worker_pid); - try { - register_shutdown_function(function () use ($server) { - $error = error_get_last(); - if ($error["type"] != 0) { - Console::error("Internal fatal error: " . $error["message"] . " at " . $error["file"] . "({$error["line"]})"); - } - //DataProvider::saveBuffer(); - /** @var Server $server */ - if (server() === null) $server->shutdown(); - else server()->shutdown(); - }); - - Console::verbose("Worker #{$server->worker_id} 启动中"); - Framework::$server = $server; - //ZMBuf::resetCache(); //清空变量缓存 - //ZMBuf::set("wait_start", []); //添加队列,在workerStart运行完成前先让其他协程等待执行 - foreach ($server->connections as $v) { - $server->close($v); - } - - - // 这里执行的是只需要执行一遍的代码,比如终端监听器和键盘监听器 - /*if ($server->worker_id === 0) { - global $terminal_id; - if ($terminal_id !== null) - go(function () { - while (true) { - $r = server()->process->exportSocket(); - $result = $r->recv(); - try { - if (!Terminal::executeCommand($result)) { - //if ($result == "stop" || $result == "reload" || $result == "r") { - //echo "Stopped.\n"; - break; - } - } catch (Exception $e) { - Console::error($e->getMessage()); - } catch (Error $e) { - Console::error($e->getMessage()); - } - } - }); - }*/ - //TODO: 单独抽出来MySQL和Redis连接池 - if (ZMConfig::get("global", "sql_config")["sql_host"] != "") { - if (SqlPoolStorage::$sql_pool !== null) { - SqlPoolStorage::$sql_pool->close(); - SqlPoolStorage::$sql_pool = null; - } - Console::info("新建SQL连接池中"); - 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 = ZMConfig::get("global", "sql_config"); - SqlPoolStorage::$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"]) - ->withOptions($sql["sql_options"] ?? [PDO::ATTR_STRINGIFY_FETCHES => false]) - ); - DB::initTableList(); - } - - // 开箱即用的Redis - $redis = ZMConfig::get("global", "redis_config"); - if ($redis !== null && $redis["host"] != "") { - if (!extension_loaded("redis")) Console::error("Can not find redis extension.\n"); - else ZMRedisPool::init($redis); - } - - $this->loadAnnotations(); //加载composer资源、phar外置包、注解解析注册等 - - //echo json_encode(debug_backtrace(), 128|256); - - EventManager::registerTimerTick(); //启动计时器 - //ZMBuf::unsetCache("wait_start"); - set_coroutine_params(["server" => $server, "worker_id" => $worker_id]); - $dispatcher = new EventDispatcher(OnStart::class); - $dispatcher->setRuleFunction(function ($v) { - return server()->worker_id === $v->worker_id || $v->worker_id === -1; - }); - $dispatcher->dispatchEvents($server, $worker_id); - if ($dispatcher->status === EventDispatcher::STATUS_NORMAL) Console::debug("@OnStart 执行完毕"); - else Console::warning("@OnStart 执行异常!"); - Console::success("Worker #" . $worker_id . " 已启动"); - } catch (Exception $e) { - Console::error("Worker加载出错!停止服务!"); - Console::error($e->getMessage() . "\n" . $e->getTraceAsString()); - ZMUtil::stop(); - return; - } catch (Error $e) { - Console::error("PHP Error: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine()); - Console::error("Maybe it caused by your own code if in your own Module directory."); - Console::log($e->getTraceAsString(), 'gray'); - posix_kill($server->master_pid, SIGINT); - } - } else { - // 这里是TaskWorker初始化的内容部分 - try { - Framework::$server = $server; - $this->loadAnnotations(); - Console::success("TaskWorker #" . $server->worker_id . " 已启动"); - } catch (Exception $e) { - Console::error("Worker加载出错!停止服务!"); - Console::error($e->getMessage() . "\n" . $e->getTraceAsString()); - ZMUtil::stop(); - return; - } catch (Error $e) { - Console::error("PHP Error: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine()); - Console::error("Maybe it caused by your own code if in your own Module directory."); - Console::log($e->getTraceAsString(), 'gray'); - posix_kill($server->master_pid, SIGINT); - } - } - } - - /** - * @SwooleHandler("message") - * @param $server - * @param Frame $frame - */ - public function onMessage($server, Frame $frame) { - - Console::debug("Calling Swoole \"message\" from fd=" . $frame->fd . ": " . TermColor::ITALIC . $frame->data . TermColor::RESET); - unset(Context::$context[Coroutine::getCid()]); - $conn = ManagerGM::get($frame->fd); - set_coroutine_params(["server" => $server, "frame" => $frame, "connection" => $conn]); - $dispatcher1 = new EventDispatcher(OnMessageEvent::class); - $dispatcher1->setRuleFunction(function ($v) { - /** @noinspection PhpUnreachableStatementInspection */ - return ctx()->getConnection()->getName() == $v->connect_type && eval("return " . $v->getRule() . ";"); - }); - - - $dispatcher = new EventDispatcher(OnSwooleEvent::class); - $dispatcher->setRuleFunction(function ($v) { - if ($v->getRule() == '') { - return strtolower($v->type) == 'message'; - } else { - /** @noinspection PhpUnreachableStatementInspection - * @noinspection RedundantSuppression - */ - if (strtolower($v->type) == 'message' && eval("return " . $v->getRule() . ";")) return true; - else return false; - } - }); - try { - //$starttime = microtime(true); - $dispatcher1->dispatchEvents($conn); - $dispatcher->dispatchEvents($conn); - //Console::success("Used ".round((microtime(true) - $starttime) * 1000, 3)." ms!"); - } catch (Exception $e) { - $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; - Console::error("Uncaught exception " . get_class($e) . " when calling \"message\": " . $error_msg); - Console::trace(); - } catch (Error $e) { - $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; - Console::error("Uncaught " . get_class($e) . " when calling \"message\": " . $error_msg); - Console::trace(); - } - - } - - /** - * @SwooleHandler("request") - * @param $request - * @param $response - */ - public function onRequest(?Request $request, ?\Swoole\Http\Response $response) { - $response = new Response($response); - foreach (ZMConfig::get("global")["http_header"] as $k => $v) { - $response->setHeader($k, $v); - } - unset(Context::$context[Co::getCid()]); - Console::debug("Calling Swoole \"request\" event from fd=" . $request->fd); - set_coroutine_params(["request" => $request, "response" => $response]); - - $dis1 = new EventDispatcher(OnRequestEvent::class); - $dis1->setRuleFunction(function ($v) { - return eval("return " . $v->getRule() . ";") ? true : false; - }); - - $dis = new EventDispatcher(OnSwooleEvent::class); - $dis->setRuleFunction(function ($v) { - if ($v->getRule() == '') { - return strtolower($v->type) == 'request'; - } else { - /** @noinspection PhpUnreachableStatementInspection */ - if (strtolower($v->type) == 'request' && eval("return " . $v->getRule() . ";")) return true; - else return false; - } - }); - - try { - $dis1->dispatchEvents($request, $response); - $dis->dispatchEvents($request, $response); - if ($dis->status === EventDispatcher::STATUS_NORMAL && $dis1->status === EventDispatcher::STATUS_NORMAL) { - $result = HttpUtil::parseUri($request, $response, $request->server["request_uri"], $node, $params); - if ($result === true) { - ctx()->setCache("params", $params); - $dispatcher = new EventDispatcher(RequestMapping::class); - $div = new RequestMapping(); - $div->route = $node["route"]; - $div->params = $params; - $div->method = $node["method"]; - $div->request_method = $node["request_method"]; - $div->class = $node["class"]; - //Console::success("正在执行路由:".$node["method"]); - $dispatcher->dispatchEvent($div, null, $params, $request, $response); - if (is_string($dispatcher->store) && !$response->isEnd()) $response->end($dispatcher->store); - } - } - if (!$response->isEnd()) { - //Console::warning('返回了404'); - HttpUtil::responseCodePage($response, 404); - } - } catch (InterruptException $e) { - // do nothing - } catch (Exception $e) { - $response->status(500); - Console::info($request->server["remote_addr"] . ":" . $request->server["remote_port"] . - " [" . $response->getStatusCode() . "] " . $request->server["request_uri"] - ); - if (!$response->isEnd()) { - if (ZMConfig::get("global", "debug_mode")) - $response->end("Internal server exception: " . $e->getMessage()); - else - $response->end("Internal server error."); - } - Console::error("Internal server exception (500), caused by " . get_class($e) . ": " . $e->getMessage()); - Console::log($e->getTraceAsString(), "gray"); - } catch (Error $e) { - $response->status(500); - Console::info($request->server["remote_addr"] . ":" . $request->server["remote_port"] . - " [" . $response->getStatusCode() . "] " . $request->server["request_uri"] - ); - if (!$response->isEnd()) { - $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; - if (ZMConfig::get("global", "debug_mode")) - $response->end("Internal server error: " . $error_msg); - else - $response->end("Internal server error."); - } - Console::error("Internal server error (500), caused by " . get_class($e) . ": " . $e->getMessage()); - Console::log($e->getTraceAsString(), "gray"); - } - } - - /** - * @SwooleHandler("open") - * @param $server - * @param Request $request - */ - public function onOpen($server, Request $request) { - Console::debug("Calling Swoole \"open\" event from fd=" . $request->fd); - unset(Context::$context[Co::getCid()]); - $type = strtolower($request->header["x-client-role"] ?? $request->get["type"] ?? ""); - $access_token = explode(" ", $request->header["authorization"] ?? "")[1] ?? $request->get["token"] ?? ""; - $token = ZMConfig::get("global", "access_token"); - if ($token instanceof Closure) { - if (!$token($access_token)) { - $server->close($request->fd); - Console::warning("Unauthorized access_token: " . $access_token); - return; - } - } elseif (is_string($token)) { - if ($access_token !== $token && $token !== "") { - $server->close($request->fd); - Console::warning("Unauthorized access_token: " . $access_token); - return; - } - } - $type_conn = ManagerGM::getTypeClassName($type); - ManagerGM::pushConnect($request->fd, $type_conn); - $conn = ManagerGM::get($request->fd); - set_coroutine_params(["server" => $server, "request" => $request, "connection" => $conn, "fd" => $request->fd]); - $conn->setOption("connect_id", strval($request->header["x-self-id"] ?? "")); - - $dispatcher1 = new EventDispatcher(OnOpenEvent::class); - $dispatcher1->setRuleFunction(function ($v) { - return ctx()->getConnection()->getName() == $v->connect_type && eval("return " . $v->getRule() . ";"); - }); - - $dispatcher = new EventDispatcher(OnSwooleEvent::class); - $dispatcher->setRuleFunction(function ($v) { - if ($v->getRule() == '') { - return strtolower($v->type) == 'open'; - } else { - /** @noinspection PhpUnreachableStatementInspection */ - if (strtolower($v->type) == 'open' && eval("return " . $v->getRule() . ";")) return true; - else return false; - } - }); - try { - $obb_onebot = ZMConfig::get("global", "onebot") ?? - ZMConfig::get("global", "modules")["onebot"] ?? - ["status" => true, "single_bot_mode" => false, "message_level" => 99999]; - $onebot_status = $obb_onebot["status"]; - if ($conn->getName() === 'qq' && $onebot_status === true) { - if ($obb_onebot["single_bot_mode"]) { - LightCacheInside::set("connect", "conn_fd", $request->fd); - } - } - $dispatcher1->dispatchEvents($conn); - $dispatcher->dispatchEvents($conn); - } catch (Exception $e) { - $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; - Console::error("Uncaught exception " . get_class($e) . " when calling \"open\": " . $error_msg); - Console::trace(); - } catch (Error $e) { - $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; - Console::error("Uncaught " . get_class($e) . " when calling \"open\": " . $error_msg); - Console::trace(); - } - } - - /** - * @SwooleHandler("close") - * @param $server - * @param $fd - */ - public function onClose($server, $fd) { - unset(Context::$context[Co::getCid()]); - $conn = ManagerGM::get($fd); - if ($conn === null) return; - Console::debug("Calling Swoole \"close\" event from fd=" . $fd); - set_coroutine_params(["server" => $server, "connection" => $conn, "fd" => $fd]); - - $dispatcher1 = new EventDispatcher(OnCloseEvent::class); - $dispatcher1->setRuleFunction(function ($v) { - return $v->connect_type == ctx()->getConnection()->getName() && eval("return " . $v->getRule() . ";"); - }); - - $dispatcher = new EventDispatcher(OnSwooleEvent::class); - $dispatcher->setRuleFunction(function ($v) { - if ($v->getRule() == '') { - return strtolower($v->type) == 'close'; - } else { - /** @noinspection PhpUnreachableStatementInspection */ - if (strtolower($v->type) == 'close' && eval("return " . $v->getRule() . ";")) return true; - else return false; - } - }); - try { - $obb_onebot = ZMConfig::get("global", "onebot") ?? - ZMConfig::get("global", "modules")["onebot"] ?? - ["status" => true, "single_bot_mode" => false, "message_level" => 99999]; - if ($conn->getName() === 'qq' && $obb_onebot["status"] === true) { - if ($obb_onebot["single_bot_mode"]) { - LightCacheInside::set("connect", "conn_fd", -1); - } - } - $dispatcher1->dispatchEvents($conn); - $dispatcher->dispatchEvents($conn); - } catch (Exception $e) { - $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; - Console::error("Uncaught exception " . get_class($e) . " when calling \"close\": " . $error_msg); - Console::trace(); - } catch (Error $e) { - $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; - Console::error("Uncaught " . get_class($e) . " when calling \"close\": " . $error_msg); - Console::trace(); - } - ManagerGM::popConnect($fd); - } - - /** - * @SwooleHandler("pipeMessage") - * @param Server $server - * @param $src_worker_id - * @param $data - * @throws Exception - * @noinspection PhpUnusedParameterInspection - */ - public function onPipeMessage(Server $server, $src_worker_id, $data) { - //var_dump($data, $server->worker_id); - //unset(Context::$context[Co::getCid()]); - $data = json_decode($data, true); - ProcessManager::workerAction($src_worker_id, $data); - } - - /** - * @SwooleHandler("beforeReload") - */ - public function onBeforeReload() { - for($i = 0; $i < ZM_WORKER_NUM; ++$i) { - $pid = zm_atomic("_#worker_".$i)->get(); - Process::kill($pid, SIGUSR1); - } - - Console::info(Console::setColor("Reloading server...", "gold")); - usleep(800 * 1000); - LightCacheInside::unset("wait_api", "wait_api"); - } - - /** - * @SwooleHandler("task") - * @param Server|null $server - * @param Server\Task $task - * @noinspection PhpUnusedParameterInspection - */ - public function onTask(?Server $server, Server\Task $task) { - if (isset($task->data["task"])) { - $dispatcher = new EventDispatcher(OnTask::class); - $dispatcher->setRuleFunction(function ($v) use ($task) { - /** @var OnTask $v */ - return $v->task_name == $task->data["task"]; - }); - $dispatcher->setReturnFunction(function ($return) { - EventDispatcher::interrupt($return); - }); - $params = $task->data["params"]; - try { - $dispatcher->dispatchEvents(...$params); - } catch (Throwable $e) { - $finish["throw"] = $e; - } - if ($dispatcher->status === EventDispatcher::STATUS_EXCEPTION) { - $finish["result"] = null; - $finish["retcode"] = -1; - } else { - $finish["result"] = $dispatcher->store; - $finish["retcode"] = 0; - } - if (zm_atomic("server_is_stopped")->get() === 1) { - return; - } - $task->finish($finish); - } else { - try { - $dispatcher = new EventDispatcher(OnTaskEvent::class); - $dispatcher->setRuleFunction(function ($v) { - /** @var OnTaskEvent $v */ - return eval("return " . $v->getRule() . ";"); - }); - $dispatcher->dispatchEvents(); - } catch (Exception $e) { - $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; - Console::error("Uncaught exception " . get_class($e) . " when calling \"task\": " . $error_msg); - Console::trace(); - } catch (Error $e) { - $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; - Console::error("Uncaught " . get_class($e) . " when calling \"task\": " . $error_msg); - Console::trace(); - } - } - } - - /** - * @throws ReflectionException - * @throws Exception - */ - private function loadAnnotations() { - //加载phar包 - /*Console::debug("加载外部phar包中"); - $dir = DataProvider::getWorkingDir() . "/resources/package/"; - if (version_compare(SWOOLE_VERSION, "4.4.0", ">=")) Timer::clearAll(); - if (is_dir($dir)) { - $list = scandir($dir); - unset($list[0], $list[1]); - foreach ($list as $v) { - if (is_dir($dir . $v)) continue; - if (pathinfo($dir . $v, 4) == "phar") { - Console::debug("加载Phar: " . $dir . $v . " 中"); - require_once($dir . $v); - } - } - }*/ - - //加载各个模块的注解类,以及反射 - Console::debug("检索Module中"); - $parser = new AnnotationParser(); - $path = DataProvider::getWorkingDir() . "/src/"; - $dir = scandir($path); - unset($dir[0], $dir[1]); - $composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true); - foreach ($dir as $v) { - if (is_dir($path . "/" . $v) && isset($composer["autoload"]["psr-4"][$v . "\\"]) && !in_array($composer["autoload"]["psr-4"][$v . "\\"], $composer["extra"]["exclude_annotate"] ?? [])) { - if (\server()->worker_id == 0) - Console::verbose("Add " . $v . " to register path"); - $parser->addRegisterPath(DataProvider::getWorkingDir() . "/src/" . $v . "/", $v); - } - } - $parser->registerMods(); - EventManager::loadEventByParser($parser); //加载事件 - - //加载自定义的全局函数 - Console::debug("加载自定义上下文中..."); - $context_class = ZMConfig::get("global", "context_class"); - if (!is_a($context_class, ContextInterface::class, true)) { - throw new Exception("Context class must implemented from ContextInterface!"); - } - - //加载插件 - $obb_onebot = ZMConfig::get("global", "onebot") ?? - ZMConfig::get("global", "modules")["onebot"] ?? - ["status" => true, "single_bot_mode" => false, "message_level" => 99999]; - - if ($obb_onebot["status"]) { - $obj = new OnSwooleEvent(); - $obj->class = QQBot::class; - $obj->method = 'handleByEvent'; - $obj->type = 'message'; - $obj->level = $obb_onebot["message_level"] ?? 99999; - $obj->rule = 'connectIsQQ()'; - EventManager::addEvent(OnSwooleEvent::class, $obj); - if ($obb_onebot["single_bot_mode"]) { - LightCacheInside::set("connect", "conn_fd", -1); - } else { - LightCacheInside::set("connect", "conn_fd", -2); - } - } - - //TODO: 编写加载外部插件的方式 - } - - private function addWatcher($maindir, $fd) { - $dir = scandir($maindir); - unset($dir[0], $dir[1]); - foreach ($dir as $subdir) { - if (is_dir($maindir . "/" . $subdir)) { - Console::debug("添加监听目录:" . $maindir . "/" . $subdir); - inotify_add_watch($fd, $maindir . "/" . $subdir, IN_ATTRIB | IN_ISDIR); - $this->addWatcher($maindir . "/" . $subdir, $fd); - } - } - } -} diff --git a/src/ZM/Event/SwooleEvent.php b/src/ZM/Event/SwooleEvent.php new file mode 100644 index 00000000..b5834ee2 --- /dev/null +++ b/src/ZM/Event/SwooleEvent.php @@ -0,0 +1,9 @@ +get(), SIGUSR1); + } + usleep(800 * 1000); + } +} \ No newline at end of file diff --git a/src/ZM/Event/SwooleEvent/OnClose.php b/src/ZM/Event/SwooleEvent/OnClose.php new file mode 100644 index 00000000..acddcdec --- /dev/null +++ b/src/ZM/Event/SwooleEvent/OnClose.php @@ -0,0 +1,73 @@ + $server, "connection" => $conn, "fd" => $fd]); + + $dispatcher1 = new EventDispatcher(OnCloseEvent::class); + $dispatcher1->setRuleFunction(function ($v) { + return $v->connect_type == ctx()->getConnection()->getName() && eval("return " . $v->getRule() . ";"); + }); + + $dispatcher = new EventDispatcher(OnSwooleEvent::class); + $dispatcher->setRuleFunction(function ($v) { + if ($v->getRule() == '') { + return strtolower($v->type) == 'close'; + } else { + /** @noinspection PhpUnreachableStatementInspection */ + if (strtolower($v->type) == 'close' && eval("return " . $v->getRule() . ";")) return true; + else return false; + } + }); + try { + $obb_onebot = ZMConfig::get("global", "onebot") ?? + ZMConfig::get("global", "modules")["onebot"] ?? + ["status" => true, "single_bot_mode" => false, "message_level" => 99999]; + if ($conn->getName() === 'qq' && $obb_onebot["status"] === true) { + if ($obb_onebot["single_bot_mode"]) { + LightCacheInside::set("connect", "conn_fd", -1); + } + } + $dispatcher1->dispatchEvents($conn); + $dispatcher->dispatchEvents($conn); + } catch (Exception $e) { + $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; + Console::error("Uncaught exception " . get_class($e) . " when calling \"close\": " . $error_msg); + Console::trace(); + } catch (Error $e) { + $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; + Console::error("Uncaught " . get_class($e) . " when calling \"close\": " . $error_msg); + Console::trace(); + } + ManagerGM::popConnect($fd); + } +} \ No newline at end of file diff --git a/src/ZM/Event/SwooleEvent/OnManagerStart.php b/src/ZM/Event/SwooleEvent/OnManagerStart.php new file mode 100644 index 00000000..240f1233 --- /dev/null +++ b/src/ZM/Event/SwooleEvent/OnManagerStart.php @@ -0,0 +1,27 @@ +fd . ": " . TermColor::ITALIC . $frame->data . TermColor::RESET); + unset(Context::$context[Coroutine::getCid()]); + $conn = ManagerGM::get($frame->fd); + set_coroutine_params(["server" => $server, "frame" => $frame, "connection" => $conn]); + $dispatcher1 = new EventDispatcher(OnMessageEvent::class); + $dispatcher1->setRuleFunction(function ($v) { + /** @noinspection PhpUnreachableStatementInspection */ + return ctx()->getConnection()->getName() == $v->connect_type && eval("return " . $v->getRule() . ";"); + }); + + + $dispatcher = new EventDispatcher(OnSwooleEvent::class); + $dispatcher->setRuleFunction(function ($v) { + if ($v->getRule() == '') { + return strtolower($v->type) == 'message'; + } else { + /** @noinspection PhpUnreachableStatementInspection + * @noinspection RedundantSuppression + */ + if (strtolower($v->type) == 'message' && eval("return " . $v->getRule() . ";")) return true; + else return false; + } + }); + try { + //$starttime = microtime(true); + $dispatcher1->dispatchEvents($conn); + $dispatcher->dispatchEvents($conn); + //Console::success("Used ".round((microtime(true) - $starttime) * 1000, 3)." ms!"); + } catch (Exception $e) { + $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; + Console::error("Uncaught exception " . get_class($e) . " when calling \"message\": " . $error_msg); + Console::trace(); + } catch (Error $e) { + $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; + Console::error("Uncaught " . get_class($e) . " when calling \"message\": " . $error_msg); + Console::trace(); + } + + } +} \ No newline at end of file diff --git a/src/ZM/Event/SwooleEvent/OnOpen.php b/src/ZM/Event/SwooleEvent/OnOpen.php new file mode 100644 index 00000000..db41f2e1 --- /dev/null +++ b/src/ZM/Event/SwooleEvent/OnOpen.php @@ -0,0 +1,92 @@ +fd); + unset(Context::$context[Co::getCid()]); + $type = strtolower($request->header["x-client-role"] ?? $request->get["type"] ?? ""); + $access_token = explode(" ", $request->header["authorization"] ?? "")[1] ?? $request->get["token"] ?? ""; + $token = ZMConfig::get("global", "access_token"); + if ($token instanceof Closure) { + if (!$token($access_token)) { + $server->close($request->fd); + Console::warning("Unauthorized access_token: " . $access_token); + return; + } + } elseif (is_string($token)) { + if ($access_token !== $token && $token !== "") { + $server->close($request->fd); + Console::warning("Unauthorized access_token: " . $access_token); + return; + } + } + $type_conn = ManagerGM::getTypeClassName($type); + ManagerGM::pushConnect($request->fd, $type_conn); + $conn = ManagerGM::get($request->fd); + set_coroutine_params(["server" => $server, "request" => $request, "connection" => $conn, "fd" => $request->fd]); + $conn->setOption("connect_id", strval($request->header["x-self-id"] ?? "")); + + $dispatcher1 = new EventDispatcher(OnOpenEvent::class); + $dispatcher1->setRuleFunction(function ($v) { + return ctx()->getConnection()->getName() == $v->connect_type && eval("return " . $v->getRule() . ";"); + }); + + $dispatcher = new EventDispatcher(OnSwooleEvent::class); + $dispatcher->setRuleFunction(function ($v) { + if ($v->getRule() == '') { + return strtolower($v->type) == 'open'; + } else { + if (strtolower($v->type) == 'open' && eval("return " . $v->getRule() . ";")) return true; + else return false; + } + }); + try { + $obb_onebot = ZMConfig::get("global", "onebot") ?? + ZMConfig::get("global", "modules")["onebot"] ?? + ["status" => true, "single_bot_mode" => false, "message_level" => 99999]; + $onebot_status = $obb_onebot["status"]; + if ($conn->getName() === 'qq' && $onebot_status === true) { + if ($obb_onebot["single_bot_mode"]) { + LightCacheInside::set("connect", "conn_fd", $request->fd); + } + } + $dispatcher1->dispatchEvents($conn); + $dispatcher->dispatchEvents($conn); + } catch (Exception $e) { + $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; + Console::error("Uncaught exception " . get_class($e) . " when calling \"open\": " . $error_msg); + Console::trace(); + } catch (Error $e) { + $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; + Console::error("Uncaught " . get_class($e) . " when calling \"open\": " . $error_msg); + Console::trace(); + } + } +} \ No newline at end of file diff --git a/src/ZM/Event/SwooleEvent/OnPipeMessage.php b/src/ZM/Event/SwooleEvent/OnPipeMessage.php new file mode 100644 index 00000000..e2adf0cf --- /dev/null +++ b/src/ZM/Event/SwooleEvent/OnPipeMessage.php @@ -0,0 +1,23 @@ + $v) { + $response->setHeader($k, $v); + } + unset(Context::$context[Co::getCid()]); + Console::debug("Calling Swoole \"request\" event from fd=" . $request->fd); + set_coroutine_params(["request" => $request, "response" => $response]); + + $dis1 = new EventDispatcher(OnRequestEvent::class); + $dis1->setRuleFunction(function ($v) { + /** @noinspection PhpUnreachableStatementInspection */ + return eval("return " . $v->getRule() . ";") ? true : false; + }); + + $dis = new EventDispatcher(OnSwooleEvent::class); + $dis->setRuleFunction(function ($v) { + if ($v->getRule() == '') { + return strtolower($v->type) == 'request'; + } else { + /** @noinspection PhpUnreachableStatementInspection */ + if (strtolower($v->type) == 'request' && eval("return " . $v->getRule() . ";")) return true; + else return false; + } + }); + + try { + $dis1->dispatchEvents($request, $response); + $dis->dispatchEvents($request, $response); + if ($dis->status === EventDispatcher::STATUS_NORMAL && $dis1->status === EventDispatcher::STATUS_NORMAL) { + $result = HttpUtil::parseUri($request, $response, $request->server["request_uri"], $node, $params); + if ($result === true) { + ctx()->setCache("params", $params); + $dispatcher = new EventDispatcher(RequestMapping::class); + $div = new RequestMapping(); + $div->route = $node["route"]; + $div->params = $params; + $div->method = $node["method"]; + $div->request_method = $node["request_method"]; + $div->class = $node["class"]; + //Console::success("正在执行路由:".$node["method"]); + $dispatcher->dispatchEvent($div, null, $params, $request, $response); + if (is_string($dispatcher->store) && !$response->isEnd()) $response->end($dispatcher->store); + } + } + if (!$response->isEnd()) { + //Console::warning('返回了404'); + HttpUtil::responseCodePage($response, 404); + } + } catch (InterruptException $e) { + // do nothing + } catch (Exception $e) { + $response->status(500); + Console::info($request->server["remote_addr"] . ":" . $request->server["remote_port"] . + " [" . $response->getStatusCode() . "] " . $request->server["request_uri"] + ); + if (!$response->isEnd()) { + if (ZMConfig::get("global", "debug_mode")) + $response->end("Internal server exception: " . $e->getMessage()); + else + $response->end("Internal server error."); + } + Console::error("Internal server exception (500), caused by " . get_class($e) . ": " . $e->getMessage()); + Console::log($e->getTraceAsString(), "gray"); + } catch (Error $e) { + $response->status(500); + Console::info($request->server["remote_addr"] . ":" . $request->server["remote_port"] . + " [" . $response->getStatusCode() . "] " . $request->server["request_uri"] + ); + if (!$response->isEnd()) { + $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; + if (ZMConfig::get("global", "debug_mode")) + $response->end("Internal server error: " . $error_msg); + else + $response->end("Internal server error."); + } + Console::error("Internal server error (500), caused by " . get_class($e) . ": " . $e->getMessage()); + Console::log($e->getTraceAsString(), "gray"); + } + } +} \ No newline at end of file diff --git a/src/ZM/Event/SwooleEvent/OnShutdown.php b/src/ZM/Event/SwooleEvent/OnShutdown.php new file mode 100644 index 00000000..2996929a --- /dev/null +++ b/src/ZM/Event/SwooleEvent/OnShutdown.php @@ -0,0 +1,22 @@ +get() === 1) { + zm_atomic("_int_is_reload")->set(0); + $server->reload(); + } else { + echo "\r"; + Console::warning("Server interrupted(SIGINT) on Master."); + if ((Framework::$server->inotify ?? null) !== null) + /** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify); + Process::kill($server->master_pid, SIGTERM); + } + }); + if (Framework::$argv["daemon"]) { + $daemon_data = json_encode([ + "pid" => $server->master_pid, + "stdout" => ZMConfig::get("global")["swoole"]["log_file"] + ], 128 | 256); + file_put_contents(DataProvider::getWorkingDir() . "/.daemon_pid", $daemon_data); + } + if (Framework::$argv["watch"]) { + if (extension_loaded('inotify')) { + Console::warning("Enabled File watcher, do not use in production."); + /** @noinspection PhpUndefinedFieldInspection */ + Framework::$server->inotify = $fd = inotify_init(); + $this->addWatcher(DataProvider::getWorkingDir() . "/src", $fd); + Event::add($fd, function () use ($fd) { + $r = inotify_read($fd); + dump($r); + ZMUtil::reload(); + }); + } else { + Console::warning("You have not loaded \"inotify\" extension, please install first."); + } + } + } + + private function addWatcher($maindir, $fd) { + $dir = scandir($maindir); + unset($dir[0], $dir[1]); + foreach ($dir as $subdir) { + if (is_dir($maindir . "/" . $subdir)) { + Console::debug("添加监听目录:" . $maindir . "/" . $subdir); + inotify_add_watch($fd, $maindir . "/" . $subdir, IN_ATTRIB | IN_ISDIR); + $this->addWatcher($maindir . "/" . $subdir, $fd); + } + } + } +} \ No newline at end of file diff --git a/src/ZM/Event/SwooleEvent/OnTask.php b/src/ZM/Event/SwooleEvent/OnTask.php new file mode 100644 index 00000000..cd299130 --- /dev/null +++ b/src/ZM/Event/SwooleEvent/OnTask.php @@ -0,0 +1,77 @@ +data["task"])) { + $dispatcher = new EventDispatcher(\ZM\Annotation\Swoole\OnTask::class); + $dispatcher->setRuleFunction(function ($v) use ($task) { + /** @var \ZM\Annotation\Swoole\OnTask $v */ + return $v->task_name == $task->data["task"]; + }); + $dispatcher->setReturnFunction(function ($return) { + EventDispatcher::interrupt($return); + }); + $params = $task->data["params"]; + try { + $dispatcher->dispatchEvents(...$params); + } catch (Throwable $e) { + $finish["throw"] = $e; + } + if ($dispatcher->status === EventDispatcher::STATUS_EXCEPTION) { + $finish["result"] = null; + $finish["retcode"] = -1; + } else { + $finish["result"] = $dispatcher->store; + $finish["retcode"] = 0; + } + if (zm_atomic("server_is_stopped")->get() === 1) { + return; + } + $task->finish($finish); + } else { + try { + $dispatcher = new EventDispatcher(OnTaskEvent::class); + $dispatcher->setRuleFunction(function ($v) { + /** @var OnTaskEvent $v */ + return eval("return " . $v->getRule() . ";"); + }); + $dispatcher->dispatchEvents(); + } catch (Exception $e) { + $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; + Console::error("Uncaught exception " . get_class($e) . " when calling \"task\": " . $error_msg); + Console::trace(); + } catch (Error $e) { + $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")"; + Console::error("Uncaught " . get_class($e) . " when calling \"task\": " . $error_msg); + Console::trace(); + } + } + } +} \ No newline at end of file diff --git a/src/ZM/Event/SwooleEvent/OnWorkerExit.php b/src/ZM/Event/SwooleEvent/OnWorkerExit.php new file mode 100644 index 00000000..c4023b90 --- /dev/null +++ b/src/ZM/Event/SwooleEvent/OnWorkerExit.php @@ -0,0 +1,24 @@ +taskworker === false) { + Process::signal(SIGUSR1, function () use ($worker_id) { + Timer::clearAll(); + ProcessManager::resumeAllWorkerCoroutines(); + }); + zm_atomic("_#worker_" . $worker_id)->set($server->worker_pid); + if (LightCacheInside::get("wait_api", "wait_api") !== null) { + LightCacheInside::unset("wait_api", "wait_api"); + } + try { + register_shutdown_function(function () use ($server) { + $error = error_get_last(); + if ($error["type"] != 0) { + Console::error("Internal fatal error: " . $error["message"] . " at " . $error["file"] . "({$error["line"]})"); + } + //DataProvider::saveBuffer(); + /** @var Server $server */ + if (server() === null) $server->shutdown(); + else server()->shutdown(); + }); + + Console::verbose("Worker #{$server->worker_id} 启动中"); + Framework::$server = $server; + //ZMBuf::resetCache(); //清空变量缓存 + //ZMBuf::set("wait_start", []); //添加队列,在workerStart运行完成前先让其他协程等待执行 + foreach ($server->connections as $v) { + $server->close($v); + } + + //TODO: 单独抽出来MySQL和Redis连接池 + if (ZMConfig::get("global", "sql_config")["sql_host"] != "") { + if (SqlPoolStorage::$sql_pool !== null) { + SqlPoolStorage::$sql_pool->close(); + SqlPoolStorage::$sql_pool = null; + } + Console::info("新建SQL连接池中"); + 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 = ZMConfig::get("global", "sql_config"); + SqlPoolStorage::$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"]) + ->withOptions($sql["sql_options"] ?? [PDO::ATTR_STRINGIFY_FETCHES => false]) + ); + DB::initTableList(); + } + + // 开箱即用的Redis + $redis = ZMConfig::get("global", "redis_config"); + if ($redis !== null && $redis["host"] != "") { + if (!extension_loaded("redis")) Console::error("Can not find redis extension.\n"); + else ZMRedisPool::init($redis); + } + + $this->loadAnnotations(); //加载composer资源、phar外置包、注解解析注册等 + + //echo json_encode(debug_backtrace(), 128|256); + + EventManager::registerTimerTick(); //启动计时器 + //ZMBuf::unsetCache("wait_start"); + set_coroutine_params(["server" => $server, "worker_id" => $worker_id]); + $dispatcher = new EventDispatcher(OnStart::class); + $dispatcher->setRuleFunction(function ($v) { + return server()->worker_id === $v->worker_id || $v->worker_id === -1; + }); + $dispatcher->dispatchEvents($server, $worker_id); + if ($dispatcher->status === EventDispatcher::STATUS_NORMAL) Console::debug("@OnStart 执行完毕"); + else Console::warning("@OnStart 执行异常!"); + Console::success("Worker #" . $worker_id . " 已启动"); + } catch (Exception $e) { + Console::error("Worker加载出错!停止服务!"); + Console::error($e->getMessage() . "\n" . $e->getTraceAsString()); + Process::kill($server->master_pid, SIGTERM); + return; + } catch (Error $e) { + Console::error("PHP Error: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine()); + Console::error("Maybe it caused by your own code if in your own Module directory."); + Console::log($e->getTraceAsString(), 'gray'); + Process::kill($server->master_pid, SIGTERM); + } + } else { + // 这里是TaskWorker初始化的内容部分 + try { + Framework::$server = $server; + $this->loadAnnotations(); + Console::success("TaskWorker #" . $server->worker_id . " 已启动"); + } catch (Exception $e) { + Console::error("Worker加载出错!停止服务!"); + Console::error($e->getMessage() . "\n" . $e->getTraceAsString()); + Process::kill($server->master_pid, SIGTERM); + return; + } catch (Error $e) { + Console::error("PHP Error: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine()); + Console::error("Maybe it caused by your own code if in your own Module directory."); + Console::log($e->getTraceAsString(), 'gray'); + Process::kill($server->master_pid, SIGTERM); + } + } + } + + /** + * @throws ReflectionException + * @throws Exception + */ + private function loadAnnotations() { + //加载phar包 + /*Console::debug("加载外部phar包中"); + $dir = DataProvider::getWorkingDir() . "/resources/package/"; + if (version_compare(SWOOLE_VERSION, "4.4.0", ">=")) Timer::clearAll(); + if (is_dir($dir)) { + $list = scandir($dir); + unset($list[0], $list[1]); + foreach ($list as $v) { + if (is_dir($dir . $v)) continue; + if (pathinfo($dir . $v, 4) == "phar") { + Console::debug("加载Phar: " . $dir . $v . " 中"); + require_once($dir . $v); + } + } + }*/ + + //加载各个模块的注解类,以及反射 + Console::debug("检索Module中"); + $parser = new AnnotationParser(); + $path = DataProvider::getWorkingDir() . "/src/"; + $dir = scandir($path); + unset($dir[0], $dir[1]); + $composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true); + foreach ($dir as $v) { + if (is_dir($path . "/" . $v) && isset($composer["autoload"]["psr-4"][$v . "\\"]) && !in_array($composer["autoload"]["psr-4"][$v . "\\"], $composer["extra"]["exclude_annotate"] ?? [])) { + if (\server()->worker_id == 0) + Console::verbose("Add " . $v . " to register path"); + $parser->addRegisterPath(DataProvider::getWorkingDir() . "/src/" . $v . "/", $v); + } + } + $parser->registerMods(); + EventManager::loadEventByParser($parser); //加载事件 + + //加载自定义的全局函数 + Console::debug("加载自定义上下文中..."); + $context_class = ZMConfig::get("global", "context_class"); + if (!is_a($context_class, ContextInterface::class, true)) { + throw new Exception("Context class must implemented from ContextInterface!"); + } + + //加载插件 + $obb_onebot = ZMConfig::get("global", "onebot") ?? + ZMConfig::get("global", "modules")["onebot"] ?? + ["status" => true, "single_bot_mode" => false, "message_level" => 99999]; + + if ($obb_onebot["status"]) { + $obj = new OnSwooleEvent(); + $obj->class = QQBot::class; + $obj->method = 'handleByEvent'; + $obj->type = 'message'; + $obj->level = $obb_onebot["message_level"] ?? 99999; + $obj->rule = 'connectIsQQ()'; + EventManager::addEvent(OnSwooleEvent::class, $obj); + if ($obb_onebot["single_bot_mode"]) { + LightCacheInside::set("connect", "conn_fd", -1); + } else { + LightCacheInside::set("connect", "conn_fd", -2); + } + } + + //TODO: 编写加载外部插件的方式 + } +} \ No newline at end of file diff --git a/src/ZM/Event/SwooleEvent/OnWorkerStop.php b/src/ZM/Event/SwooleEvent/OnWorkerStop.php new file mode 100644 index 00000000..68d08115 --- /dev/null +++ b/src/ZM/Event/SwooleEvent/OnWorkerStop.php @@ -0,0 +1,27 @@ +taskworker ? "Task" : "") . "Worker #$worker_id 已停止"); + } +} \ No newline at end of file diff --git a/src/ZM/Framework.php b/src/ZM/Framework.php index faf59cfa..2bf72bbb 100644 --- a/src/ZM/Framework.php +++ b/src/ZM/Framework.php @@ -12,7 +12,6 @@ use ZM\Annotation\Swoole\OnSetup; use ZM\Config\ZMConfig; use ZM\ConnectionManager\ManagerGM; use ZM\Console\TermColor; -use ZM\Event\ServerEventHandler; use ZM\Store\LightCache; use ZM\Store\LightCacheInside; use ZM\Store\Lock\SpinLock; @@ -97,6 +96,9 @@ class Framework $add_port = ZMConfig::get("global", "remote_terminal")["status"] ?? false; $this->parseCliArgs(self::$argv, $add_port); + if (!isset($this->server_set["max_wait_time"])) { + $this->server_set["max_wait_time"] = 5; + } $worker = $this->server_set["worker_num"] ?? swoole_cpu_num(); define("ZM_WORKER_NUM", $worker); ZMAtomic::init(); @@ -286,6 +288,7 @@ class Framework self::$loaded_files = get_included_files(); self::$server->start(); zm_atomic("server_is_stopped")->set(1); + Console::log("zhamao-framework is stopped."); } /** @@ -294,13 +297,23 @@ class Framework * @throws ReflectionException */ private function registerServerEvents() { - $all_event_class = ZMConfig::get("global", "server_event_handler_class") ?? []; - if (!in_array(ServerEventHandler::class, $all_event_class)) { - $all_event_class[] = ServerEventHandler::class; - } $event_list = []; + $reader = new AnnotationReader(); + + $all = getAllClasses(FRAMEWORK_ROOT_DIR . "/src/ZM/Event/SwooleEvent/", "ZM\\Event\\SwooleEvent"); + foreach ($all as $v) { + $class = new $v(); + $reflection_class = new ReflectionClass($class); + $anno_class = $reader->getClassAnnotation($reflection_class, SwooleHandler::class); + if ($anno_class !== null) { // 类名形式的注解 + $anno_class->class = $v; + $anno_class->method = "onCall"; + $event_list[strtolower($anno_class->event)] = $anno_class; + } + } + + $all_event_class = ZMConfig::get("global", "server_event_handler_class") ?? []; foreach ($all_event_class as $v) { - $reader = new AnnotationReader(); $reflection_class = new ReflectionClass($v); $methods = $reflection_class->getMethods(ReflectionMethod::IS_PUBLIC); foreach ($methods as $vs) { @@ -338,81 +351,69 @@ class Framework global $terminal_id; $terminal_id = uuidgen(); foreach ($args as $x => $y) { - switch ($x) { - case 'worker-num': - if ($y) { + if ($y) { + switch ($x) { + case 'worker-num': if (intval($y) >= 1 && intval($y) <= 1024) { $this->server_set["worker_num"] = intval($y); } else { - Console::warning("Invalid worker num! Turn to default value (".($this->server_set["worker_num"] ?? swoole_cpu_num()).")"); + Console::warning("Invalid worker num! Turn to default value (" . ($this->server_set["worker_num"] ?? swoole_cpu_num()) . ")"); } - } - break; - case 'task-worker-num': - if ($y) { + break; + case 'task-worker-num': if (intval($y) >= 1 && intval($y) <= 1024) { $this->server_set["task_worker_num"] = intval($y); $this->server_set["task_enable_coroutine"] = true; } else { Console::warning("Invalid worker num! Turn to default value (0)"); } - } - break; - case 'disable-coroutine': - if ($y) { + break; + case 'disable-coroutine': $coroutine_mode = false; - } - break; - case 'debug-mode': - if ($y || ZMConfig::get("global", "debug_mode")) { + break; + case 'debug-mode': $coroutine_mode = false; $terminal_id = null; Console::warning("You are in debug mode, do not use in production!"); - } - break; - case 'daemon': - if ($y) { + break; + case 'daemon': $this->server_set["daemonize"] = 1; Console::$theme = "no-color"; Console::log("已启用守护进程,输出重定向到 " . $this->server_set["log_file"]); $terminal_id = null; - } - break; - case 'disable-console-input': - case 'no-interaction': - if ($y) $terminal_id = null; - break; - case 'log-error': - if ($y) Console::setLevel(0); - break; - case 'log-warning': - if ($y) Console::setLevel(1); - break; - case 'log-info': - if ($y) Console::setLevel(2); - break; - case 'log-verbose': - case 'verbose': - if ($y) Console::setLevel(3); - break; - case 'log-debug': - if ($y) Console::setLevel(4); - break; - case 'log-theme': - if ($y !== null) { + break; + case 'disable-console-input': + case 'no-interaction': + $terminal_id = null; + break; + case 'log-error': + Console::setLevel(0); + break; + case 'log-warning': + Console::setLevel(1); + break; + case 'log-info': + Console::setLevel(2); + break; + case 'log-verbose': + case 'verbose': + Console::setLevel(3); + break; + case 'log-debug': + Console::setLevel(4); + break; + case 'log-theme': Console::$theme = $y; - } - break; - case 'remote-terminal': - if ($y) { + break; + case 'remote-terminal': $add_port = true; - } - break; - case 'show-php-ver': - default: - //Console::info("Calculating ".$x); - //dump($y); - break; + break; + case 'show-php-ver': + default: + //Console::info("Calculating ".$x); + //dump($y); + break; + } } } if ($coroutine_mode) Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL); @@ -508,4 +509,15 @@ class Framework public static function getTtyWidth(): string { return explode(" ", trim(exec("stty size")))[1]; } + + public static function loadFrameworkState() { + if (!file_exists(DataProvider::getDataFolder() . ".state.json")) return []; + $r = json_decode(file_get_contents(DataProvider::getDataFolder() . ".state.json"), true); + if ($r === null) $r = []; + return $r; + } + + public static function saveFrameworkState($data) { + return file_put_contents(DataProvider::getDataFolder() . ".state.json", json_encode($data, 64 | 128 | 256)); + } } diff --git a/src/ZM/Store/LightCache.php b/src/ZM/Store/LightCache.php index 0d7e57a0..064809df 100644 --- a/src/ZM/Store/LightCache.php +++ b/src/ZM/Store/LightCache.php @@ -10,6 +10,8 @@ use ZM\Annotation\Swoole\OnSave; use ZM\Console\Console; use ZM\Event\EventDispatcher; use ZM\Exception\ZMException; +use ZM\Framework; +use ZM\Utils\ProcessManager; class LightCache { @@ -50,6 +52,31 @@ class LightCache } if ($result === false) { self::$last_error = '系统内存不足,申请失败'; + } else { + $obj = Framework::loadFrameworkState(); + foreach(($obj["expiring_light_cache"] ?? []) as $k => $v) { + $value = $v["value"]; + if (is_array($value)) { + $value = json_encode($value, JSON_UNESCAPED_UNICODE); + if (strlen($value) >= self::$config["max_strlen"]) return false; + $data_type = "json"; + } elseif (is_string($value)) { + $data_type = ""; + } elseif (is_int($value)) { + $data_type = "int"; + } elseif (is_bool($value)) { + $data_type = "bool"; + $value = json_encode($value); + } else { + return false; + } + $result = self::$kv_table->set($k, [ + "value" => $value, + "expire" => $v["expire"], + "data_type" => $data_type + ]); + if ($result === false) return false; + } } return $result; } @@ -78,6 +105,19 @@ class LightCache return $r === false ? null : $r - time(); } + /** + * @param string $key + * @return mixed|null + * @throws ZMException + * @since 2.4.3 + */ + public static function getExpireTS(string $key) { + if (self::$kv_table === null) throw new ZMException("not initialized LightCache"); + self::checkExpire($key); + $r = self::$kv_table->get($key, "expire"); + return $r === false ? null : $r; + } + /** * @param string $key * @param string|array|int $value @@ -206,6 +246,10 @@ class LightCache * @throws Exception */ public static function savePersistence() { + if (server()->worker_id !== MAIN_WORKER) { + ProcessManager::sendActionToWorker(MAIN_WORKER, "save_persistence", []); + return; + } $dispatcher = new EventDispatcher(OnSave::class); $dispatcher->dispatchEvents(); @@ -220,6 +264,25 @@ class LightCache } file_put_contents(self::$config["persistence_path"], json_encode($r, 64 | 128 | 256)); } + + $obj = Framework::loadFrameworkState(); + $obj["expiring_light_cache"] = []; + $del = []; + foreach (self::$kv_table as $k => $v) { + if ($v["expire"] <= time() && $v["expire"] >= 0) { + $del[] = $k; + continue; + } elseif ($v["expire"] > time()) { + $obj["expiring_light_cache"][$k] = [ + "expire" => $v["expire"], + "value" => self::parseGet($v) + ]; + } + } + foreach ($del as $v) { + self::unset($v); + } + Framework::saveFrameworkState($obj); Console::verbose("Saved."); } diff --git a/src/ZM/Utils/CoMessage.php b/src/ZM/Utils/CoMessage.php index faee45b6..05c22f50 100644 --- a/src/ZM/Utils/CoMessage.php +++ b/src/ZM/Utils/CoMessage.php @@ -68,7 +68,7 @@ class CoMessage LightCacheInside::set("wait_api", "wait_api", $all); SpinLock::unlock("wait_api"); if ($all[$last]["worker_id"] != server()->worker_id) { - ZMUtil::sendActionToWorker($all[$last]["worker_id"], "resume_ws_message", $all[$last]); + ProcessManager::sendActionToWorker($all[$last]["worker_id"], "resume_ws_message", $all[$last]); } else { Co::resume($all[$last]["coroutine"]); } diff --git a/src/ZM/Utils/ProcessManager.php b/src/ZM/Utils/ProcessManager.php index 3047c1bc..e53f386e 100644 --- a/src/ZM/Utils/ProcessManager.php +++ b/src/ZM/Utils/ProcessManager.php @@ -20,7 +20,7 @@ class ProcessManager $server = server(); switch ($data["action"] ?? '') { case 'add_short_command': - Console::verbose("Adding short command ".$data["data"][0]); + Console::verbose("Adding short command " . $data["data"][0]); $obj = new CQCommand(); $obj->method = quick_reply_closure($data["data"][1]); $obj->match = $data["data"][0]; @@ -111,11 +111,11 @@ class ProcessManager public static function resumeAllWorkerCoroutines() { if (server()->worker_id === -1) { - Console::warning("Cannot call '".__FUNCTION__."' in non-worker process!"); + Console::warning("Cannot call '" . __FUNCTION__ . "' in non-worker process!"); return; } foreach ((LightCacheInside::get("wait_api", "wait_api") ?? []) as $k => $v) { - if (($v["result"] ?? false) === null && isset($v["coroutine"], $v["worker_id"])) { + if (isset($v["coroutine"], $v["worker_id"])) { if (server()->worker_id == $v["worker_id"]) Co::resume($v["coroutine"]); } } diff --git a/src/ZM/Utils/Terminal.php b/src/ZM/Utils/Terminal.php index 28d80783..fee5714d 100644 --- a/src/ZM/Utils/Terminal.php +++ b/src/ZM/Utils/Terminal.php @@ -4,17 +4,20 @@ namespace ZM\Utils; +use Doctrine\Common\Annotations\AnnotationReader; use Exception; -use Psy\Shell; +use ReflectionClass; +use Swoole\Process; use ZM\Annotation\Command\TerminalCommand; use ZM\ConnectionManager\ManagerGM; use ZM\Console\Console; use ZM\Event\EventDispatcher; use ZM\Event\EventManager; -use ZM\Framework; class Terminal { + public static $default_commands = false; + /** * @param string $cmd * @return bool @@ -23,84 +26,22 @@ class Terminal * @throws Exception */ public static function executeCommand(string $cmd) { + if (self::$default_commands === false) { + self::init(); + } $it = explodeMsg($cmd); - switch ($it[0] ?? '') { - case 'help': - $help[] = "exit | q:\t断开远程终端"; - $help[] = "logtest:\t输出所有可以打印的log等级示例消息,用于测试Console"; - $help[] = "call:\t\t用于执行不需要参数的动态函数,比如 `call \Module\Example\Hello hitokoto`"; - $help[] = "level:\t\t设置log等级,例如 `level 0|1|2|3|4`"; - $help[] = "bc:\t\teval执行代码,但输入必须是将代码base64之后的,如 `bc em1faW5mbygn5L2g5aW9Jyk7`"; - $help[] = "stop:\t\t停止服务器"; - $help[] = "reload | r:\t热重启用户编写的模块代码"; - foreach((EventManager::$events[TerminalCommand::class] ?? []) as $v) { - $help[]=$v->command.":\t\t".(empty($v->description) ? "<用户自定义指令>" : $v->description); - } - echo implode("\n", $help) . PHP_EOL; - return true; - case 'logtest': - Console::log(date("[H:i:s]") . " [L] This is normal msg. (0)"); - Console::error("This is error msg. (0)"); - Console::warning("This is warning msg. (1)"); - Console::info("This is info msg. (2)"); - Console::success("This is success msg. (2)"); - Console::verbose("This is verbose msg. (3)"); - Console::debug("This is debug msg. (4)"); - return true; - case 'call': - $class_name = $it[1]; - $function_name = $it[2]; - $class = new $class_name([]); - $r = $class->$function_name(); - if (is_string($r)) Console::success($r); - return true; - case 'psysh': - if (Framework::$argv["disable-coroutine"]) { - (new Shell())->run(); - } else - Console::error("Only \"--disable-coroutine\" mode can use psysh!!!"); - return true; - case 'level': - $level = intval(is_numeric($it[1] ?? 99) ? ($it[1] ?? 99) : 99); - if ($level > 4 || $level < 0) Console::warning("Usage: 'level 0|1|2|3|4'"); - else Console::setLevel($level) || Console::success("Success!!"); - break; - case 'bc': - $code = base64_decode($it[1] ?? '', true); - try { - eval($code); - } catch (Exception $e) { - } - return true; - case 'echo': - Console::info($it[1]); - return true; - case 'color': - Console::log($it[2], $it[1]); - return true; - case 'stop': - ZMUtil::stop(); - return false; - case 'reload': - case 'r': - ZMUtil::reload(); - return false; - case '': - return true; - default: - $dispatcher = new EventDispatcher(TerminalCommand::class); - $dispatcher->setRuleFunction(function ($v) use ($it) { - /** @var TerminalCommand $v */ - return $v->command == $it[0]; - }); - $dispatcher->setReturnFunction(function () { - EventDispatcher::interrupt('none'); - }); - $dispatcher->dispatchEvents($it); - if ($dispatcher->store !== 'none') { - Console::info("Command not found: " . $cmd); - return true; - } + $dispatcher = new EventDispatcher(TerminalCommand::class); + $dispatcher->setRuleFunction(function ($v) use ($it) { + /** @var TerminalCommand $v */ + return $v->command == $it[0] || $v->alias == $it[0]; + }); + $dispatcher->setReturnFunction(function () { + EventDispatcher::interrupt('none'); + }); + $dispatcher->dispatchEvents($it); + if ($dispatcher->store !== 'none') { + Console::info("Command not found: " . $cmd); + return true; } return false; } @@ -119,4 +60,129 @@ class Terminal server()->send($v->getFd(), ">>> "); } } + + public static function init() { + Console::debug("Initializing Terminal..."); + foreach ((EventManager::$events[TerminalCommand::class] ?? []) as $v) { + if ($v->command == "help") { + self::$default_commands = true; + break; + } + } + $class = new Terminal(); + $reader = new AnnotationReader(); + $reflection = new ReflectionClass($class); + foreach ($reflection->getMethods() as $k => $v) { + $r = $reader->getMethodAnnotation($v, TerminalCommand::class); + if ($r !== null) { + Console::debug("adding command " . $r->command); + $r->class = Terminal::class; + $r->method = $v->getName(); + EventManager::addEvent(TerminalCommand::class, $r); + } + } + self::$default_commands = true; + } + + /** + * @TerminalCommand(command="help",alias="h",description="显示帮助菜单") + */ + public function help() { + $help = []; + foreach ((EventManager::$events[TerminalCommand::class] ?? []) as $v) { + /** @var TerminalCommand $v */ + $cmd = $v->command . ($v->alias !== "" ? (" | " . $v->alias) : ""); + $painted = Console::setColor($v->command, "green") . ($v->alias !== "" ? (" | " . Console::setColor($v->alias, "green")) : ""); + $help[] = $painted . ":" . str_pad("", 16 - strlen($cmd) - 1) . ($v->description === "" ? "<无描述>" : $v->description); + } + echo implode("\n", $help) . PHP_EOL; + } + + /** + * @TerminalCommand(command="status",description="显示Swoole Server运行状态(需要安装league/climate组件)") + * @noinspection PhpFullyQualifiedNameUsageInspection + */ + public function status() { + if (!class_exists("\League\CLImate\CLImate")) { + Console::warning("你还没有安装 league/climate 组件,无法使用此功能!"); + return; + } + $climate = new \League\CLImate\CLImate; + $climate->output->addDefault('buffer'); + + $objs = server()->stats(); + $climate->columns($objs); + $obj = $climate->output->get('buffer')->get(); + $climate->output->get("buffer")->clean(); + echo $obj; + } + + /** + * @TerminalCommand(command="logtest",description="测试log的显示等级") + */ + public function testlog() { + Console::log(date("[H:i:s]") . " [L] This is normal msg. (0)"); + Console::error("This is error msg. (0)"); + Console::warning("This is warning msg. (1)"); + Console::info("This is info msg. (2)"); + Console::success("This is success msg. (2)"); + Console::verbose("This is verbose msg. (3)"); + Console::debug("This is debug msg. (4)"); + } + + /** + * @TerminalCommand(command="call",description="用于执行不需要参数的动态函数,比如 `call \Module\Example\Hello hitokoto`") + * @param $it + */ + public function call($it) { + $class_name = $it[1]; + $function_name = $it[2]; + $class = new $class_name([]); + $r = $class->$function_name(); + if (is_string($r)) Console::success($r); + } + + /** + * @TerminalCommand(command="level",description="设置log等级,例如 `level 0|1|2|3|4`") + * @param $it + */ + public function level($it) { + $level = intval(is_numeric($it[1] ?? 99) ? ($it[1] ?? 99) : 99); + if ($level > 4 || $level < 0) Console::warning("Usage: 'level 0|1|2|3|4'"); + else Console::setLevel($level) || Console::success("Success!!"); + } + + /** + * @TerminalCommand(command="bc",description="eval执行代码,但输入必须是将代码base64之后的,如 `bc em1faW5mbygn5L2g5aW9Jyk7`") + * @param $it + */ + public function bc($it) { + $code = base64_decode($it[1] ?? '', true); + try { + eval($code); + } catch (Exception $e) { + } + } + + /** + * @TerminalCommand(command="echo",description="输出内容,用法:`echo hello`") + * @param $it + */ + public function echoI($it) { + Console::info($it[1]); + } + + /** + * @TerminalCommand(command="stop",description="停止框架") + */ + public function stop() { + Process::kill(server()->master_pid, SIGTERM); + } + + /** + * @TerminalCommand(command="reload",alias="r",description="重启框架(重载用户代码)") + */ + public function reload() { + Process::kill(server()->master_pid, SIGUSR1); + } } diff --git a/src/ZM/Utils/ZMUtil.php b/src/ZM/Utils/ZMUtil.php index 30057fd6..df28c0ca 100644 --- a/src/ZM/Utils/ZMUtil.php +++ b/src/ZM/Utils/ZMUtil.php @@ -27,15 +27,13 @@ class ZMUtil Process::kill(zm_atomic("_#worker_" . $i)->get(), SIGUSR1); } server()->shutdown(); - server()->stop(); } /** * @throws Exception */ public static function reload() { - zm_atomic("_int_is_reload")->set(1); - system("kill -INT " . intval(server()->master_pid)); + Process::kill(server()->master_pid, SIGUSR1); } public static function getModInstance($class) { @@ -47,11 +45,6 @@ class ZMUtil } } - public static function sendActionToWorker($target_id, $action, $data) { - Console::verbose($action . ": " . $data); - server()->sendMessage(json_encode(["action" => $action, "data" => $data]), $target_id); - } - /** * 在工作进程中返回可以通过reload重新加载的php文件列表 * @return string[]|string[][] diff --git a/src/ZM/global_defines.php b/src/ZM/global_defines.php index f65a9fb4..dd96acff 100644 --- a/src/ZM/global_defines.php +++ b/src/ZM/global_defines.php @@ -7,6 +7,7 @@ define("ZM_START_TIME", microtime(true)); define("ZM_DATA", ZMConfig::get("global", "zm_data")); define("APP_VERSION", LOAD_MODE == 1 ? (json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true)["version"] ?? "unknown") : "unknown"); define("CRASH_DIR", ZMConfig::get("global", "crash_dir")); +define("MAIN_WORKER", ZMConfig::get("global", "worker_cache")["worker"] ?? 0); @mkdir(ZM_DATA); @mkdir(CRASH_DIR); @@ -19,3 +20,9 @@ define("ZM_MATCH_SECOND", 3); define("ZM_BREAKPOINT", 'if(\ZM\Framework::$argv["debug-mode"]) extract(\Psy\debug(get_defined_vars(), isset($this) ? $this : @get_called_class()));'); define("BP", ZM_BREAKPOINT); define("ZM_DEFAULT_FETCH_MODE", 4); + +define("ZM_LOG_ERROR", 0); +define("ZM_LOG_WARNING", 1); +define("ZM_LOG_INFO", 2); +define("ZM_LOG_VERBOSE", 3); +define("ZM_LOG_DEBUG", 4); diff --git a/src/ZM/global_functions.php b/src/ZM/global_functions.php index 83662aa9..ae1e54ee 100644 --- a/src/ZM/global_functions.php +++ b/src/ZM/global_functions.php @@ -285,6 +285,10 @@ function zm_timer_tick($ms, callable $callable) { } } +function zm_go(callable $callable) { + call_with_catch($callable); +} + function zm_data_hash($v): string { return md5($v["user_id"] . "^" . $v["self_id"] . "^" . $v["message_type"] . "^" . ($v[$v["message_type"] . "_id"] ?? $v["user_id"])); }