From 0ff4e52ed323e5932dd62af3e26cd52f2d31131d Mon Sep 17 00:00:00 2001 From: jerry Date: Mon, 22 Mar 2021 07:44:11 +0800 Subject: [PATCH] tmp connect --- composer.json | 4 +- config/global.php | 27 ++-- docs/advanced/inside-class.md | 41 ++++++ docs/component/global-functions.md | 60 ++++++++- docs/component/zmutil.md | 44 +++++++ docs/event/framework-annotations.md | 24 ++++ docs/update/v2.md | 14 +++ src/Module/Example/Hello.php | 41 ++++++ src/ZM/API/CQAPI.php | 24 ++-- src/ZM/API/TuringAPI.php | 118 ++++++++++++++++++ src/ZM/Annotation/Command/TerminalCommand.php | 27 ++++ src/ZM/Command/DaemonStopCommand.php | 2 +- src/ZM/Command/RunServerCommand.php | 1 + src/ZM/ConsoleApplication.php | 4 +- src/ZM/Entity/MatchResult.php | 17 +++ src/ZM/Event/EventDispatcher.php | 2 + src/ZM/Event/ServerEventHandler.php | 90 ++++--------- src/ZM/Framework.php | 96 +++++++++++++- src/ZM/Module/QQBot.php | 54 ++------ src/ZM/Store/LightCache.php | 17 +-- src/ZM/Store/LightCacheInside.php | 6 +- src/ZM/Store/ZMAtomic.php | 3 + src/ZM/Utils/MessageUtil.php | 84 +++++++++++++ src/ZM/Utils/ProcessManager.php | 110 ++++++++++++++-- src/ZM/Utils/Terminal.php | 62 +++++++-- src/ZM/Utils/ZMUtil.php | 52 ++++---- 26 files changed, 812 insertions(+), 212 deletions(-) create mode 100644 src/ZM/API/TuringAPI.php create mode 100644 src/ZM/Annotation/Command/TerminalCommand.php create mode 100644 src/ZM/Entity/MatchResult.php diff --git a/composer.json b/composer.json index 827632a4..b1658cbf 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,9 @@ }, "autoload": { "psr-4": { - "ZM\\": "src/ZM" + "ZM\\": "src/ZM", + "Module\\": "src/Module", + "Custom\\": "src/Custom" }, "files": [ "src/ZM/global_functions.php" diff --git a/config/global.php b/config/global.php index 5a55d125..8237a76f 100644 --- a/config/global.php +++ b/config/global.php @@ -113,21 +113,18 @@ $config['server_event_handler_class'] = [ ]; /** 服务器启用的外部第三方和内部插件 */ -$config['modules'] = [ - 'onebot' => [ // 机器人解析模块,关闭后无法使用如@CQCommand等注解 - 'status' => true, - 'single_bot_mode' => false - ], - 'http_proxy_server' => [ // 一个内置的简单HTTP代理服务器,目前还没有认证功能,预计2.4.0版本完成 - 'status' => false, - 'host' => '0.0.0.0', - 'port' => 8083, - 'swoole_set_override' => [ - 'backlog' => 128, - 'buffer_output_size' => 1024 * 1024 * 128, - 'socket_buffer_size' => 1024 * 1024 * 1 - ] - ], +$config['modules']['onebot'] = [ + // 机器人解析模块,关闭后无法使用如@CQCommand等注解 + 'status' => true, + 'single_bot_mode' => false +]; + +$config['modules']['remote_terminal'] = [ + // 一个远程简易终端,使用nc直接连接即可,但是不建议开放host为0.0.0.0(远程连接) + 'status' => false, + 'host' => '127.0.0.1', + 'port' => 20002, + 'token' => '' ]; return $config; diff --git a/docs/advanced/inside-class.md b/docs/advanced/inside-class.md index b71a30b9..fb300250 100644 --- a/docs/advanced/inside-class.md +++ b/docs/advanced/inside-class.md @@ -27,3 +27,44 @@ TODO:先放一放。 ``` +## ZM\Entity\MatchObject + +此类是调用方法 `MessageUtil::matchCommand()` 返回的对象体,含有匹配成功与否和匹配到的注解相关的信息。 + +### 属性 + +- `$match`:`bool` 类型,返回匹配是否成功 +- `$object`:`CQCommand` 注解类,如果匹配成功则返回对应的 `@CQCommand` 信息 +- `match`:`array` 类型,如果匹配成功则返回匹配到的参数 + +```php +// 假设我有一个注解事件 @CQCommand(match="你好"),绑定的函数是 \Module\Example\Hello 下的 hello123() + +$obj = MessageUtil::matchCommand("你好 我叫顺溜 我今年二十八", ctx()->getData()); +/* 以下是返回信息,仅供参考 +$obj->match ==> true +$obj->object ==> \ZM\Annotation\CQ\CQCommand: ( + match: "你好", + pattern: "", + regex: "", + start_with: "", + end_with: "", + keyword: "", + alias: [], + message_type: "", + user_id: 0, + group_id: 0, + discuss_id: 0, + level: 20, + method: "hello123", + class: \Module\Example\Hello::class +) +$obj->match ==> [ + "我叫顺溜", + "我今年二十八" +] +*/ +``` + + + diff --git a/docs/component/global-functions.md b/docs/component/global-functions.md index f4e6e37e..897fbccc 100644 --- a/docs/component/global-functions.md +++ b/docs/component/global-functions.md @@ -134,10 +134,6 @@ set_coroutine_params(["data" => [ 别名:`context()`,获取当前协程的上下文,见 [上下文](/component/context/)。 -## zm_debug() - -同 `Console::debug($msg)`。 - ## zm_sleep() 协程版 `sleep()` 函数。 @@ -255,4 +251,58 @@ bot()->sendPrivateMsg(123456, "你好啊!!"); 定义:`getAllFdByConnectType(string $type = 'default'): array` -当 `$type` 为 `qq` 时,则返回所有 OneBot 机器人接入的 WebSocket 连接号。 \ No newline at end of file +当 `$type` 为 `qq` 时,则返回所有 OneBot 机器人接入的 WebSocket 连接号。 + +## zm_dump() + +更漂亮地输出变量值,可替代 `var_dump()`。 + +```php +class Pass { + public $foo = 123; + public $bar = ["a", "b"]; +} +$pass = new Pass(); +$pass->obj = true; +zm_dump($pass); +``` + +image-20210321193956832 + +## zm_config() + +同 `ZMConfig::get()`。 + +定义:`zm_config($name, $key = null)`。 + +有关 ZMConfig 模块的说明,见 [指南 - 基本配置](/guide/basic-config/)。 + +```php +zm_config("global"); //等同于 ZMConfig::get("global"); +zm_config("global", "swoole"); //等同于 ZMConfig::get("global", "swoole"); +``` + +## zm_info() + +同 `Console::info($msg)`。 + +## zm_debug() + +同 `Console::debug($msg)`。 + +## zm_warning() + +同 `Console::warning($msg)`。 + +## zm_success() + +同 `Console::success($msg)`。 + +## zm_error() + +同 `Console::error($msg)`。 + +## zm_verbose() + +同 `Console::verbose($msg)`。 + diff --git a/docs/component/zmutil.md b/docs/component/zmutil.md index 1ee6cb78..68f0d795 100644 --- a/docs/component/zmutil.md +++ b/docs/component/zmutil.md @@ -21,3 +21,47 @@ class ASD{ ZMUtil::getModInstance(ASD::class)->test = 5; ``` +## ZMUtil::getReloadableFiles() + +返回可通过热重启(reload)来重新加载的 php 文件列表。 + +以下是示例模块下的例子(直接拉取最新的框架源码并运行框架后获取的)。 + +```php +array:31 [ + 94 => "src/ZM/Context/Context.php" + 95 => "src/ZM/Context/ContextInterface.php" + 96 => "src/ZM/Annotation/AnnotationParser.php" + 97 => "src/Custom/Annotation/Example.php" + 98 => "src/ZM/Annotation/Interfaces/CustomAnnotation.php" + 99 => "src/Module/Example/Hello.php" + 100 => "src/ZM/Annotation/Swoole/OnStart.php" + 101 => "src/ZM/Annotation/CQ/CQCommand.php" + 102 => "src/ZM/Annotation/Interfaces/Level.php" + 103 => "src/ZM/Annotation/Command/TerminalCommand.php" + 104 => "src/ZM/Annotation/Http/RequestMapping.php" + 105 => "src/ZM/Annotation/Http/RequestMethod.php" + 106 => "src/ZM/Annotation/Http/Middleware.php" + 107 => "src/ZM/Annotation/Interfaces/ErgodicAnnotation.php" + 108 => "src/ZM/Annotation/Swoole/OnOpenEvent.php" + 109 => "src/ZM/Annotation/Swoole/OnSwooleEventBase.php" + 110 => "src/ZM/Annotation/Interfaces/Rule.php" + 111 => "src/ZM/Annotation/Swoole/OnCloseEvent.php" + 112 => "src/ZM/Annotation/Swoole/OnRequestEvent.php" + 113 => "src/ZM/Http/RouteManager.php" + 114 => "vendor/symfony/routing/RouteCollection.php" + 115 => "vendor/symfony/routing/Route.php" + 116 => "src/Module/Middleware/TimerMiddleware.php" + 117 => "src/ZM/Http/MiddlewareInterface.php" + 118 => "src/ZM/Annotation/Http/MiddlewareClass.php" + 119 => "src/ZM/Annotation/Http/HandleBefore.php" + 120 => "src/ZM/Annotation/Http/HandleAfter.php" + 121 => "src/ZM/Annotation/Http/HandleException.php" + 122 => "src/ZM/Event/EventManager.php" + 123 => "src/ZM/Annotation/Swoole/OnSwooleEvent.php" + 124 => "src/ZM/Event/EventDispatcher.php" +] +``` + +> 为什么不能重载所有文件?因为框架是多进程模型,而重载相当于只重新启动了一次 Worker 进程,Manager 和 Master 进程未重启,所以被 Manager、Master 进程已经加载的 PHP 文件无法使用 reload 命令重新加载。详见 [进阶 - 进程间隔离](/advanced/multi-process/#_5)。 + diff --git a/docs/event/framework-annotations.md b/docs/event/framework-annotations.md index 874d227d..db8e3bcc 100644 --- a/docs/event/framework-annotations.md +++ b/docs/event/framework-annotations.md @@ -262,6 +262,27 @@ 无。 +## TerminalCommand() + +添加一个远程终端的自定义命令。(2.4.0 版本起可用) + +### 属性 + +| 类型 | 值 | +| ---------- | --------------------------------------- | +| 名称 | `@TerminalCommand` | +| 触发前提 | 连接到远程终端可触发 | +| 命名空间 | `ZM\Annotation\Command\TerminalCommand` | +| 适用位置 | 方法 | +| 返回值处理 | 无 | + +### 注解参数 + +| 参数名称 | 参数范围 | 默认 | +| ----------- | ------------------------------ | ---- | +| command | `string`,**必填**,命令字符串 | | +| description | `string`,要显示的帮助文本 | 空 | + ## 示例1(机器人连接框架后输出信息) ```php @@ -406,3 +427,6 @@ public function onCrawl() { } ``` +## 示例6(创建一个远程终端命令并调试框架) + +> 开个坑,以后填。(__填坑标记__) \ No newline at end of file diff --git a/docs/update/v2.md b/docs/update/v2.md index c1f12e2f..3756df6e 100644 --- a/docs/update/v2.md +++ b/docs/update/v2.md @@ -1,5 +1,19 @@ # 更新日志(v2 版本) +## v2.4.0(390) + +> 更新时间:2021.3.18 + +- 新增:全局方法(`zm_dump()`,`zm_error()`,`zm_warning()`,`zm_info()`,`zm_success()`,`zm_verbose()`,`zm_debug()`,`zm_config()`) + +- 修复:部分内部存储存在的内存泄漏问题 +- 新增:remote_terminal(远程终端)内置模块,可以用 nc 直接远程连接框架(弥补原先删除掉的终端输入) +- 新增:`DataProvider::getReloadableFiles()` 返回可通过热重启(reload)来重新加载的 php 文件列表(多用于调试) +- 兼容性变更:**要求最低 Swoole 版本为 4.5.0** +- 优化:reload 和 stop 重载和停止框架的逻辑,防止卡死 +- 新增:远程终端命令支持自定义添加(`@TerminalCommand` 注解) +- + ## v2.3.2 > 更新时间:2021.3.18 diff --git a/src/Module/Example/Hello.php b/src/Module/Example/Hello.php index 5abf3a96..52f3aa9e 100644 --- a/src/Module/Example/Hello.php +++ b/src/Module/Example/Hello.php @@ -2,10 +2,16 @@ namespace Module\Example; +use ZM\Annotation\Command\TerminalCommand; +use ZM\Annotation\CQ\CQBefore; +use ZM\Annotation\CQ\CQMessage; use ZM\Annotation\Http\Middleware; use ZM\Annotation\Swoole\OnCloseEvent; use ZM\Annotation\Swoole\OnOpenEvent; use ZM\Annotation\Swoole\OnRequestEvent; +use ZM\Annotation\Swoole\OnStart; +use ZM\API\CQ; +use ZM\API\TuringAPI; use ZM\ConnectionManager\ConnectionObject; use ZM\Console\Console; use ZM\Annotation\CQ\CQCommand; @@ -13,6 +19,7 @@ use ZM\Annotation\Http\RequestMapping; use ZM\Event\EventDispatcher; use ZM\Exception\InterruptException; use ZM\Requests\ZMRequest; +use ZM\Utils\MessageUtil; use ZM\Utils\ZMUtil; /** @@ -22,6 +29,12 @@ use ZM\Utils\ZMUtil; */ class Hello { + /** + * @OnStart() + */ + public function onStart() { + } + /* * 默认的图片监听路由对应目录,如需要使用可取消下面的注释,把上面的 /* 换成 /** * @OnStart(-1) @@ -67,6 +80,34 @@ class Hello return $obj["hitokoto"] . "\n----「" . $obj["from"] . "」"; } + /** + * @CQCommand(start_with="机器人",end_with="机器人",message_type="group") + * @CQMessage(message_type="private",level=1) + */ + public function turingAPI() { + $user_id = ctx()->getUserId(); + $api = "83513e3d316f44de9c952cda4c9aed30"; // 请在这里填入你的图灵机器人的apikey + if ($api === "") return false; //如果没有填入apikey则此功能关闭 + if (($this->_running_annotation ?? null) instanceof CQCommand) { + $msg = ctx()->getFullArg("我在!有什么事吗?"); + } else { + $msg = ctx()->getMessage(); + } + zm_dump($msg); + return TuringAPI::getTuringMsg($msg, $user_id, $api); + } + + /** + * @CQBefore("message") + */ + public function changeAt() { + if (MessageUtil::isAtMe(ctx()->getMessage(), ctx()->getRobotId())) { + $msg = str_replace(CQ::at(ctx()->getRobotId()), "", ctx()->getMessage()); + ctx()->setMessage("机器人 ".$msg); + } + return true; + } + /** * 一个简单随机数的功能demo * 问法1:随机数 1 20 diff --git a/src/ZM/API/CQAPI.php b/src/ZM/API/CQAPI.php index 68819329..727e88cc 100644 --- a/src/ZM/API/CQAPI.php +++ b/src/ZM/API/CQAPI.php @@ -29,25 +29,15 @@ trait CQAPI public function processWebsocketAPI($connection, $reply, $function = false) { $api_id = ZMAtomic::get("wait_msg_id")->add(1); $reply["echo"] = $api_id; - SpinLock::lock("wait_api"); - $r = LightCacheInside::get("wait_api", "wait_api"); - $r[$api_id] = [ - "data" => $reply, - "time" => microtime(true), - "self_id" => $connection->getOption("connect_id"), - "echo" => $api_id - ]; - LightCacheInside::set("wait_api", "wait_api", $r); - SpinLock::unlock("wait_api"); if (server()->push($connection->getFd(), json_encode($reply))) { if ($function === true) { - return CoMessage::yieldByWS($r[$api_id], ["echo"], 60); - } else { - SpinLock::lock("wait_api"); - $r = LightCacheInside::get("wait_api", "wait_api"); - unset($r[$api_id]); - LightCacheInside::set("wait_api", "wait_api", $r); - SpinLock::unlock("wait_api"); + $obj = [ + "data" => $reply, + "time" => microtime(true), + "self_id" => $connection->getOption("connect_id"), + "echo" => $api_id + ]; + return CoMessage::yieldByWS($obj, ["echo"], 60); } return true; } else { diff --git a/src/ZM/API/TuringAPI.php b/src/ZM/API/TuringAPI.php new file mode 100644 index 00000000..ab5de163 --- /dev/null +++ b/src/ZM/API/TuringAPI.php @@ -0,0 +1,118 @@ + 0, + "userInfo" => [ + "apiKey" => $api, + "userId" => $user_id + ] + ]; + if ($msg != "") { + $content["perception"]["inputText"]["text"] = $msg; + } + $msg = trim($msg); + if (mb_strlen($msg) < 1 && !isset($url)) return "请说出你想说的话"; + if (isset($url)) { + $content["perception"]["inputImage"]["url"] = $url; + $content["reqType"] = 1; + } + if (!isset($content["perception"])) return "请说出你想说的话"; + $client = new Client("openapi.tuling123.com", 80); + $client->setHeaders(["Content-type" => "application/json"]); + $client->post("/openapi/api/v2", json_encode($content, JSON_UNESCAPED_UNICODE)); + $api_return = json_decode($client->body, true); + if (!isset($api_return["intent"]["code"])) return "XD 哎呀,我脑子突然短路了,请稍后再问我吧!"; + $status = self::getResultStatus($api_return); + if ($status !== true) { + if ($status == "err:输入文本内容超长(上限150)") return "你的话太多了!!!"; + if ($api_return["intent"]["code"] == 4003) { + return "哎呀,我刚才有点走神了,可能忘记你说什么了,可以重说一遍吗"; + } + Console::error("图灵机器人发送错误!\n错误原始内容:" . $origin . "\n来自:" . $user_id . "\n错误信息:" . $status); + //echo json_encode($r, 128|256); + return "哎呀,我刚才有点走神了,要不一会儿换一种问题试试?"; + } + $result = $api_return["results"]; + //Console::info(Console::setColor(json_encode($result, 128 | 256), "green")); + $final = ""; + foreach ($result as $k => $v) { + switch ($v["resultType"]) { + case "url": + $final .= "\n" . $v["values"]["url"]; + break; + case "text": + $final .= "\n" . $v["values"]["text"]; + break; + case "image": + $final .= "\n" . CQ::image($v["values"]["image"]); + break; + } + } + return trim($final); + } + + public static function getResultStatus($r) { + switch ($r["intent"]["code"]) { + case 5000: + return "err:无解析结果"; + case 4000: + case 6000: + return "err:暂不支持该功能"; + case 4001: + return "err:加密方式错误"; + case 4005: + case 4002: + return "err:无功能权限"; + case 4003: + return "err:该apikey没有可用请求次数"; + case 4007: + return "err:apikey不合法"; + case 4100: + return "err:userid获取失败"; + case 4200: + return "err:上传格式错误"; + case 4300: + return "err:批量操作超过限制"; + case 4400: + return "err:没有上传合法userid"; + case 4500: + return "err:userid申请个数超过限制"; + case 4600: + return "err:输入内容为空"; + case 4602: + return "err:输入文本内容超长(上限150)"; + case 7002: + return "err:上传信息失败"; + case 8008: + return "err:服务器错误"; + default: + return true; + } + } +} \ No newline at end of file diff --git a/src/ZM/Annotation/Command/TerminalCommand.php b/src/ZM/Annotation/Command/TerminalCommand.php new file mode 100644 index 00000000..30034cb8 --- /dev/null +++ b/src/ZM/Annotation/Command/TerminalCommand.php @@ -0,0 +1,27 @@ +daemon_file["pid"])); + system("kill -INT " . intval($this->daemon_file["pid"])); unlink(DataProvider::getWorkingDir() . "/.daemon_pid"); $output->writeln("成功停止!"); return Command::SUCCESS; diff --git a/src/ZM/Command/RunServerCommand.php b/src/ZM/Command/RunServerCommand.php index c5a93d6d..6d4b77c2 100644 --- a/src/ZM/Command/RunServerCommand.php +++ b/src/ZM/Command/RunServerCommand.php @@ -23,6 +23,7 @@ class RunServerCommand extends Command new InputOption("log-error", null, null, "调整消息等级到error (log-level=0)"), new InputOption("log-theme", null, InputOption::VALUE_REQUIRED, "改变终端的主题配色"), new InputOption("disable-console-input", null, null, "禁止终端输入内容 (废弃)"), + new InputOption("remote-terminal", null, null, "启用远程终端,配置使用global.php中的"), new InputOption("disable-coroutine", null, null, "关闭协程Hook"), new InputOption("daemon", null, null, "以守护进程的方式运行框架"), new InputOption("watch", null, null, "监听 src/ 目录的文件变化并热更新"), diff --git a/src/ZM/ConsoleApplication.php b/src/ZM/ConsoleApplication.php index c0cc583b..8df87e5f 100644 --- a/src/ZM/ConsoleApplication.php +++ b/src/ZM/ConsoleApplication.php @@ -18,7 +18,7 @@ use ZM\Utils\DataProvider; class ConsoleApplication extends Application { - const VERSION_ID = 389; + const VERSION_ID = 390; const VERSION = "2.3.2"; public function __construct(string $name = 'UNKNOWN') { @@ -100,7 +100,7 @@ class ConsoleApplication extends Application private function selfCheck(): bool { if (!extension_loaded("swoole")) die("Can not find swoole extension.\nSee: https://github.com/zhamao-robot/zhamao-framework/issues/19\n"); - if (version_compare(SWOOLE_VERSION, "4.4.13") == -1) die("You must install swoole version >= 4.4.13 !"); + if (version_compare(SWOOLE_VERSION, "4.5.0") == -1) die("You must install swoole version >= 4.5.0 !"); if (version_compare(PHP_VERSION, "7.2") == -1) die("PHP >= 7.2 required."); return true; } diff --git a/src/ZM/Entity/MatchResult.php b/src/ZM/Entity/MatchResult.php new file mode 100644 index 00000000..d345f797 --- /dev/null +++ b/src/ZM/Entity/MatchResult.php @@ -0,0 +1,17 @@ +_running_annotation = $v; if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . "::" . $q_f . " ..."); $this->store = $q_o->$q_f(...$params); } catch (Exception $e) { @@ -188,6 +189,7 @@ class EventDispatcher return false; } else { $q_o = ZMUtil::getModInstance($q_c); + $q_o->_running_annotation = $v; if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . "::" . $q_f . " ..."); $this->store = $q_o->$q_f(...$params); $this->status = self::STATUS_NORMAL; diff --git a/src/ZM/Event/ServerEventHandler.php b/src/ZM/Event/ServerEventHandler.php index 1167e61c..d0d766a1 100644 --- a/src/ZM/Event/ServerEventHandler.php +++ b/src/ZM/Event/ServerEventHandler.php @@ -17,13 +17,13 @@ use Swoole\Database\PDOConfig; use Swoole\Database\PDOPool; use Swoole\Event; use Swoole\Process; +use Swoole\Timer; use Throwable; use ZM\Annotation\AnnotationParser; use ZM\Annotation\Http\RequestMapping; use ZM\Annotation\Swoole\OnCloseEvent; use ZM\Annotation\Swoole\OnMessageEvent; use ZM\Annotation\Swoole\OnOpenEvent; -use ZM\Annotation\Swoole\OnPipeMessageEvent; use ZM\Annotation\Swoole\OnRequestEvent; use ZM\Annotation\Swoole\OnStart; use ZM\Annotation\Swoole\OnSwooleEvent; @@ -49,9 +49,9 @@ use ZM\Store\LightCache; use ZM\Store\LightCacheInside; use ZM\Store\MySQL\SqlPoolStorage; use ZM\Store\Redis\ZMRedisPool; -use ZM\Store\WorkerCache; use ZM\Utils\DataProvider; use ZM\Utils\HttpUtil; +use ZM\Utils\ProcessManager; use ZM\Utils\ZMUtil; class ServerEventHandler @@ -83,7 +83,7 @@ class ServerEventHandler Process::signal(SIGINT, function () use ($r) { if (zm_atomic("_int_is_reload")->get() === 1) { zm_atomic("_int_is_reload")->set(0); - ZMUtil::reload(); + \server()->reload(); } else { echo "\r"; Console::warning("Server interrupted(SIGINT) on Master."); @@ -133,17 +133,25 @@ class ServerEventHandler if ($worker_id == (ZMConfig::get("worker_cache")["worker"] ?? 0)) { LightCache::savePersistence(); } - Console::debug(($server->taskworker ? "Task" : "") . "Worker #$worker_id 已停止"); + 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) { + zm_atomic("_#worker_".$worker_id)->set($server->worker_pid); //if (ZMBuf::atomic("stop_signal")->get() != 0) return; + Process::signal(SIGUSR1, function() use ($worker_id){ + Timer::clearAll(); + ProcessManager::resumeAllWorkerCoroutines(); + }); 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); }); @@ -472,7 +480,6 @@ class ServerEventHandler Console::error("Uncaught " . get_class($e) . " when calling \"open\": " . $error_msg); Console::trace(); } - //EventHandler::callSwooleEvent("open", $server, $request); } /** @@ -528,70 +535,27 @@ class ServerEventHandler * @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); - switch ($data["action"] ?? '') { - case "resume_ws_message": - $obj = $data["data"]; - Co::resume($obj["coroutine"]); - break; - case "getWorkerCache": - $r = WorkerCache::get($data["key"]); - $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r]; - $server->sendMessage(json_encode($action, 256), $src_worker_id); - break; - case "setWorkerCache": - $r = WorkerCache::set($data["key"], $data["value"]); - $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r]; - $server->sendMessage(json_encode($action, 256), $src_worker_id); - break; - case "unsetWorkerCache": - $r = WorkerCache::unset($data["key"]); - $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r]; - $server->sendMessage(json_encode($action, 256), $src_worker_id); - break; - case "hasKeyWorkerCache": - $r = WorkerCache::hasKey($data["key"], $data["subkey"]); - $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r]; - $server->sendMessage(json_encode($action, 256), $src_worker_id); - break; - case "asyncAddWorkerCache": - WorkerCache::add($data["key"], $data["value"], true); - break; - case "asyncSubWorkerCache": - WorkerCache::sub($data["key"], $data["value"], true); - break; - case "asyncSetWorkerCache": - WorkerCache::set($data["key"], $data["value"], true); - break; - case "asyncUnsetWorkerCache": - WorkerCache::unset($data["key"], true); - break; - case "addWorkerCache": - $r = WorkerCache::add($data["key"], $data["value"]); - $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r]; - $server->sendMessage(json_encode($action, 256), $src_worker_id); - break; - case "subWorkerCache": - $r = WorkerCache::sub($data["key"], $data["value"]); - $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r]; - $server->sendMessage(json_encode($action, 256), $src_worker_id); - break; - case "returnWorkerCache": - WorkerCache::$transfer[$data["cid"]] = $data["value"]; - zm_resume($data["cid"]); - break; - default: - $dispatcher = new EventDispatcher(OnPipeMessageEvent::class); - $dispatcher->setRuleFunction(function (OnPipeMessageEvent $v) use ($data) { - return $v->action == $data["action"]; - }); - $dispatcher->dispatchEvents($data); - break; + 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"); } /** diff --git a/src/ZM/Framework.php b/src/ZM/Framework.php index 0e966a45..1b349f37 100644 --- a/src/ZM/Framework.php +++ b/src/ZM/Framework.php @@ -5,7 +5,9 @@ namespace ZM; use Doctrine\Common\Annotations\AnnotationReader; +use Error; use Exception; +use Swoole\Server\Port; use ZM\Annotation\Swoole\OnSetup; use ZM\Config\ZMConfig; use ZM\ConnectionManager\ManagerGM; @@ -23,6 +25,7 @@ use Swoole\Runtime; use Swoole\WebSocket\Server; use ZM\Annotation\Swoole\SwooleHandler; use ZM\Console\Console; +use ZM\Utils\Terminal; use ZM\Utils\ZMUtil; class Framework @@ -35,11 +38,16 @@ class Framework * @var Server */ public static $server; + /** + * @var string[] + */ + public static $loaded_files = []; /** * @var array|bool|mixed|null */ private $server_set; + /** @noinspection PhpUnusedParameterInspection */ public function __construct($args = []) { $tty_width = $this->getTtyWidth(); @@ -54,7 +62,6 @@ class Framework //定义常量 include_once "global_defines.php"; - ZMAtomic::init(); try { $sw = ZMConfig::get("global"); if (!is_dir($sw["zm_data"])) mkdir($sw["zm_data"]); @@ -82,11 +89,17 @@ class Framework ($o = ZMConfig::get("console_color")) === false ? [] : $o ); + $worker = ZMConfig::get("global", "swoole")["worker_num"] ?? swoole_cpu_num(); + define("ZM_WORKER_NUM", $worker); + ZMAtomic::init(); $timezone = ZMConfig::get("global", "timezone") ?? "Asia/Shanghai"; date_default_timezone_set($timezone); $this->server_set = ZMConfig::get("global", "swoole"); - $this->parseCliArgs(self::$argv); + $this->server_set["log_level"] = SWOOLE_LOG_DEBUG; + $add_port = ZMConfig::get("global", "modules")["remote_terminal"]["status"] ?? false; + + $this->parseCliArgs(self::$argv, $add_port); // 打印初始信息 $out["listen"] = ZMConfig::get("global", "host") . ":" . ZMConfig::get("global", "port"); @@ -114,12 +127,82 @@ class Framework $out["php_version"] = PHP_VERSION; $out["swoole_version"] = SWOOLE_VERSION; } + if ($add_port) { + $conf = ZMConfig::get("global", "modules")["remote_terminal"]; + $out["terminal"] = $conf["host"] . ":" . $conf["port"]; + } $out["working_dir"] = DataProvider::getWorkingDir(); self::printProps($out, $tty_width, $args["log-theme"] === null); self::$server = new Server(ZMConfig::get("global", "host"), ZMConfig::get("global", "port")); + if ($add_port) { + $welcome_msg = Console::setColor("Welcome! You can use `help` for usage.", "green"); + /** @var Port $port */ + $port = self::$server->listen("127.0.0.1", 20002, SWOOLE_SOCK_TCP); + $port->set([ + 'open_http_protocol' => false + ]); + $port->on('connect', function (?\Swoole\Server $serv, $fd) use ($port, $welcome_msg) { + ManagerGM::pushConnect($fd, "terminal"); + $serv->send($fd, file_get_contents(working_dir() . "/config/motd.txt")); + if (!empty(ZMConfig::get("global", "modules")["remote_terminal"]["token"] ?? '')) { + $serv->send($fd, "Please input token: "); + } else { + $serv->send($fd, $welcome_msg . "\n>>> "); + } + }); + + $port->on('receive', function ($serv, $fd, $reactor_id, $data) use ($welcome_msg) { + ob_start(); + try { + $arr = LightCacheInside::get("light_array", "input_token") ?? []; + if (empty($arr[$fd] ?? '')) { + if (ZMConfig::get("global", "modules")["remote_terminal"]["token"] != '') { + $token = trim($data); + if ($token === ZMConfig::get("global", "modules")["remote_terminal"]["token"]) { + SpinLock::transaction("input_token", function () use ($fd, $token) { + $arr = LightCacheInside::get("light_array", "input_token"); + $arr[$fd] = $token; + LightCacheInside::set("light_array", "input_token", $arr); + }); + $serv->send($fd, Console::setColor("Auth success!!\n", "green")); + $serv->send($fd, $welcome_msg . "\n>>> "); + } else { + $serv->send($fd, Console::setColor("Auth failed!!\n", "red")); + $serv->close($fd); + } + return; + } + } + if (trim($data) == "exit" || trim($data) == "q") { + $serv->send($fd, Console::setColor("Bye!\n", "blue")); + $serv->close($fd); + return; + } + Terminal::executeCommand(trim($data)); + } 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(); + } + + $r = ob_get_clean(); + if (!empty($r)) $serv->send($fd, $r); + if (!in_array(trim($data), ['r', 'reload', 'stop'])) $serv->send($fd, ">>> "); + }); + + $port->on('close', function ($serv, $fd) { + ManagerGM::popConnect($fd); + //echo "Client: Close.\n"; + }); + } + self::$server->set($this->server_set); Console::setServer(self::$server); self::printMotd($tty_width); @@ -195,9 +278,9 @@ class Framework } public function start() { + self::$loaded_files = get_included_files(); self::$server->start(); zm_atomic("server_is_stopped")->set(1); - Console::setLevel(0); } /** @@ -243,9 +326,9 @@ class Framework /** * 解析命令行的 $argv 参数们 * @param $args - * @throws Exception + * @param $add_port */ - private function parseCliArgs($args) { + private function parseCliArgs($args, &$add_port) { $coroutine_mode = true; global $terminal_id; $terminal_id = uuidgen(); @@ -296,6 +379,9 @@ class Framework Console::$theme = $y; } break; + case 'remote-terminal': + $add_port = true; + break; case 'show-php-ver': default: //Console::info("Calculating ".$x); diff --git a/src/ZM/Module/QQBot.php b/src/ZM/Module/QQBot.php index be54c454..9fbd9721 100644 --- a/src/ZM/Module/QQBot.php +++ b/src/ZM/Module/QQBot.php @@ -15,6 +15,7 @@ use ZM\Event\EventDispatcher; use ZM\Exception\InterruptException; use ZM\Exception\WaitTimeoutException; use ZM\Utils\CoMessage; +use ZM\Utils\MessageUtil; /** * Class QQBot @@ -76,58 +77,19 @@ class QQBot //Console::warning("最xia数据包:".json_encode($data)); switch ($data["post_type"]) { case "message": - $word = explodeMsg(str_replace("\r", "", context()->getMessage())); - if (empty($word)) $word = [""]; - if (count(explode("\n", $word[0])) >= 2) { - $enter = explode("\n", context()->getMessage()); - $first = split_explode(" ", array_shift($enter)); - $word = array_merge($first, $enter); - foreach ($word as $k => $v) { - $word[$k] = trim($word[$k]); - } - } //分发CQCommand事件 $dispatcher = new EventDispatcher(CQCommand::class); - $dispatcher->setRuleFunction(function (CQCommand $v) use ($word) { - if (array_diff([$v->match, $v->pattern, $v->regex, $v->keyword, $v->end_with, $v->start_with], [""]) == []) return false; - elseif (($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == ctx()->getUserId())) && - ($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == (ctx()->getGroupId() ?? 0))) && - ($v->message_type == '' || ($v->message_type != '' && $v->message_type == ctx()->getMessageType())) - ) { - if (($word[0] != "" && $v->match == $word[0]) || in_array($word[0], $v->alias)) { - array_shift($word); - ctx()->setCache("match", $word); - return true; - } elseif ($v->start_with != "" && mb_strpos(ctx()->getMessage(), $v->start_with) === 0) { - ctx()->setCache("match", [mb_substr(ctx()->getMessage(), mb_strlen($v->start_with))]); - return true; - } elseif ($v->end_with != "" && strlen(ctx()->getMessage()) == (strripos(ctx()->getMessage(), $v->end_with) + strlen($v->end_with))) { - ctx()->setCache("match", [substr(ctx()->getMessage(), 0, strripos(ctx()->getMessage(), $v->end_with))]); - return true; - } elseif ($v->keyword != "" && mb_strpos(ctx()->getMessage(), $v->keyword) !== false) { - ctx()->setCache("match", explode($v->keyword, ctx()->getMessage())); - return true; - } elseif ($v->pattern != "") { - $match = matchArgs($v->pattern, ctx()->getMessage()); - if ($match !== false) { - ctx()->setCache("match", $match); - return true; - } - } elseif ($v->regex != "") { - if (preg_match("/" . $v->regex . "/u", ctx()->getMessage(), $word2) != 0) { - ctx()->setCache("match", $word2); - return true; - } - } - } - return false; - }); $dispatcher->setReturnFunction(function ($result) { if (is_string($result)) ctx()->reply($result); if (ctx()->getCache("has_reply") === true) EventDispatcher::interrupt(); }); - $dispatcher->dispatchEvents(); - if ($dispatcher->status == EventDispatcher::STATUS_INTERRUPTED) EventDispatcher::interrupt(); + $s = MessageUtil::matchCommand(ctx()->getMessage(), ctx()->getData()); + if ($s->status !== false) { + if (!empty($s->match)) ctx()->setCache("match", $s->match); + $dispatcher->dispatchEvent($s->object, null); + if (is_string($dispatcher->store)) ctx()->reply($dispatcher->store); + if (ctx()->getCache("has_reply") === true) EventDispatcher::interrupt(); + } //分发CQMessage事件 $msg_dispatcher = new EventDispatcher(CQMessage::class); diff --git a/src/ZM/Store/LightCache.php b/src/ZM/Store/LightCache.php index 060cc0cf..b88ca3dd 100644 --- a/src/ZM/Store/LightCache.php +++ b/src/ZM/Store/LightCache.php @@ -7,7 +7,6 @@ namespace ZM\Store; use Exception; use Swoole\Table; use ZM\Annotation\Swoole\OnSave; -use ZM\Config\ZMConfig; use ZM\Console\Console; use ZM\Event\EventDispatcher; use ZM\Exception\ZMException; @@ -182,17 +181,12 @@ class LightCache } /** - * @param false $only_worker + * 这个只能在唯一一个工作进程中执行 * @throws Exception */ - public static function savePersistence($only_worker = false) { - // 下面将OnSave激活一下 - if (server()->worker_id == (ZMConfig::get("global", "worker_cache")["worker"] ?? 0)) { - $dispatcher = new EventDispatcher(OnSave::class); - $dispatcher->dispatchEvents(); - } - - if($only_worker) return; + public static function savePersistence() { + $dispatcher = new EventDispatcher(OnSave::class); + $dispatcher->dispatchEvents(); if (self::$kv_table === null) return; $r = []; @@ -207,8 +201,7 @@ class LightCache $r = file_put_contents(self::$config["persistence_path"], json_encode($r, 128 | 256)); if ($r === false) Console::error("Not saved, please check your \"persistence_path\"!"); } - - + Console::verbose("Saved."); } private static function checkExpire($key) { diff --git a/src/ZM/Store/LightCacheInside.php b/src/ZM/Store/LightCacheInside.php index b9bcc81a..92c3ab38 100644 --- a/src/ZM/Store/LightCacheInside.php +++ b/src/ZM/Store/LightCacheInside.php @@ -18,6 +18,7 @@ class LightCacheInside self::createTable("wait_api", 3, 65536); self::createTable("connect", 3, 64); //用于存单机器人模式下的机器人fd的 self::createTable("static_route", 64, 256);//用于存储 + self::createTable("light_array", 8, 512, 0.6); } catch (ZMException $e) { return false; } //用于存协程等待的状态内容的 @@ -59,10 +60,11 @@ class LightCacheInside * @param $name * @param $size * @param $str_size + * @param int $conflict_proportion * @throws ZMException */ - private static function createTable($name, $size, $str_size) { - self::$kv_table[$name] = new Table($size, 0); + private static function createTable($name, $size, $str_size, $conflict_proportion = 0) { + self::$kv_table[$name] = new Table($size, $conflict_proportion); self::$kv_table[$name]->column("value", Table::TYPE_STRING, $str_size); $r = self::$kv_table[$name]->create(); if ($r === false) throw new ZMException("内存不足,创建静态表失败!"); diff --git a/src/ZM/Store/ZMAtomic.php b/src/ZM/Store/ZMAtomic.php index c2cb72a2..c1c8bed3 100644 --- a/src/ZM/Store/ZMAtomic.php +++ b/src/ZM/Store/ZMAtomic.php @@ -32,6 +32,9 @@ class ZMAtomic self::$atomics["wait_msg_id"] = new Atomic(0); self::$atomics["_event_id"] = new Atomic(0); self::$atomics["server_is_stopped"] = new Atomic(0); + for($i = 0; $i < ZM_WORKER_NUM; ++$i) { + self::$atomics["_#worker_".$i] = new Atomic(0); + } for ($i = 0; $i < 10; ++$i) { self::$atomics["_tmp_" . $i] = new Atomic(0); } diff --git a/src/ZM/Utils/MessageUtil.php b/src/ZM/Utils/MessageUtil.php index 1396d703..79b48dff 100644 --- a/src/ZM/Utils/MessageUtil.php +++ b/src/ZM/Utils/MessageUtil.php @@ -4,9 +4,13 @@ namespace ZM\Utils; +use ZM\Annotation\CQ\CQCommand; use ZM\API\CQ; use ZM\Config\ZMConfig; use ZM\Console\Console; +use ZM\Entity\MatchResult; +use ZM\Event\EventDispatcher; +use ZM\Event\EventManager; use ZM\Requests\ZMRequest; class MessageUtil @@ -55,6 +59,10 @@ class MessageUtil return false; } + public static function isAtMe($msg, $me_id) { + return strpos($msg, CQ::at($me_id)) !== false; + } + /** * 通过本地地址返回图片的 CQ 码 * type == 0 : 返回图片的 base64 CQ 码 @@ -76,4 +84,80 @@ class MessageUtil } return ""; } + + /** + * 分割字符,将用户消息通过空格或换行分割为数组 + * @param $msg + * @return array|string[] + */ + public static function splitCommand($msg) { + $word = explodeMsg(str_replace("\r", "", $msg)); + if (empty($word)) $word = [""]; + if (count(explode("\n", $word[0])) >= 2) { + $enter = explode("\n", $msg); + $first = split_explode(" ", array_shift($enter)); + $word = array_merge($first, $enter); + foreach ($word as $k => $v) { + $word[$k] = trim($word[$k]); + } + } + return $word; + } + + /** + * @param $msg + * @param $obj + * @return MatchResult + */ + public static function matchCommand($msg, $obj) { + $ls = EventManager::$events[CQCommand::class]; + $word = self::splitCommand($msg); + $matched = new MatchResult(); + foreach ($ls as $k => $v) { + if (array_diff([$v->match, $v->pattern, $v->regex, $v->keyword, $v->end_with, $v->start_with], [""]) == []) continue; + elseif (($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == $obj["user_id"])) && + ($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == ($obj["group_id"] ?? 0))) && + ($v->message_type == '' || ($v->message_type != '' && $v->message_type == $obj["message_type"])) + ) { + if (($word[0] != "" && $v->match == $word[0]) || in_array($word[0], $v->alias)) { + array_shift($word); + $matched->match = $word; + $matched->object = $v; + $matched->status = true; + break; + } elseif ($v->start_with != "" && mb_strpos($msg, $v->start_with) === 0) { + $matched->match = [mb_substr($msg, mb_strlen($v->start_with))]; + $matched->object = $v; + $matched->status = true; + break; + } elseif ($v->end_with != "" && strlen($msg) == (strripos($msg, $v->end_with) + strlen($v->end_with))) { + $matched->match = [substr($msg, 0, strripos($msg, $v->end_with))]; + $matched->object = $v; + $matched->status = true; + break; + } elseif ($v->keyword != "" && mb_strpos($msg, $v->keyword) !== false) { + $matched->match = explode($v->keyword, $msg); + $matched->object = $v; + $matched->status = true; + break; + } elseif ($v->pattern != "") { + $match = matchArgs($v->pattern, $msg); + if ($match !== false) { + $matched->match = $match; + $matched->object = $v; + $matched->status = true; + break; + } + } elseif ($v->regex != "") { + if (preg_match("/" . $v->regex . "/u", $msg, $word2) != 0) { + $matched->match = $word2; + $matched->object = $v; + $matched->status = true; + break; + } + } + } + } + return $matched; + } } \ No newline at end of file diff --git a/src/ZM/Utils/ProcessManager.php b/src/ZM/Utils/ProcessManager.php index 83b65ac5..973f3d43 100644 --- a/src/ZM/Utils/ProcessManager.php +++ b/src/ZM/Utils/ProcessManager.php @@ -4,14 +4,110 @@ namespace ZM\Utils; +use Co; +use ZM\Annotation\Swoole\OnPipeMessageEvent; +use ZM\Console\Console; +use ZM\Event\EventDispatcher; +use ZM\Store\LightCache; +use ZM\Store\LightCacheInside; +use ZM\Store\WorkerCache; + class ProcessManager { - public static function runOnTask($param, $timeout = 0.5, $dst_worker_id = -1) { - return server()->taskwait([ - "action" => "runMethod", - "class" => $param["class"], - "method" => $param["method"], - "params" => $param["params"] - ], $timeout, $dst_worker_id); + public static function workerAction($src_worker_id, $data) { + $server = server(); + switch ($data["action"] ?? '') { + case "eval": + eval($data["data"]); + break; + case "call_static": + call_user_func_array([$data["data"]["class"], $data["data"]["method"]], $data["data"]["params"]); + break; + case "save_persistence": + LightCache::savePersistence(); + break; + case "resume_ws_message": + $obj = $data["data"]; + Co::resume($obj["coroutine"]); + break; + case "getWorkerCache": + $r = WorkerCache::get($data["key"]); + $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r]; + $server->sendMessage(json_encode($action, 256), $src_worker_id); + break; + case "setWorkerCache": + $r = WorkerCache::set($data["key"], $data["value"]); + $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r]; + $server->sendMessage(json_encode($action, 256), $src_worker_id); + break; + case "unsetWorkerCache": + $r = WorkerCache::unset($data["key"]); + $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r]; + $server->sendMessage(json_encode($action, 256), $src_worker_id); + break; + case "hasKeyWorkerCache": + $r = WorkerCache::hasKey($data["key"], $data["subkey"]); + $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r]; + $server->sendMessage(json_encode($action, 256), $src_worker_id); + break; + case "asyncAddWorkerCache": + WorkerCache::add($data["key"], $data["value"], true); + break; + case "asyncSubWorkerCache": + WorkerCache::sub($data["key"], $data["value"], true); + break; + case "asyncSetWorkerCache": + WorkerCache::set($data["key"], $data["value"], true); + break; + case "asyncUnsetWorkerCache": + WorkerCache::unset($data["key"], true); + break; + case "addWorkerCache": + $r = WorkerCache::add($data["key"], $data["value"]); + $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r]; + $server->sendMessage(json_encode($action, 256), $src_worker_id); + break; + case "subWorkerCache": + $r = WorkerCache::sub($data["key"], $data["value"]); + $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r]; + $server->sendMessage(json_encode($action, 256), $src_worker_id); + break; + case "returnWorkerCache": + WorkerCache::$transfer[$data["cid"]] = $data["value"]; + zm_resume($data["cid"]); + break; + default: + $dispatcher = new EventDispatcher(OnPipeMessageEvent::class); + $dispatcher->setRuleFunction(function (OnPipeMessageEvent $v) use ($data) { + return $v->action == $data["action"]; + }); + $dispatcher->dispatchEvents($data); + break; + } + } + + public static function sendActionToWorker($worker_id, $action, $data) { + $obj = ["action" => $action, "data" => $data]; + if (server()->worker_id === -1 && server()->getManagerPid() != posix_getpid()) { + Console::warning("Cannot send worker action from master or manager process!"); + return; + } + if (server()->worker_id == $worker_id) { + self::workerAction($worker_id, $obj); + } else { + server()->sendMessage(json_encode($obj), $worker_id); + } + } + + public static function resumeAllWorkerCoroutines() { + if (server()->worker_id === -1) { + 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 (server()->worker_id == $v["worker_id"]) Co::resume($v["coroutine"]); + } + } } } \ No newline at end of file diff --git a/src/ZM/Utils/Terminal.php b/src/ZM/Utils/Terminal.php index fc01cc82..f1eb1054 100644 --- a/src/ZM/Utils/Terminal.php +++ b/src/ZM/Utils/Terminal.php @@ -6,22 +6,38 @@ namespace ZM\Utils; use Exception; use Psy\Shell; -use Swoole\Event; +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 { /** * @param string $cmd - * @param $resource * @return bool * @noinspection PhpMissingReturnTypeInspection * @noinspection PhpUnused + * @throws Exception */ - public static function executeCommand(string $cmd, $resource) { + public static function executeCommand(string $cmd) { $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)"); @@ -35,7 +51,8 @@ class Terminal $class_name = $it[1]; $function_name = $it[2]; $class = new $class_name([]); - $class->$function_name(); + $r = $class->$function_name(); + if (is_string($r)) Console::success($r); return true; case 'psysh': if (Framework::$argv["disable-coroutine"]) { @@ -43,6 +60,11 @@ class Terminal } 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 { @@ -57,7 +79,6 @@ class Terminal Console::log($it[2], $it[1]); return true; case 'stop': - Event::del($resource); ZMUtil::stop(); return false; case 'reload': @@ -67,8 +88,35 @@ class Terminal case '': return true; default: - 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]; + }); + $dispatcher->setReturnFunction(function () { + EventDispatcher::interrupt('none'); + }); + $dispatcher->dispatchEvents($it); + if ($dispatcher->store !== 'none') { + Console::info("Command not found: " . $cmd); + return true; + } + } + return false; + } + + public static function log($type, $log_msg) { + ob_start(); + if (!in_array($type, ["log", "info", "debug", "success", "warning", "error", "verbose"])) { + ob_get_clean(); + return; + } + Console::$type($log_msg); + $r = ob_get_clean(); + $all = ManagerGM::getAllByName("terminal"); + foreach ($all as $k => $v) { + server()->send($v->getFd(), "\r" . $r); + server()->send($v->getFd(), ">>> "); } } } diff --git a/src/ZM/Utils/ZMUtil.php b/src/ZM/Utils/ZMUtil.php index 0efc014b..dc891515 100644 --- a/src/ZM/Utils/ZMUtil.php +++ b/src/ZM/Utils/ZMUtil.php @@ -4,13 +4,10 @@ namespace ZM\Utils; -use Co; use Exception; -use Swoole\Event; -use Swoole\Timer; +use Swoole\Process; use ZM\Console\Console; -use ZM\Store\LightCache; -use ZM\Store\LightCacheInside; +use ZM\Framework; use ZM\Store\Lock\SpinLock; use ZM\Store\ZMAtomic; use ZM\Store\ZMBuf; @@ -24,39 +21,20 @@ class ZMUtil if (SpinLock::tryLock("_stop_signal") === false) return; Console::warning(Console::setColor("Stopping server...", "red")); if (Console::getLevel() >= 4) Console::trace(); - LightCache::savePersistence(); - if (ZMBuf::$terminal !== null) - Event::del(ZMBuf::$terminal); ZMAtomic::get("stop_signal")->set(1); - try { - LightCache::set('stop', 'OK'); - } catch (Exception $e) { + for($i = 0; $i < ZM_WORKER_NUM; ++$i) { + Process::kill(zm_atomic("_#worker_".$i)->get(), SIGUSR1); } server()->shutdown(); server()->stop(); } /** - * @param int $delay * @throws Exception */ - public static function reload($delay = 800) { - if (server()->worker_id !== -1) { - Console::info(server()->worker_id); - zm_atomic("_int_is_reload")->set(1); - system("kill -INT " . intval(server()->master_pid)); - return; - } - Console::info(Console::setColor("Reloading server...", "gold")); - usleep($delay * 1000); - foreach ((LightCacheInside::get("wait_api", "wait_api") ?? []) as $k => $v) { - if (($v["result"] ?? false) === null && isset($v["coroutine"])) Co::resume($v["coroutine"]); - } - LightCacheInside::unset("wait_api", "wait_api"); - LightCache::savePersistence(); - //DataProvider::saveBuffer(); - Timer::clearAll(); - server()->reload(); + public static function reload() { + zm_atomic("_int_is_reload")->set(1); + system("kill -INT " . intval(server()->master_pid)); } public static function getModInstance($class) { @@ -69,6 +47,22 @@ 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[][] + */ + public static function getReloadableFiles() { + return array_map( + function ($x) { + return str_replace(DataProvider::getWorkingDir() . "/", "", $x); + }, array_diff( + get_included_files(), + Framework::$loaded_files + ) + ); + } }