diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 0225d218..00000000 --- a/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM zmbot/swoole:latest - -# TODO: auto-setup entrypoint diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index ceccc54f..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,14 +0,0 @@ -# Security Policy - -## Supported Versions - -| Version | Supported | -| ------- | ------------------ | -| 2.0 | :white_check_mark: | -| 1.6.x | :white_check_mark: | -| 1.1.x | :x: | -| 1.0.x | :x: | - -## Reporting a Vulnerability - -If you find a bug which is safety related, you should post a new issue named **Security Issue**, and I will check it as soon as possible. diff --git a/config/global.php b/config/global.php index 5a55d125..208f2770 100644 --- a/config/global.php +++ b/config/global.php @@ -39,7 +39,7 @@ $config['light_cache'] = [ 'size' => 512, //最多允许储存的条数(需要2的倍数) 'max_strlen' => 32768, //单行字符串最大长度(需要2的倍数) 'hash_conflict_proportion' => 0.6, //Hash冲突率(越大越好,但是需要的内存更多) - 'persistence_path' => $config['zm_data'].'_cache.json', + 'persistence_path' => $config['zm_data'] . '_cache.json', 'auto_save_interval' => 900 ]; @@ -112,22 +112,19 @@ $config['server_event_handler_class'] = [ // 这里添加例如 \ZM\Event\ServerEventHandler::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 - ] - ], +/** 机器人解析模块,关闭后无法使用如CQCommand等注解(上面的modules即将废弃) */ +$config['onebot'] = [ + 'status' => true, + 'single_bot_mode' => false, + 'message_level' => 99999 +]; + +/** 一个远程简易终端,使用nc直接连接即可,但是不建议开放host为0.0.0.0(远程连接) */ +$config['remote_terminal'] = [ + 'status' => false, + 'host' => '127.0.0.1', + 'port' => 20002, + 'token' => '' ]; return $config; diff --git a/docs/FAQ.md b/docs/FAQ.md index c974f108..a67fdd0e 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,3 +1,23 @@ # FAQ -这里会写一些常见的疑难解答。 \ No newline at end of file +这里会写一些常见的疑难解答。 + +## 启动时报错 Address already in use + +1. 检查是否开启了两次框架,每个端口只能开启一个框架。 +2. 如果是之前已经在 20001 端口或者你设置了别的应用同样占用此端口,更换配置文件 `global.php` 中的 port 即可。 +3. 如果是之前框架成功启动,但是使用 Ctrl+C 停止后再次启动导致的报错,请根据下面的步骤来检查是否存在僵尸进程。 + +- 如果系统内装有 `htop`,可以直接在 `htop` 中开启 Tree 模式并使用 filter 过滤 php,检查残留的框架进程。 +- 如果系统没有 `htop`,使用 `ps aux | grep vendor/bin/start | grep -v grep` 如果存在进程,请使用以下命令尝试杀掉: + +```bash +# 如果确定框架的数据都已保存且没有需要保存的缓存数据,直接杀掉 SIGKILL 即可,输入下面这条 +ps aux | grep vendor/bin/start | grep -v grep | awk '{print $2}' | xargs kill -9 + +# 如果不确定框架是不是还继续运行,想尝试正常关闭(走一遍储存保存数据的事件),使用下面这条 +# 首先使用 'ps aux | grep vendor/bin/start | grep -v grep' 找到进程中第二列最小的pid +# 然后使用下面的这条命令,假设最小的pid是23643 +kill -INT 23643 +# 如果使用 ps aux 看不到框架相关进程,证明关闭成功,否则需要使用第一条强行杀死 +``` \ No newline at end of file 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/advanced/task-worker.md b/docs/advanced/task-worker.md new file mode 100644 index 00000000..18495898 --- /dev/null +++ b/docs/advanced/task-worker.md @@ -0,0 +1,3 @@ +# 使用 TaskWorker 进程处理密集运算 + +> 新开个坑,有时间补上。(__填坑标记__) \ No newline at end of file diff --git a/docs/assets/img/image-20210321193956832.png b/docs/assets/img/image-20210321193956832.png new file mode 100644 index 00000000..e69de29b diff --git a/docs/component/global-functions.md b/docs/component/global-functions.md index f4e6e37e..484dd7f5 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,62 @@ 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() + +> v2.4.0 起可用。 + +同 `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() + +> v2.4.0 起可用。(下面的 log 类也一样) + +同 `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/message-util.md b/docs/component/message-util.md new file mode 100644 index 00000000..72f6507a --- /dev/null +++ b/docs/component/message-util.md @@ -0,0 +1,89 @@ +# MessageUtil 消息处理工具类 + +类定义:`\ZM\Utils\MessageUtil` + +> 2.3.0 版本起可用。 + +这里放置一些机器人聊天消息处理的便捷静态方法,例如下载图片等。 + +## 方法 + +### downloadCQImage() + +下载用户消息中所带的所有图片,并返回文件路径。 + +定义:`downloadCQImage($msg, $path = null)` + +参数 `$msg` 为带图片的用户消息,例如 `你好啊!\n[CQ:image,file=a.jpg,url=https://zhamao.xin/file/hello.jpg]` + +参数 `$path` 为图片下载的路径,如果不填(默认 null)则指定为 `zm_data/images/` 目录,且不存在会自动创建。 + +```php +$r = MessageUtil::downloadCQImage("你好啊!\n[CQ:image,file=a.jpg,url=https://zhamao.xin/file/hello.jpg]"); +/* +$r == [ + "/path-to/zhamao-framework/zm_data/images/a.jpg" +]; +*/ +``` + +如果返回的是空数组 `[ ]`,则表明消息中没有图片。如果返回的是 `false`,则表明其中至少一张下载失败或路径有误。 + +### containsImage() + +检查消息中是否含有图片。 + +定义:`containsImage($msg)` + +返回:`bool`,你懂的,true 就是有,false 就没有。 + +```php +MessageUtil::containsImage("[CQ:image,file=a.jpg,url=http://xxx]"); // true +MessageUtil::containsImage("[CQ:face,id=140] 咦,这是一条带表情的消息"); // false +``` + +### getImageCQFromLocal() + +通过文件路径获取图片的发送 CQ 码。 + +定义:`getImageCQFromLocal($file, $type = 0)` + +参数 `$file` 为图片的绝对路径。 + +返回:图片的 CQ 码,如 `[CQ:image,file=xxxxx]` + +参数 `$type`: + +- `0`:以 base64 的方式发送图片,返回结果如 `[CQ:image,file=base64://xxxxxx]` +- `1`:以 `file://` 本地文件的方式发送图片,返回结果如 `[CQ:image,file=file:///path-to/images/a.jpg]` +- `2`:返回图片的 http:// CQ 码(默认为 /images/ 路径就是文件对应所在的目录),如 `[CQ:image,file=http://127.0.0.1:20001/images/a.jpg]` + +### splitCommand() + +切割用户消息为数组形式(`@CQCommand` 就是使用此方式切割的) + +定义:`splitCommand($msg): array` + +返回:数组,切分后的。 + +!!! tip "为什么不直接使用 explode 呢" + + 因为 `explode()` 只会简单粗暴的切割字符串,假设用户输入的消息中两个词中间有多个空格,则会有空的词出现。例如 `你好 我是一个长空格`。此函数会将多个空格当作一个空格来对待。 + +```php +MessageUtil::splitCommand("你好 我是傻瓜\n我是傻瓜二号"); // ["你好","我是傻瓜","我是傻瓜二号"] +MessageUtil::splitCommand("我有 三个空格"); // ["我有","三个空格"] +``` + +### matchCommand() + +匹配一条消息到 `@CQCommand` 规则的注解事件,返回要执行的类和函数位置。 + +定义:`matchCommand($msg, $obj)` + +参数 `$msg` 为消息内容。 + +参数 `$obj` 为事件的对象,可使用 `ctx()->getData()` 获取原先的事件体(仅限 OneBot 消息类型事件中使用) + +返回:`\ZM\Entity\MatchObject` 对象,含有匹配成功与否,匹配到的注解对象,匹配到的分割词等,见 [] + diff --git a/docs/component/route-manager.md b/docs/component/route-manager.md new file mode 100644 index 00000000..698e125d --- /dev/null +++ b/docs/component/route-manager.md @@ -0,0 +1,54 @@ +# HTTP 路由管理 + +HTTP 路由管理器用作管理炸毛框架内 `@RequestMapping` 和静态目录的路由操作的,可在运行过程中编写添加路由。 + +类定义:`\ZM\Http\RouteManager` + +> 2.3.0 版本起可用。 + +!!! warning "注意" + + 因为炸毛框架的路由实现是不基于跨进程的共享内存的,所以每次使用这里面的工具函数都需要单独在所有 Worker 进程中执行一次,最好的办法就是在启动框架时执行(`@OnStart(-1)` 即可,代表此注解事件将在每个工作进程中都被执行一次)。 + +## 方法 + +### importRouteByAnnotation() + +通过注解类导入路由。(注:此方法一般为框架内部使用) + +定义:`importRouteByAnnotation(RequestMapping $vss, $method, $class, $methods_annotations)` + +参数 `$vss`:RequestMapping 注解类,类中定义 `route` 和 `request_method` 即可。 + +参数 `$method, $class`:执行的目标注解事件函数位置,比如 `$class = \Module\Example\Hello::class`,`$method = 'hitokoto'`。 + +参数 `$methods_annotations`:需要绑定的 Controller 注解类数组,一般数组内建议只带有一个 Controller,如 `[$controller]`。 + +### addStaticFileRoute() + +添加一个单目录(此目录下无子目录,只有文件)并绑定为一个路由。 + +定义:`addStaticFileRoute($route, $path)` + +参数 `$route`:绑定的目标路由,如 `/images/`。 + +参数 `$path`:绑定的文件目录位置,如 `/root/zhamao-framework-starter/zm_data/images/`。 + +```php +/** + * @OnStart(-1) + */ +public function onStart() { + RouteManager::addStaticFileRoute("/images/", DataProvider::getDataFolder()."images/"); +} +``` + +## 属性 + +### RouteManager::$routes + +此为存放路由树的变量,请谨慎操作。 + +定义:`\Symfony\Component\Routing\RouteCollection | null` + +炸毛框架使用了 Symfony 框架的 route 组件,有关详情请查阅 [文档](https://symfony.com/doc/current/routing.html)。 \ No newline at end of file diff --git a/docs/component/task-worker.md b/docs/component/task-worker.md new file mode 100644 index 00000000..77c955b8 --- /dev/null +++ b/docs/component/task-worker.md @@ -0,0 +1,26 @@ +# TaskManager 工作进程管理 + +此类管理的是 TaskWorker 相关工作。有关使用 TaskWorker 的教程,见 [进阶 - 使用 TaskWorker 进程处理密集运算](/advanced/task-worker) + +类定义:`\ZM\Utils\TaskManager` + +使用 TaskWorker 需要先在 `global.php` 配置文件中开启! + +## 方法 + +### runTask() + +在 TaskWorker 运行任务。 + +定义:`runTask($task_name, $timeout = -1, ...$params)` + +参数 `$task_name`:对应 `@OnTask` 注解绑定的任务函数。 + +参数 `$timeout`:等待任务函数最长运行的时间(秒),如果超过此时间将返回 false。 + +参数 `剩余`:将变量传入 TaskWorker 进程,除 Closure,资源类型外,可序列化的变量均可。 + +```php +TaskManager::runTask("heavy_task", 100, "param1", "param2"); +``` + 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/config.md b/docs/update/config.md new file mode 100644 index 00000000..5698c724 --- /dev/null +++ b/docs/update/config.md @@ -0,0 +1,25 @@ +# 配置文件变更记录 + +这里将会记录各个主版本的框架升级后,涉及 `global.php` 的更新日志,你可以根据这里描述的内容与你的旧配置文件进行合并。 + +## v2.4.0 (build 400) +- 调整 `$config['modules']['onebot']` 配置项到 `$config['onebot']`,旧版本的此段会向下兼容,建议更新, +- 新增 `$config['remote_terminal']` 远程终端的配置项,新增此段即可。 + +更新部分: +```php +/** 机器人解析模块,关闭后无法使用如CQCommand等注解(上面的modules即将废弃) */ +$config['onebot'] = [ + 'status' => true, + 'single_bot_mode' => false, + 'message_level' => 99999 +]; + +/** 一个远程简易终端,使用nc直接连接即可,但是不建议开放host为0.0.0.0(远程连接) */ +$config['remote_terminal'] = [ + 'status' => false, + 'host' => '127.0.0.1', + 'port' => 20002, + 'token' => '' +]; +``` \ No newline at end of file diff --git a/docs/update/v2.md b/docs/update/v2.md index 057086e5..bb9855b4 100644 --- a/docs/update/v2.md +++ b/docs/update/v2.md @@ -1,5 +1,39 @@ # 更新日志(v2 版本) +## v2.4.0(build 400) + +> 更新时间:2021.3.24 + +- 新增:检查全局配置文件的命令 +- 新增:全局配置文件更新记录 +- 依赖变更:**Swoole 最低版本需要 4.5.0** +- 优化:reload 和 stop 命令重载和停止框架的逻辑 +- 新增:`$_running_annotation` 变量,可在注解事件中的类使用 +- 新增:远程终端(Remote Terminal),弥补原来删掉的本地终端,通过 nc 命令连接即可 +- 新增:启动参数 `--worker-num`,`--task-worker-num`,`--remote-terminal` +- 更新:全局配置文件结构 +- 新增:Swoole 计时器报错处理 +- 新增:全局方法(`zm_dump()`,`zm_error()`,`zm_warning()`,`zm_info()`,`zm_success()`,`zm_verbose()`,`zm_debug()`,`zm_config()`) +- 新增:示例模块的图灵机器人和 at 机器人的处理函数 +- 新增:MessageUtil 工具类新增 `isAtMe(), splitCommand(), matchCommand()` 方法 +- 新增:ProcessManager 进程管理类新增 `workerAction(), sendActionToWorker(), resumeAllWorkerCoroutines()` 方法 +- 优化:CQCommand 的匹配逻辑 +- 新增:支持添加自定义远程终端指令的 `@TerminalCommand` 注解 +- 新增:图灵机器人 API 封装函数 +- 新增:ZMUtil 工具杂项类 `getReloadableFiles()` 函数 + + + + + + +- 修复:部分内部存储存在的内存泄漏问题 +- 新增:remote_terminal(远程终端)内置模块,可以用 nc 直接远程连接框架(弥补原先删除掉的终端输入) +- 新增:`DataProvider::getReloadableFiles()` 返回可通过热重启(reload)来重新加载的 php 文件列表(多用于调试) +- 兼容性变更:**要求最低 Swoole 版本为 4.5.0** +- 优化:reload 和 stop 重载和停止框架的逻辑,防止卡死 +- 新增:远程终端命令支持自定义添加(`@TerminalCommand` 注解) + ## v2.3.5 (build 398) > 更新时间:2021.3.23 @@ -15,7 +49,6 @@ - 规范代码,修复一个小报错的 bug - ## v2.3.0 > 更新时间:2021.3.16 diff --git a/mkdocs.yml b/mkdocs.yml index 9c7613ef..28aee039 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -72,9 +72,12 @@ nav: - 事件分发器: event/event-dispatcher.md - 框架组件: - 框架组件: component/index.md - - 机器人 API: component/robot-api.md - - CQ 码(多媒体消息): component/cqcode.md - 上下文: component/context.md + - 聊天机器人组件: + - 机器人 API: component/robot-api.md + - CQ 码(多媒体消息): component/cqcode.md + - 机器人消息处理: component/message-util.md + - Token 验证: component/access-token.md - 存储: - LightCache 轻量缓存: component/light-cache.md - MySQL 数据库: component/mysql.md @@ -82,13 +85,15 @@ nav: - ZMAtomic 原子计数器: component/atomics.md - SpinLock 自旋锁: component/spin-lock.md - 文件管理: component/data-provider.md + - HTTP 服务器工具类: + - HTTP 和 WebSocket 客户端: component/zmrequest.md + - HTTP 路由管理: component/route-manager.md - 协程池: component/coroutine-pool.md - 单例类: component/singleton-trait.md - ZMUtil 杂项: component/zmutil.md - 全局方法: component/global-functions.md - - HTTP 和 WebSocket 客户端: component/zmrequest.md - Console 终端: component/console.md - - Token 验证: component/access-token.md + - TaskWorker 管理: component/task-worker.md - 进阶开发: - 进阶开发: advanced/index.md - 框架剖析: advanced/framework-structure.md @@ -97,6 +102,7 @@ nav: - 内部类文件手册: advanced/inside-class.md - 接入 WebSocket 客户端: advanced/connect-ws-client.md - 框架多进程: advanced/multi-process.md + - TaskWorker 提高并发: advanced/task-worker.md - 开发实战教程: - 编写管理员才能触发的功能: advanced/example/admin.md - FAQ: FAQ.md diff --git a/resources/images/logo.png b/resources/images/logo.png deleted file mode 100644 index c7e36850..00000000 Binary files a/resources/images/logo.png and /dev/null differ diff --git a/resources/images/logo2.png b/resources/images/logo2.png deleted file mode 100644 index bd828823..00000000 Binary files a/resources/images/logo2.png and /dev/null differ diff --git a/src/Module/Example/Hello.php b/src/Module/Example/Hello.php index 5abf3a96..78be3122 100644 --- a/src/Module/Example/Hello.php +++ b/src/Module/Example/Hello.php @@ -2,17 +2,23 @@ namespace Module\Example; +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\API\CQ; +use ZM\API\TuringAPI; use ZM\ConnectionManager\ConnectionObject; use ZM\Console\Console; use ZM\Annotation\CQ\CQCommand; use ZM\Annotation\Http\RequestMapping; use ZM\Event\EventDispatcher; use ZM\Exception\InterruptException; +use ZM\Module\QQBot; use ZM\Requests\ZMRequest; +use ZM\Utils\MessageUtil; use ZM\Utils\ZMUtil; /** @@ -67,6 +73,42 @@ class Hello return $obj["hitokoto"] . "\n----「" . $obj["from"] . "」"; } + /** + * 图灵机器人的内置实现,在tuling123.com申请一个apikey填入下方变量即可。 + * @CQCommand(start_with="机器人",end_with="机器人",message_type="group") + * @CQMessage(message_type="private",level=1) + */ + public function turingAPI() { + $user_id = ctx()->getUserId(); + $api = ""; // 请在这里填入你的图灵机器人的apikey + if ($api === "") return false; //如果没有填入apikey则此功能关闭 + if (($this->_running_annotation ?? null) instanceof CQCommand) { + $msg = ctx()->getFullArg("我在!有什么事吗?"); + } else { + $msg = ctx()->getMessage(); + } + ctx()->setMessage($msg); + if (MessageUtil::matchCommand($msg, ctx()->getData())->status === false) { + return TuringAPI::getTuringMsg($msg, $user_id, $api); + } else { + QQBot::getInstance()->handle(ctx()->getData(), ctx()->getCache("level") + 1); + return true; + } + } + + /** + * 响应at机器人的消息 + * @CQBefore("message") + */ + public function changeAt() { + if (MessageUtil::isAtMe(ctx()->getMessage(), ctx()->getRobotId())) { + $msg = str_replace(CQ::at(ctx()->getRobotId()), "", ctx()->getMessage()); + ctx()->setMessage("机器人" . $msg); + Console::info(ctx()->getMessage()); + } + return true; + } + /** * 一个简单随机数的功能demo * 问法1:随机数 1 20 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 @@ +setDescription("检查配置文件是否和框架当前版本有更新"); + } + + /** @noinspection PhpIncludeInspection */ + protected function execute(InputInterface $input, OutputInterface $output): int { + if (LOAD_MODE !== 1) { + $output->writeln("仅限在Composer依赖模式中使用此命令!"); + return Command::FAILURE; + } + $current_cfg = WORKING_DIR . "/config/"; + $remote_cfg = include_once $current_cfg . "/vendor/zhamao/framework/config/global.php"; + if (file_exists($current_cfg . "global.php")) { + $this->check($remote_cfg, "global.php", $output); + } + if (file_exists($current_cfg . "global.development.php")) { + $this->check($remote_cfg, "global.development.php", $output); + } + if (file_exists($current_cfg . "global.staging.php")) { + $this->check($remote_cfg, "global.staging.php", $output); + } + if (file_exists($current_cfg . "global.production.php")) { + $this->check($remote_cfg, "global.production.php", $output); + } + if ($this->need_update === true) { + $output->writeln("有配置文件需要更新,详情见文档 https://framework.zhamao.xin/update/config.md"); + } + return Command::SUCCESS; + } + + private function check($remote, $local, OutputInterface $out) { + $local_file = include_once WORKING_DIR . "/config/".$local; + foreach($remote as $k => $v) { + if (!isset($local_file[$k])) { + $out->writeln("配置文件 ".$local . " 需要更新!"); + $this->need_update = true; + return; + } + } + } +} diff --git a/src/ZM/Command/DaemonStopCommand.php b/src/ZM/Command/DaemonStopCommand.php index 3a10f45b..98245cb1 100644 --- a/src/ZM/Command/DaemonStopCommand.php +++ b/src/ZM/Command/DaemonStopCommand.php @@ -18,7 +18,7 @@ class DaemonStopCommand extends DaemonCommand protected function execute(InputInterface $input, OutputInterface $output): int { parent::execute($input, $output); - system("kill -TERM " . intval($this->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..9b54b721 100644 --- a/src/ZM/Command/RunServerCommand.php +++ b/src/ZM/Command/RunServerCommand.php @@ -23,8 +23,11 @@ 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("worker-num", null, InputOption::VALUE_REQUIRED, "启动框架时运行的 Worker 进程数量"), + new InputOption("task-worker-num", null, InputOption::VALUE_REQUIRED, "启动框架时运行的 TaskWorker 进程数量"), new InputOption("watch", null, null, "监听 src/ 目录的文件变化并热更新"), new InputOption("show-php-ver", null, null, "启动时显示PHP和Swoole版本"), new InputOption("env", null, InputOption::VALUE_REQUIRED, "设置环境类型 (production, development, staging)"), diff --git a/src/ZM/ConsoleApplication.php b/src/ZM/ConsoleApplication.php index 74fc50b9..21802594 100644 --- a/src/ZM/ConsoleApplication.php +++ b/src/ZM/ConsoleApplication.php @@ -5,6 +5,7 @@ namespace ZM; use Exception; +use ZM\Command\CheckConfigCommand; use ZM\Command\DaemonReloadCommand; use ZM\Command\DaemonStatusCommand; use ZM\Command\DaemonStopCommand; @@ -18,8 +19,8 @@ use ZM\Utils\DataProvider; class ConsoleApplication extends Application { - const VERSION_ID = 398; - const VERSION = "2.3.5"; + const VERSION_ID = 399; + const VERSION = "2.4.0"; public function __construct(string $name = 'UNKNOWN') { define("ZM_VERSION_ID", self::VERSION_ID); @@ -75,6 +76,9 @@ class ConsoleApplication extends Application new InitCommand(), //初始化用的,用于项目初始化和phar初始化 new PureHttpCommand() //纯HTTP服务器指令 ]); + if (LOAD_MODE === 1) { + $this->add(new CheckConfigCommand()); + } /* $command_register = ZMConfig::get("global", "command_register_class") ?? []; foreach ($command_register as $v) { @@ -100,7 +104,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/Context/Context.php b/src/ZM/Context/Context.php index c40b3443..1fe5bf7a 100644 --- a/src/ZM/Context/Context.php +++ b/src/ZM/Context/Context.php @@ -216,7 +216,7 @@ class Context implements ContextInterface * @throws WaitTimeoutException */ public function getArgs($mode, $prompt_msg) { - $arg = ctx()->getCache("match"); + $arg = ctx()->getCache("match") ?? []; switch ($mode) { case ZM_MATCH_ALL: $p = $arg; 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..c11cce95 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,22 +133,31 @@ 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) { //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(); @@ -456,8 +465,12 @@ class ServerEventHandler } }); try { - if ($conn->getName() === 'qq' && ZMConfig::get("global", "modules")["onebot"]["status"] === true) { - if (ZMConfig::get("global", "modules")["onebot"]["single_bot_mode"]) { + $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); } } @@ -472,7 +485,6 @@ class ServerEventHandler Console::error("Uncaught " . get_class($e) . " when calling \"open\": " . $error_msg); Console::trace(); } - //EventHandler::callSwooleEvent("open", $server, $request); } /** @@ -503,8 +515,11 @@ class ServerEventHandler } }); try { - if ($conn->getName() === 'qq' && ZMConfig::get("global", "modules")["onebot"]["status"] === true) { - if (ZMConfig::get("global", "modules")["onebot"]["single_bot_mode"]) { + $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); } } @@ -528,70 +543,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"); } /** @@ -693,18 +665,19 @@ class ServerEventHandler } //加载插件 - $plugins = ZMConfig::get("global", "modules") ?? []; - if (!isset($plugins["onebot"])) $plugins["onebot"] = ["status" => true, "single_bot_mode" => false, "message_level" => 99999]; + $obb_onebot = ZMConfig::get("global", "onebot") ?? + ZMConfig::get("global", "modules")["onebot"] ?? + ["status" => true, "single_bot_mode" => false, "message_level" => 99999]; - if ($plugins["onebot"]) { + if ($obb_onebot["status"]) { $obj = new OnSwooleEvent(); $obj->class = QQBot::class; - $obj->method = 'handle'; + $obj->method = 'handleByEvent'; $obj->type = 'message'; - $obj->level = $plugins["onebot"]["message_level"] ?? 99999; + $obj->level = $obb_onebot["message_level"] ?? 99999; $obj->rule = 'connectIsQQ()'; EventManager::addEvent(OnSwooleEvent::class, $obj); - if ($plugins["onebot"]["single_bot_mode"]) { + if ($obb_onebot["single_bot_mode"]) { LightCacheInside::set("connect", "conn_fd", -1); } else { LightCacheInside::set("connect", "conn_fd", -2); diff --git a/src/ZM/Framework.php b/src/ZM/Framework.php index c164aeb0..5e478345 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"]); @@ -86,18 +93,23 @@ class Framework 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", "remote_terminal")["status"] ?? false; + $this->parseCliArgs(self::$argv, $add_port); + $worker = $this->server_set["worker_num"] ?? swoole_cpu_num(); + define("ZM_WORKER_NUM", $worker); + ZMAtomic::init(); // 打印初始信息 $out["listen"] = ZMConfig::get("global", "host") . ":" . ZMConfig::get("global", "port"); - if (!isset(ZMConfig::get("global", "swoole")["worker_num"])) $out["worker"] = swoole_cpu_num() . " (auto)"; - else $out["worker"] = ZMConfig::get("global", "swoole")["worker_num"]; + if (!isset($this->server_set["worker_num"])) $out["worker"] = swoole_cpu_num() . " (auto)"; + else $out["worker"] = $this->server_set["worker_num"]; $out["environment"] = $args["env"] === null ? "default" : $args["env"]; $out["log_level"] = Console::getLevel(); $out["version"] = ZM_VERSION . (LOAD_MODE == 0 ? (" (build " . ZM_VERSION_ID . ")") : ""); if (APP_VERSION !== "unknown") $out["app_version"] = APP_VERSION; - if (isset(ZMConfig::get("global", "swoole")["task_worker_num"])) { - $out["task_worker"] = ZMConfig::get("global", "swoole")["task_worker_num"]; + if (isset($this->server_set["task_worker_num"])) { + $out["task_worker"] = $this->server_set["task_worker_num"]; } if (ZMConfig::get("global", "sql_config")["sql_host"] !== "") { $conf = ZMConfig::get("global", "sql_config"); @@ -114,12 +126,88 @@ class Framework $out["php_version"] = PHP_VERSION; $out["swoole_version"] = SWOOLE_VERSION; } + if ($add_port) { + $conf = ZMConfig::get("global", "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) { + $conf = ZMConfig::get("global", "remote_terminal") ?? [ + 'status' => true, + 'host' => '127.0.0.1', + 'port' => 20002, + 'token' => '' + ]; + $welcome_msg = Console::setColor("Welcome! You can use `help` for usage.", "green"); + /** @var Port $port */ + $port = self::$server->listen($conf["host"], $conf["port"], SWOOLE_SOCK_TCP); + $port->set([ + 'open_http_protocol' => false + ]); + $port->on('connect', function (?\Swoole\Server $serv, $fd) use ($port, $welcome_msg, $conf) { + ManagerGM::pushConnect($fd, "terminal"); + $serv->send($fd, file_get_contents(working_dir() . "/config/motd.txt")); + if (!empty($conf["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, $conf) { + ob_start(); + try { + $arr = LightCacheInside::get("light_array", "input_token") ?? []; + if (empty($arr[$fd] ?? '')) { + if ($conf["token"] != '') { + $token = trim($data); + if ($token === $conf["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 +283,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,14 +331,29 @@ 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(); foreach ($args as $x => $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()).")"); + } + 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) { $coroutine_mode = false; @@ -296,6 +399,9 @@ class Framework Console::$theme = $y; } break; + case 'remote-terminal': + $add_port = true; + break; case 'show-php-ver': default: //Console::info("Calculating ".$x); @@ -304,6 +410,7 @@ class Framework } } if ($coroutine_mode) Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL); + else Runtime::enableCoroutine(false, SWOOLE_HOOK_ALL); } private static function writeNoDouble($k, $v, &$line_data, &$line_width, &$current_line, $colorful, $max_border) { diff --git a/src/ZM/Module/QQBot.php b/src/ZM/Module/QQBot.php index be54c454..1985d435 100644 --- a/src/ZM/Module/QQBot.php +++ b/src/ZM/Module/QQBot.php @@ -15,6 +15,8 @@ use ZM\Event\EventDispatcher; use ZM\Exception\InterruptException; use ZM\Exception\WaitTimeoutException; use ZM\Utils\CoMessage; +use ZM\Utils\MessageUtil; +use ZM\Utils\SingletonTrait; /** * Class QQBot @@ -22,20 +24,28 @@ use ZM\Utils\CoMessage; */ class QQBot { + use SingletonTrait; + + public function handleByEvent() { + $data = json_decode(context()->getFrame()->data, true); + $this->handle($data); + } + /** + * @param $data + * @param int $level * @throws InterruptException - * @throws Exception */ - public function handle() { + public function handle($data, $level = 0) { try { - $data = json_decode(context()->getFrame()->data, true); + if ($level > 10) return; set_coroutine_params(["data" => $data]); if (isset($data["post_type"])) { //echo TermColor::ITALIC.json_encode($data, 128|256).TermColor::RESET.PHP_EOL; - ctx()->setCache("level", 0); + ctx()->setCache("level", $level); //Console::debug("Calling CQ Event from fd=" . ctx()->getConnection()->getFd()); if ($data["post_type"] != "meta_event") { - $r = $this->dispatchBeforeEvents($data); // before在这里执行,元事件不执行before为减少不必要的调试日志 + $r = $this->dispatchBeforeEvents($data, "pre"); // before在这里执行,元事件不执行before为减少不必要的调试日志 if ($r->store === "block") EventDispatcher::interrupt(); } //Console::warning("最上数据包:".json_encode($data)); @@ -43,6 +53,10 @@ class QQBot if (isset($data["echo"]) || isset($data["post_type"])) { if (CoMessage::resumeByWS()) EventDispatcher::interrupt(); } + if (($data["post_type"] ?? "meta_event") != "meta_event") { + $r = $this->dispatchBeforeEvents($data, "post"); // before在这里执行,元事件不执行before为减少不必要的调试日志 + if ($r->store === "block") EventDispatcher::interrupt(); + } if (isset($data["post_type"])) $this->dispatchEvents($data); else $this->dispatchAPIResponse($data); } /** @noinspection PhpRedundantCatchClauseInspection */ catch (WaitTimeoutException $e) { @@ -52,14 +66,21 @@ class QQBot /** * @param $data + * @param $time * @return EventDispatcher * @throws Exception */ - public function dispatchBeforeEvents($data): EventDispatcher { + public function dispatchBeforeEvents($data, $time): EventDispatcher { $before = new EventDispatcher(CQBefore::class); - $before->setRuleFunction(function ($v) use ($data) { - return $v->cq_event == $data["post_type"]; - }); + if ($time === "pre") { + $before->setRuleFunction(function($v) use ($data){ + return $v->level >= 200 && $v->cq_event == $data["post_type"]; + }); + } else { + $before->setRuleFunction(function($v) use ($data){ + return $v->level < 200 && $v->cq_event == $data["post_type"]; + }); + } $before->setReturnFunction(function ($result) { if (!$result) EventDispatcher::interrupt("block"); }); @@ -76,58 +97,20 @@ 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(); + zm_dump(ctx()->getData()); + $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..9955f9f8 100644 --- a/src/ZM/Store/ZMAtomic.php +++ b/src/ZM/Store/ZMAtomic.php @@ -32,6 +32,10 @@ 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); + } + echo ("初始化工作进程数量:".ZM_WORKER_NUM.PHP_EOL); 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..0c11814f 100644 --- a/src/ZM/Utils/MessageUtil.php +++ b/src/ZM/Utils/MessageUtil.php @@ -4,9 +4,12 @@ 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\EventManager; use ZM\Requests\ZMRequest; class MessageUtil @@ -55,6 +58,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 +83,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_substr($msg, 0, mb_strlen($v->start_with)) === $v->start_with) { + $matched->match = [mb_substr($msg, mb_strlen($v->start_with))]; + $matched->object = $v; + $matched->status = true; + break; + } elseif ($v->end_with != "" && mb_substr($msg, 0 - mb_strlen($v->end_with)) === $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..28d80783 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..30057fd6 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,21 @@ 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) { + if (Process::kill(zm_atomic("_#worker_" . $i)->get(), 0)) + 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 +48,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 + ) + ); + } } diff --git a/src/ZM/global_functions.php b/src/ZM/global_functions.php index 14e790dd..9b38f3cf 100644 --- a/src/ZM/global_functions.php +++ b/src/ZM/global_functions.php @@ -3,6 +3,7 @@ use Swoole\Atomic; use Swoole\Coroutine; use Swoole\WebSocket\Server; +use Symfony\Component\VarDumper\VarDumper; use ZM\API\ZMRobot; use ZM\Config\ZMConfig; use ZM\ConnectionManager\ManagerGM; @@ -243,8 +244,6 @@ function ctx(): ?ContextInterface { } } -function zm_debug($msg) { Console::debug($msg); } - function onebot_target_id_name($message_type): string { return ($message_type == "group" ? "group_id" : "user_id"); } @@ -255,13 +254,21 @@ function zm_sleep($s = 1): bool { return true; } -function zm_exec($cmd): array { return System::exec($cmd); } +function zm_exec($cmd): array { + return System::exec($cmd); +} -function zm_cid() { return Co::getCid(); } +function zm_cid() { + return Co::getCid(); +} -function zm_yield() { Co::yield(); } +function zm_yield() { + Co::yield(); +} -function zm_resume(int $cid) { Co::resume($cid); } +function zm_resume(int $cid) { + Co::resume($cid); +} function zm_timer_after($ms, callable $callable) { Swoole\Timer::after($ms, function () use ($callable) { @@ -287,10 +294,14 @@ function zm_timer_tick($ms, callable $callable) { if (zm_cid() === -1) { return go(function () use ($ms, $callable) { Console::debug("Adding extra timer tick of " . $ms . " ms"); - Swoole\Timer::tick($ms, function () use ($callable) {call_with_catch($callable);}); + Swoole\Timer::tick($ms, function () use ($callable) { + call_with_catch($callable); + }); }); } else { - return Swoole\Timer::tick($ms, function () use ($callable) {call_with_catch($callable);}); + return Swoole\Timer::tick($ms, function () use ($callable) { + call_with_catch($callable); + }); } } @@ -354,3 +365,32 @@ function working_dir() { elseif (LOAD_MODE == 2) return realpath('.'); return null; } + +/** @noinspection PhpMissingReturnTypeInspection */ +function zm_dump($var, ...$moreVars) { + VarDumper::dump($var); + + foreach ($moreVars as $v) { + VarDumper::dump($v); + } + + if (1 < func_num_args()) { + return func_get_args(); + } + + return $var; +} + +function zm_info($obj) { Console::info($obj); } + +function zm_warning($obj) { Console::warning($obj); } + +function zm_success($obj) { Console::success($obj); } + +function zm_debug($obj) { Console::debug($obj); } + +function zm_verbose($obj) { Console::verbose($obj); } + +function zm_error($obj) { Console::error($obj); } + +function zm_config($name, $key = null) { return ZMConfig::get($name, $key); }