Compare commits

...

10 Commits
2.2.9 ... 2.3.0

Author SHA1 Message Date
jerry
456b102c15 update docs 2021-03-16 01:39:55 +08:00
jerry
cc57997abc update to build 387 2021-03-16 01:35:01 +08:00
jerry
19e61c7cc3 update to build 386
fix ZM_DATA equals null
add containsImage, getImageCQFromLocal function for MessageUtil
2021-03-16 01:34:17 +08:00
jerry
f908513dca update to build 385
add CQObject for CQ
add after-stop action(set terminal level 0)
update global.php modules, add http_proxy_server
add MessageUtil.php for message parsing
add RouteManager::addStaticFileRoute() for quick handling static file
finish onTask function finally!!
add TaskManager::runTask()
2021-03-15 02:54:16 +08:00
jerry
7dc39e6ada update to 2.2.11 verion
add build version (start from 384)
make 启动中 log as verbose
remove console input
fix pure http server bug
add error handler for zm_timer_tick and zm_timer_after
2021-03-13 15:16:10 +08:00
jerry
b0be53554d Merge remote-tracking branch 'origin/master' 2021-03-13 03:03:14 +08:00
jerry
b98048bd39 update docs 2021-03-13 03:03:01 +08:00
Whale
fffd3fdc95 Update README.md 2021-03-12 19:45:53 +08:00
jerry
dea9ed2ccd update docs 2021-03-08 00:56:35 +08:00
jerry
de5744c9e4 update to 2.2.10 version
add build-runtime.sh
remove debug msg when stopping
add --show-php-ver argument for server
2021-03-08 00:48:51 +08:00
32 changed files with 680 additions and 310 deletions

3
.gitignore vendored
View File

@@ -10,4 +10,5 @@ composer.lock
/bin/.phpunit.result.cache
/resources/zhamao.service
.phpunit.result.cache
.daemon_pid
.daemon_pid
/runtime/

View File

@@ -66,6 +66,11 @@ public function index() {
如果旧版框架使用过程中无问题且对新功能暂无需求,可以继续使用 v1 版本,后续也将维护安全类更新和修复致命 bug。
## 下载源码
框架源码可直接克隆本仓库进行编辑,如果你在国内,访问 GitHub 和 clone 仓库比较慢,可以将 `github.com` 替换为 `fgit.zhamao.me` 进行加速。
例如:`git clone https://fgit.zhamao.me/zhamao-robot/zhamao-framework.git`
## 贡献和捐赠
如果你在使用过程中发现任何问题,可以提交 Issue 或自行 Fork 后修改并提交 Pull Request。

View File

@@ -21,9 +21,9 @@ function generate($argv) {
$s .= "\nGroup=" . exec("groups | awk '{print $1}'");
$s .= "\nWorkingDirectory=" . getcwd();
if ($argv[0] == "systemd" && !file_exists(getcwd() . '/systemd'))
$s .= "\nExecStart=" . getcwd() . "/vendor/bin/start server --disable-console-input";
$s .= "\nExecStart=" . getcwd() . "/vendor/bin/start server";
else
$s .= "\nExecStart=" . getcwd() . "/bin/start server --disable-console-input";
$s .= "\nExecStart=" . getcwd() . "/bin/start server";
$s .= "\nRestart=always\n\n[Install]\nWantedBy=multi-user.target\n";
@mkdir(getcwd() . "/resources/");
file_put_contents(getcwd() . "/resources/zhamao.service", $s);

226
build-runtime.sh Executable file
View File

@@ -0,0 +1,226 @@
#!/usr/bin/env bash
_php_ver="7.4.16"
_libiconv_ver="1.15"
_openssl_ver="1.1.1j"
_swoole_ver="4.6.3"
_home_dir=$(pwd)"/"
function checkEnv() {
echo -n "检测核心组件... "
_msg="请通过包管理安装此依赖!"
type git >/dev/null 2>&1 || { echo "失败git 不存在!"$_msg; return 1; }
type gcc >/dev/null 2>&1 || { echo "失败gcc 不存在!"$_msg; return 1; }
type g++ >/dev/null 2>&1 || { echo "失败g++ 不存在!"$_msg; return 1; }
type unzip >/dev/null 2>&1 || { echo "失败unzip 不存在!"$_msg; return 1; }
type autoconf >/dev/null 2>&1 || { echo "失败autoconf 不存在!"; return 1; }
type pkg-config >/dev/null 2>&1 || { echo "失败pkg-config 不存在!"$_msg; return 1; }
type wget >/dev/null 2>&1 || type curl >/dev/null 2>&1 || { echo "失败curl/wget 不存在!"$_msg; return 1; }
echo "完成!"
echo "如果下载过程中出现错误,请删除 runtime/ 文件夹重试!"
echo "此脚本安装的php/swoole均为最小版本不含其他扩展如zip、xml、gd"
echo -n "如果编译过程缺少依赖,请通过包管理安装对应的依赖![按回车继续] "
# shellcheck disable=SC2034
read ents
}
function downloadIt() {
downloader="wget"
type wget >/dev/null 2>&1 || { downloader="curl"; }
if [ "$downloader" = "wget" ]; then
_down_prefix="O"
else
_down_prefix="o"
fi
_down_symbol=0
if [ ! -f "$2" ]; then
$downloader "$1" -$_down_prefix "$2" >/dev/null 2>&1 && \
echo "完成!" && _down_symbol=1
else
echo "已存在!" && _down_symbol=1
fi
if [ $_down_symbol == 0 ]; then
echo "失败!请检查网络连接!"
rm -rf "$2"
return 1
fi
return 0
}
function downloadAll() {
# 创建文件夹
mkdir "$_home_dir""runtime" >/dev/null 2>&1
mkdir "$_home_dir""runtime/tmp_download" >/dev/null 2>&1
mkdir "$_home_dir""runtime/cellar" >/dev/null 2>&1
_down_dir=$_home_dir"runtime/tmp_download/"
# 下载PHP
echo -n "正在下载 php 源码... "
downloadIt "http://mirrors.sohu.com/php/php-$_php_ver.tar.gz" "$_down_dir""php.tar.gz" || { exit; }
# 下载libiconv
echo -n "正在下载 libiconv 源码... "
downloadIt "https://mirrors.tuna.tsinghua.edu.cn/gnu/libiconv/libiconv-$_libiconv_ver.tar.gz" "$_down_dir""libiconv.tar.gz" || { exit; }
echo -n "正在下载 openssl 源码... "
downloadIt "http://mirrors.cloud.tencent.com/openssl/source/openssl-$_openssl_ver.tar.gz" "$_down_dir""openssl.tar.gz" || { exit; }
echo -n "正在下载 swoole 源码... "
downloadIt "https://dl.zhamao.me/swoole/swoole-$_swoole_ver.tgz" "$_down_dir""swoole.tar.gz" || { exit; }
echo -n "正在下载 composer ... "
downloadIt "https://mirrors.aliyun.com/composer/composer.phar" "$_home_dir""runtime/cellar/composer" || { exit; }
#echo -n "正在下载 libcurl 源码... "
#downloadIt "https://curl.se/download/curl-7.75.0.tar.gz" "$_down_dir""libcurl.tar.gz" || { exit; }
}
function compileIt() {
_down_dir="$_home_dir""runtime/tmp_download/"
_source_dir="$_home_dir""runtime/tmp_source/"
_cellar_dir="$_home_dir""runtime/cellar/"
case $1 in
"libiconv")
if [ -f "$_cellar_dir""libiconv/bin/iconv" ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""libiconv.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""libiconv-"$_libiconv_ver && \
./configure --prefix="$_cellar_dir""libiconv" >/dev/null 2>&1 && \
make -j4 >/dev/null 2>&1 && \
make install >/dev/null 2>&1 && \
echo "完成!"
;;
"libzip")
if [ -f "$_cellar_dir""libzip/bin/libzip" ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""libzip.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""libzip-1.7.3" && \
./configure --prefix="$_cellar_dir""libzip" && \
make -j4 && \
make install && \
echo "完成!"
;;
"libcurl")
if [ -f "$_cellar_dir""libcurl/bin/libcurl" ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""libcurl.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""libcurl-7.75.0" && \
./configure --prefix="$_cellar_dir""libcurl" && \
make -j4 && \
make install && \
echo "完成!"
;;
"php")
if [ -f "$_cellar_dir""php/bin/php" ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""php.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""php-"$_php_ver && \
./buildconf --force && \
PKG_CONFIG_PATH="$_cellar_dir""openssl/lib/pkgconfig" ./configure --prefix="$_cellar_dir""php" \
--with-config-file-path="$_home_dir""runtime/etc" \
--disable-fpm \
--enable-cli \
--enable-posix \
--enable-ctype \
--enable-mysqlnd \
--enable-pdo \
--enable-pcntl \
--with-openssl="$_cellar_dir""openssl" \
--enable-sockets \
--disable-xml \
--disable-xmlreader \
--disable-xmlwriter \
--without-libxml \
--disable-dom \
--without-sqlite3 \
--without-pdo-sqlite \
--disable-simplexml \
--with-pdo-mysql=mysqlnd \
--with-zlib \
--with-iconv="$_cellar_dir""libiconv" \
--enable-phar && \
make -j4 && \
make install && \
cp "$_source_dir""php-$_php_ver/php.ini-production" "$_home_dir""runtime/etc/php.ini" && \
echo "完成!"
;;
"openssl")
if [ -f "$_cellar_dir""openssl/bin/openssl" ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""openssl.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""openssl-""$_openssl_ver" && \
./config --prefix="$_cellar_dir""openssl" && \
make -j4 && \
make install && \
echo "完成!"
;;
"swoole")
"$_home_dir"runtime/cellar/php/bin/php --ri swoole >/dev/null 2>&1
# shellcheck disable=SC2181
if [ $? == 0 ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""swoole.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""swoole-""$_swoole_ver" && \
PATH="$_cellar_dir""php/bin:$PATH" phpize && \
PATH="$_cellar_dir""php/bin:$PATH" ./configure --prefix="$_cellar_dir""php" \
--enable-sockets \
--enable-http2 \
--enable-openssl \
--with-openssl-dir="$_cellar_dir""openssl" \
--enable-mysqlnd && \
make -j4 && \
make install && \
echo "extension=swoole.so" >> "$_home_dir""runtime/etc/php.ini" && \
echo "完成!"
;;
esac
}
function compileAll() {
_down_dir=$_home_dir"runtime/tmp_download/"
_source_dir=$_home_dir"runtime/tmp_source/"
mkdir "$_source_dir" >/dev/null 2>&1
mkdir "$_home_dir""runtime/etc" >/dev/null 2>&1
echo -n "正在编译 libiconv ... "
compileIt libiconv || { return 1; }
#echo -n "正在编译 libcurl ... "
#compileIt libcurl || { exit; }
echo -n "正在编译 openssl ... "
compileIt openssl || { return 1; }
#echo -n "正在编译 libzip ... "
#compileIt libzip || { exit; }
echo -n "正在编译 php ... "
compileIt php || { return 1; }
echo -n "正在编译 swoole ... "
compileIt swoole || { return 1; }
return 0
}
function linkBin(){
mkdir "$_home_dir""runtime/bin" >/dev/null 2>&1
ln -s "$_home_dir""runtime/cellar/php/bin/php" "$_home_dir""runtime/bin/php" >/dev/null 2>&1
echo "runtime/cellar/php/bin/php runtime/cellar/composer \$@" > "$_home_dir""runtime/bin/composer" && chmod +x "$_home_dir""runtime/bin/composer"
echo "Done!"
runtime/bin/composer config repo.packagist composer https://mirrors.aliyun.com/composer/
}
checkEnv && \
downloadAll && \
compileAll && \
linkBin && \
echo "成功部署所有环境!" && \
echo -e "composer更新依赖\t\"runtime/bin/composer update\"" && \
echo -e "启动框架(源码模式)\t\"runtime/bin/php bin/start server\"" && \
echo -e "启动框架(普通模式)\t\"runtime/bin/php vendor/bin/start server\""

View File

@@ -3,7 +3,6 @@
"description": "High performance chat robot and web server development framework",
"minimum-stability": "stable",
"license": "Apache-2.0",
"version": "2.2.9",
"extra": {
"exclude_annotate": [
"src/ZM"

View File

@@ -109,15 +109,25 @@ $config['static_file_server'] = [
/** 注册 Swoole Server 事件注解的类列表 */
$config['server_event_handler_class'] = [
\ZM\Event\ServerEventHandler::class,
// 这里添加例如 \ZM\Event\ServerEventHandler::class 这样的启动注解类
];
/** 服务器启用的外部第三方和内部插件 */
$config['modules'] = [
'onebot' => [
'onebot' => [ // 机器人解析模块,关闭后无法使用如@CQCommand等注解
'status' => true,
'single_bot_mode' => false
], // QQ机器人事件解析器如果取消此项则默认为 true 开启状态,否则你手动填写 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
]
],
];
return $config;

View File

@@ -85,7 +85,6 @@ bin/start server # 通过源码模式启动框架
- `--debug-mode`:启用调试模式,调试模式的作用是关闭一键协程化和终端交互,减少 Swoole 本身对代码逻辑的干扰(比如执行 `shell_exec()` 报错的话可以开启这个进行调试)。
- `--log-{mode}`:设置 log 等级。支持 `--log-debug``--log-verbose``--log-info``--log-warning``--log-error`
- `--log-theme`:设置终端信息的主题。这个选项适用于多种终端信息显示的兼容,例如白色终端和不支持颜色的终端。详见 [Console - 主题设置](/component/console/#_2)。
- `--disable-console-input`:关闭终端交互,如果你使用的不是 tmux、screen 而是直接将进程使用 systemd 等方式运行到 init 守护进程下,则需要关闭终端交互输入,关闭后不可以使用 `stop, reload, logtest` 等交互命令。
- `--disable-coroutine`:关闭一键协程化。
- `--daemon`:以守护进程方式运行框架,此参数将直接在输出 motd 后将进程挂到 init 下运行,后台常驻。
- `--watch`:监控 `src/` 目录下的文件变化,有变化则自动重新载入代码。开启监控需要安装 PHP 扩展inotify。使用 pecl 就可以安装:`pecl install inotify`

File diff suppressed because one or more lines are too long

View File

@@ -128,8 +128,9 @@ $str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]"
解析 CQ 码。
- 参数`getCQ($msg);`:要解析出 CQ 码的消息。
- 返回:`数组 | null`,见下表
- 定义`getCQ($msg, $is_object = false)`
- 参数 `$is_object` 为 true 时,返回一个 `\ZM\Entity\CQObject` 对象此对象的属性和下表相同。2.3.0+ 版本可用)
- 返回:`数组 | CQObject | null`,见下表。
| 键名 | 说明 |
| ------ | ------------------------------------------------------------ |
@@ -140,6 +141,10 @@ $str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]"
### CQ::getAllCQ()
定义:`CQ::getAllCQ($msg, $is_object = false)`
参数 `$is_object` 为 true 时,返回一个 `\ZM\Entity\CQObject[]` 对象数组此对象的属性和上面的表格内相同。2.3.0+ 版本可用)
解析 CQ 码,和 `getCQ()` 的区别是,这个会将字符串中的所有 CQ 码都解析出来,并以同样上方解析出来的数组格式返回。
```php
@@ -174,7 +179,7 @@ CQ::getAllCQ("[CQ:at,qq=123]你好啊[CQ:at,qq=456]");
定义:`CQ::face($id)`
参数:`$id` 为 QQ 表情对应的 ID 号,一些常见的表情 ID 对应的表情样式见 [QQ 对应表情ID表](/assets/face_id.html)。
参数:`$id` 为 QQ 表情对应的 ID 号,一些常见的表情 ID 对应的表情样式见 [QQ 对应表情ID表](https://static.zhamao.me/face_id.html)。
```php
/**

View File

@@ -221,6 +221,27 @@
| tick_ms | `int`**必填**,间隔的毫秒数,例如 1 秒间隔为 `1000`,范围大于 0小于 86400000。 | | |
| worker_id | `int`,要在哪个 Worker 进程上执行,默认为 0范围是 0{你设定的 Worker 数量-1},如果是 -1 的话,则会在所有 Worker 进程上触发。 | 限定只执行的 Worker 进程 | |
## OnTask()
定义一个在工作进程中运行的任务函数。详情见 [进阶 - 使用 TaskWorker 进程处理密集运算](/advanced/task-worker)。
### 属性
| 类型 | 值 |
| ---------- | ----------------------------- |
| 名称 | `@OnTask` |
| 触发前提 | 在框架加载后激活 |
| 命名空间 | `ZM\Annotation\Swoole\OnTask` |
| 适用位置 | 方法 |
| 返回值处理 | 有,返回 Worker 进程的结果 |
### 注解参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| --------- | ------------------------------------------------------------ | ------------ | ---- |
| task_name | `string`**必填**,任务函数的名称,不建议重复。 | | |
| rule | 设置触发前提PHP 代码,返回 bool 值即可,参考 OnRequestEvent | 限定是否执行 | 空 |
## OnSetup()
在框架加载前执行的代码。此部分代码是在主进程执行的,不可在此事件中使用任何协程相关的功能。

View File

@@ -36,12 +36,14 @@
### 子表 **swoole**
| 配置名称 | 说明 | 默认值 |
| --------------- | ------------------------------------------------------------ | ----------------------------------- |
| `log_file` | Swoole 的日志文件 | `crash_dir` 下的 `swoole_error.log` |
| `worker_num` | Worker 工作进程数 | 运行框架的主机 CPU 核心数 |
| `dispatch_mode` | 数据包分发策略,见 [文档](https://wiki.swoole.com/#/server/setting?id=dispatch_mode) | 2 |
| `max_coroutine` | 最大协程并发数 | 300000 |
| 配置名称 | 说明 | 默认值 |
| ----------------------- | ------------------------------------------------------------ | ----------------------------------- |
| `log_file` | Swoole 的日志文件 | `crash_dir` 下的 `swoole_error.log` |
| `worker_num` | Worker 工作进程数 | 运行框架的主机 CPU 核心数 |
| `dispatch_mode` | 数据包分发策略,见 [文档](https://wiki.swoole.com/#/server/setting?id=dispatch_mode) | 2 |
| `max_coroutine` | 最大协程并发数 | 300000 |
| `task_worker_num` | TaskWorker 工作进程数 | 默认不开启(此参数被注释) |
| `task_enable_coroutine` | TaskWorker 工作进程启用协程 | 默认不开启(此参数被注释)或 `bool` |
### 子表 **light_cache**

View File

@@ -1,5 +1,37 @@
# 更新日志v2 版本)
## 2.3.0
> 更新时间2021.3.16
- 新增MessageUtil 消息处理工具类
- 新增TaskManager封装了 TaskWorker 进程的应用
- 新增CQObject使用 `CQ::getCQ()` 可获取对象形式的 CQ 码解析结果
- 新增:`@OnTask` 注解,绑定任务函数
- 新增RouteManager 路由管理类,可快速添加路由
- 修复:`ZM_DATA``DataProvider::getDataFolder()` 返回 false 的问题
- 优化:关闭显示停止框架后多余的输出信息
注:本次升级建议升级后合并全局配置文件,有一些新加的内容。
## 2.2.11
> 更新时间2021.3.13
- 新增:内部 ID 版本号ZM_VERSION_ID
- 优化:启动时 log 的等级
- 移除:终端输入命令
- 修复:纯 HTTP 服务器的启动 bug
- 新增:`zm_timer` 的报错处理,防止服务器直接崩掉
## v2.2.10
> 更新时间2021.3.8
- 新增:用户态 php 编译脚本 `build-runtime.sh`
- 移除:无用的调试信息
- 新增:`--show-php-ver` 启动参数
## v2.2.9
> 更新时间2021.3.6

View File

@@ -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

View File

@@ -6,6 +6,7 @@ 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\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
use ZM\Annotation\CQ\CQCommand;
@@ -21,6 +22,14 @@ use ZM\Utils\ZMUtil;
*/
class Hello
{
/*
* 默认的图片监听路由对应目录,如需要使用可取消下面的注释,把上面的 /* 换成 /**
* @OnStart(-1)
*/
//public function onStart() {
// \ZM\Http\RouteManager::addStaticFileRoute("/images/", \ZM\Utils\DataProvider::getWorkingDir()."/zm_data/images/");
//}
/**
* 使用命令 .reload 发给机器人远程重载,注意将 user_id 换成你自己的 QQ
* @CQCommand(".reload",user_id=627577391)

View File

@@ -5,6 +5,7 @@ namespace ZM\API;
use ZM\Console\Console;
use ZM\Entity\CQObject;
class CQ
{
@@ -305,9 +306,10 @@ class CQ
/**
* 获取消息中第一个CQ码
* @param $msg
* @return array|null
* @param bool $is_object
* @return array|CQObject|null
*/
public static function getCQ($msg) {
public static function getCQ($msg, $is_object = false) {
if (($head = mb_strpos($msg, "[CQ:")) !== false) {
$key_offset = mb_substr($msg, $head);
$close = mb_strpos($key_offset, "]");
@@ -322,7 +324,7 @@ class CQ
}
$cq["start"] = $head;
$cq["end"] = $close + $head;
return $cq;
return !$is_object ? $cq : CQObject::fromArray($cq);
} else {
return null;
}
@@ -331,9 +333,10 @@ class CQ
/**
* 获取消息中所有的CQ码
* @param $msg
* @return array
* @param bool $is_object
* @return array|CQObject[]
*/
public static function getAllCQ($msg) {
public static function getAllCQ($msg, $is_object = false) {
$cqs = [];
$offset = 0;
while (($head = mb_strpos(($submsg = mb_substr($msg, $offset)), "[CQ:")) !== false) {
@@ -352,7 +355,7 @@ class CQ
$cq["start"] = $offset + $head;
$cq["end"] = $offset + $tmpmsg + $head;
$offset += $tmpmsg + 1;
$cqs[] = $cq;
$cqs[] = (!$is_object ? $cq : CQObject::fromArray($cq));
}
return $cqs;
}

View File

@@ -0,0 +1,36 @@
<?php
namespace ZM\Annotation\Swoole;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\Rule;
/**
* Class OnTask
* @package ZM\Annotation\Swoole
* @Annotation
* @Target("METHOD")
*/
class OnTask extends AnnotationBase implements Rule
{
/**
* @var string
* @Required()
*/
public $task_name;
/**
* @var string
*/
public $rule = "";
/**
* @return mixed
*/
public function getRule() {
return $this->rule;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace ZM\Annotation\Swoole;
use Doctrine\Common\Annotations\Annotation\Target;
/**
* Class OnTaskEvent
* @package ZM\Annotation\Swoole
* @Annotation
* @Target("METHOD")
*/
class OnTaskEvent extends OnSwooleEventBase
{
}

View File

@@ -17,7 +17,9 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Framework;
use ZM\Store\ZMAtomic;
use ZM\Utils\DataProvider;
use ZM\Utils\HttpUtil;
class PureHttpCommand extends Command
@@ -40,17 +42,26 @@ class PureHttpCommand extends Command
$output->writeln("<error>Directory error(" . ($input->getArgument('dir') ?? '.') . "): no such file or directory.</error>");
return self::FAILURE;
}
ZMConfig::setDirectory(DataProvider::getWorkingDir() . '/config');
$global = ZMConfig::get("global");
$host = $input->getOption("host") ?? $global["host"];
$port = $input->getOption("port") ?? $global["port"];
$index = ["index.html", "index.htm"];
$out = [
"listen" => $host.":".$port,
"version" => ZM_VERSION,
"web_root" => realpath($input->getArgument('dir') ?? '.'),
"index" => implode(",", $index)
];
Framework::printProps($out, $tty_width);
$server = new Server($host, $port);
$server->set(ZMConfig::get("global", "swoole"));
Console::init(0, $server);
Console::init(2, $server);
ZMAtomic::$atomics["request"] = [];
for ($i = 0; $i < 32; ++$i) {
ZMAtomic::$atomics["request"][$i] = new Atomic(0);
}
$index = ["index.html", "index.htm"];
$server->on("request", function (Request $request, Response $response) use ($input, $index, $server) {
ZMAtomic::$atomics["request"][$server->worker_id]->add(1);
HttpUtil::handleStaticPage(
@@ -60,10 +71,11 @@ class PureHttpCommand extends Command
"document_root" => realpath($input->getArgument('dir') ?? '.'),
"document_index" => $index
]);
echo "\r" . Coroutine::stats()["coroutine_peak_num"];
//echo "\r" . Coroutine::stats()["coroutine_peak_num"];
});
$server->on("start", function ($server) {
Process::signal(SIGINT, function () use ($server) {
echo "\r";
Console::warning("Server interrupted by keyboard.");
for ($i = 0; $i < 32; ++$i) {
$num = ZMAtomic::$atomics["request"][$i]->get();
@@ -75,13 +87,6 @@ class PureHttpCommand extends Command
});
Console::success("Server started. Use Ctrl+C to stop.");
});
$out = [
"host" => $host,
"port" => $port,
"document_root" => realpath($input->getArgument('dir') ?? '.'),
"document_index" => implode(", ", $index)
];
Console::printProps($out, $tty_width);
$server->start();
// return this if there was no problem running the command
// (it's equivalent to returning int(0))

View File

@@ -22,10 +22,11 @@ class RunServerCommand extends Command
new InputOption("log-warning", null, null, "调整消息等级到warning (log-level=1)"),
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("disable-console-input", null, null, "禁止终端输入内容 (废弃)"),
new InputOption("disable-coroutine", null, null, "关闭协程Hook"),
new InputOption("daemon", null, null, "以守护进程的方式运行框架"),
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)"),
]);
$this->setDescription("Run zhamao-framework | 启动框架");

View File

@@ -18,9 +18,13 @@ use ZM\Utils\DataProvider;
class ConsoleApplication extends Application
{
const VERSION_ID = 387;
const VERSION = "2.3.0";
public function __construct(string $name = 'UNKNOWN') {
$version = json_decode(file_get_contents(__DIR__ . "/../../composer.json"), true)["version"] ?? "UNKNOWN";
parent::__construct($name, $version);
define("ZM_VERSION_ID", self::VERSION_ID);
define("ZM_VERSION", self::VERSION);
parent::__construct($name, ZM_VERSION);
}
public function initEnv() {
@@ -50,7 +54,7 @@ class ConsoleApplication extends Application
$composer["autoload"]["psr-4"]["Custom\\"] = "src/Custom";
$r = file_put_contents(DataProvider::getWorkingDir() . "/composer.json", json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
if ($r !== false) {
echo "成功添加!请重新进行 composer update \n";
echo "成功添加!请行 composer dump-autoload \n";
exit(1);
} else {
echo "添加失败!请按任意键继续!";

View File

@@ -0,0 +1,26 @@
<?php
namespace ZM\Entity;
class CQObject
{
public $type;
public $params;
public $start;
public $end;
public function __construct($type = "", $params = [], $start = 0, $end = 0) {
if ($type !== "") {
$this->type = $type;
$this->params = $params;
$this->start = $start;
$this->end = $end;
}
}
public static function fromArray($arr) {
return new CQObject($arr["type"], $arr["params"] ?? [], $arr["start"], $arr["end"]);
}
}

View File

@@ -100,6 +100,7 @@ class EventDispatcher
}
}
if ($this->status === self::STATUS_RULE_FAILED) $this->status = self::STATUS_NORMAL;
//TODO:没有过滤before的false可能会导致一些问题先观望一下
} catch (InterruptException $e) {
$this->store = $e->return_var;
$this->status = self::STATUS_INTERRUPTED;

View File

@@ -17,6 +17,7 @@ use Swoole\Database\PDOConfig;
use Swoole\Database\PDOPool;
use Swoole\Event;
use Swoole\Process;
use Throwable;
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Http\RequestMapping;
use ZM\Annotation\Swoole\OnCloseEvent;
@@ -26,6 +27,8 @@ use ZM\Annotation\Swoole\OnPipeMessageEvent;
use ZM\Annotation\Swoole\OnRequestEvent;
use ZM\Annotation\Swoole\OnStart;
use ZM\Annotation\Swoole\OnSwooleEvent;
use ZM\Annotation\Swoole\OnTask;
use ZM\Annotation\Swoole\OnTaskEvent;
use ZM\Config\ZMConfig;
use ZM\ConnectionManager\ManagerGM;
use ZM\Console\Console;
@@ -47,10 +50,8 @@ use ZM\Store\LightCacheInside;
use ZM\Store\MySQL\SqlPoolStorage;
use ZM\Store\Redis\ZMRedisPool;
use ZM\Store\WorkerCache;
use ZM\Store\ZMBuf;
use ZM\Utils\DataProvider;
use ZM\Utils\HttpUtil;
use ZM\Utils\Terminal;
use ZM\Utils\ZMUtil;
class ServerEventHandler
@@ -59,9 +60,9 @@ class ServerEventHandler
* @SwooleHandler("start")
*/
public function onStart() {
global $terminal_id;
//global $terminal_id;
$r = null;
if ($terminal_id !== null) {
/*if ($terminal_id !== null) {
ZMBuf::$terminal = $r = STDIN;
Event::add($r, function () use ($r) {
$fget = fgets($r);
@@ -78,7 +79,7 @@ class ServerEventHandler
Console::error("Uncaught error " . get_class($e) . ": " . $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")");
}
});
}
}*/
Process::signal(SIGINT, function () use ($r) {
if (zm_atomic("_int_is_reload")->get() === 1) {
zm_atomic("_int_is_reload")->set(0);
@@ -159,7 +160,7 @@ class ServerEventHandler
else server()->shutdown();
});
Console::info("Worker #{$server->worker_id} 启动中");
Console::verbose("Worker #{$server->worker_id} 启动中");
Framework::$server = $server;
//ZMBuf::resetCache(); //清空变量缓存
//ZMBuf::set("wait_start", []); //添加队列在workerStart运行完成前先让其他协程等待执行
@@ -233,7 +234,7 @@ class ServerEventHandler
$this->loadAnnotations(); //加载composer资源、phar外置包、注解解析注册等
//echo json_encode(debug_backtrace(), 128|256);
Console::success("Worker #" . $worker_id . " 已启动");
EventManager::registerTimerTick(); //启动计时器
//ZMBuf::unsetCache("wait_start");
set_coroutine_params(["server" => $server, "worker_id" => $worker_id]);
@@ -244,6 +245,7 @@ class ServerEventHandler
$dispatcher->dispatchEvents($server, $worker_id);
if ($dispatcher->status === EventDispatcher::STATUS_NORMAL) Console::debug("@OnStart 执行完毕");
else Console::warning("@OnStart 执行异常!");
Console::success("Worker #" . $worker_id . " 已启动");
} catch (Exception $e) {
Console::error("Worker加载出错停止服务");
Console::error($e->getMessage() . "\n" . $e->getTraceAsString());
@@ -260,7 +262,7 @@ class ServerEventHandler
try {
Framework::$server = $server;
$this->loadAnnotations();
Console::debug("TaskWorker #" . $server->worker_id . " 已启动");
Console::success("TaskWorker #" . $server->worker_id . " 已启动");
} catch (Exception $e) {
Console::error("Worker加载出错停止服务");
Console::error($e->getMessage() . "\n" . $e->getTraceAsString());
@@ -316,7 +318,7 @@ class ServerEventHandler
Console::trace();
} catch (Error $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Uncaught Error " . get_class($e) . " when calling \"message\": " . $error_msg);
Console::error("Uncaught " . get_class($e) . " when calling \"message\": " . $error_msg);
Console::trace();
}
@@ -466,7 +468,7 @@ class ServerEventHandler
Console::trace();
} catch (Error $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Uncaught Error " . get_class($e) . " when calling \"open\": " . $error_msg);
Console::error("Uncaught " . get_class($e) . " when calling \"open\": " . $error_msg);
Console::trace();
}
//EventHandler::callSwooleEvent("open", $server, $request);
@@ -513,7 +515,7 @@ class ServerEventHandler
Console::trace();
} catch (Error $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Uncaught Error " . get_class($e) . " when calling \"close\": " . $error_msg);
Console::error("Uncaught " . get_class($e) . " when calling \"close\": " . $error_msg);
Console::trace();
}
ManagerGM::popConnect($fd);
@@ -595,20 +597,54 @@ class ServerEventHandler
* @SwooleHandler("task")
* @param Server|null $server
* @param Server\Task $task
* @return mixed
* @noinspection PhpUnusedParameterInspection
* @return null
*/
public function onTask(?Server $server, Server\Task $task) {
$data = $task->data;
switch ($data["action"]) {
case "runMethod":
$c = $data["class"];
$ss = new $c();
$method = $data["method"];
$ps = $data["params"];
$task->finish($ss->$method(...$ps));
if (isset($task->data["task"])) {
$dispatcher = new EventDispatcher(OnTask::class);
$dispatcher->setRuleFunction(function ($v) use ($task) {
/** @var OnTask $v */
return $v->task_name == $task->data["task"];
});
$dispatcher->setReturnFunction(function ($return) {
EventDispatcher::interrupt($return);
});
$params = $task->data["params"];
try {
$dispatcher->dispatchEvents(...$params);
} catch (Throwable $e) {
$finish["throw"] = $e;
}
if ($dispatcher->status === EventDispatcher::STATUS_EXCEPTION) {
$finish["result"] = null;
$finish["retcode"] = -1;
} else {
$finish["result"] = $dispatcher->store;
$finish["retcode"] = 0;
}
if (zm_atomic("server_is_stopped")->get() === 1) {
return;
}
$task->finish($finish);
} else {
try {
$dispatcher = new EventDispatcher(OnTaskEvent::class);
$dispatcher->setRuleFunction(function ($v) {
/** @var OnTaskEvent $v */
return eval("return " . $v->getRule() . ";");
});
$dispatcher->dispatchEvents();
} catch (Exception $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Uncaught exception " . get_class($e) . " when calling \"task\": " . $error_msg);
Console::trace();
} catch (Error $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Uncaught " . get_class($e) . " when calling \"task\": " . $error_msg);
Console::trace();
}
}
return null;
}
/**
@@ -641,7 +677,8 @@ class ServerEventHandler
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true);
foreach ($dir as $v) {
if (is_dir($path . "/" . $v) && isset($composer["autoload"]["psr-4"][$v . "\\"]) && !in_array($composer["autoload"]["psr-4"][$v . "\\"], $composer["extra"]["exclude_annotate"] ?? [])) {
Console::verbose("Add " . $v . " to register path");
if (\server()->worker_id == 0)
Console::verbose("Add " . $v . " to register path");
$parser->addRegisterPath(DataProvider::getWorkingDir() . "/src/" . $v . "/", $v);
}
}

View File

@@ -45,14 +45,15 @@ class Framework
self::$argv = $args;
//定义常量
include_once "global_defines.php";
ZMConfig::setDirectory(DataProvider::getWorkingDir() . '/config');
ZMConfig::setEnv($args["env"] ?? "");
if (ZMConfig::get("global") === false) {
die ("Global config load failed: " . ZMConfig::$last_error . "\nPlease init first!\n");
}
//定义常量
include_once "global_defines.php";
ZMAtomic::init();
try {
$sw = ZMConfig::get("global");
@@ -74,7 +75,6 @@ class Framework
die($e->getMessage());
}
try {
Console::init(
ZMConfig::get("global", "info_level") ?? 2,
self::$server,
@@ -94,7 +94,7 @@ class Framework
else $out["worker"] = ZMConfig::get("global", "swoole")["worker_num"];
$out["environment"] = $args["env"] === null ? "default" : $args["env"];
$out["log_level"] = Console::getLevel();
$out["version"] = ZM_VERSION;
$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"];
@@ -110,6 +110,10 @@ class Framework
if (ZMConfig::get("global", "static_file_server")["status"] !== false) {
$out["static_file_server"] = "enabled";
}
if (self::$argv["show-php-ver"] !== false) {
$out["php_version"] = PHP_VERSION;
$out["swoole_version"] = SWOOLE_VERSION;
}
$out["working_dir"] = DataProvider::getWorkingDir();
self::printProps($out, $tty_width, $args["log-theme"] === null);
@@ -117,7 +121,7 @@ class Framework
self::$server = new Server(ZMConfig::get("global", "host"), ZMConfig::get("global", "port"));
self::$server->set($this->server_set);
Console::setServer(self::$server);
self::printMotd($tty_width);
global $asd;
@@ -192,6 +196,8 @@ class Framework
public function start() {
self::$server->start();
zm_atomic("server_is_stopped")->set(1);
Console::setLevel(0);
}
/**
@@ -290,6 +296,7 @@ class Framework
Console::$theme = $y;
}
break;
case 'show-php-ver':
default:
//Console::info("Calculating ".$x);
//dump($y);

View File

@@ -9,6 +9,7 @@ use Symfony\Component\Routing\RouteCollection;
use ZM\Annotation\Http\Controller;
use ZM\Annotation\Http\RequestMapping;
use ZM\Console\Console;
use ZM\Store\LightCacheInside;
class RouteManager
{
@@ -34,4 +35,26 @@ class RouteManager
self::$routes->add(md5($route_name), $route);
}
public static function addStaticFileRoute($route, $path) {
$tail = trim($route, "/");
$route_name = ($tail === "" ? "" : "/") . $tail . "/{filename}";
Console::debug("添加静态文件路由:" . $route_name);
$route = new Route($route_name, ['_class' => RouteManager::class, '_method' => "onStaticRoute"]);
//echo $path.PHP_EOL;
LightCacheInside::set("static_route", $route->getPath(), $path);
self::$routes->add(md5($route_name), $route);
}
public function onStaticRoute($params) {
$route_path = self::$routes->get($params["_route"])->getPath();
if(($path = LightCacheInside::get("static_route", $route_path)) === null) {
ctx()->getResponse()->endWithStatus(404);
return false;
}
unset($params["_route"]);
$obj = array_shift($params);
return new StaticFileHandler($obj, $path);
}
}

View File

@@ -16,17 +16,11 @@ class LightCacheInside
public static $last_error = '';
public static function init() {
self::$kv_table["wait_api"] = new Table(3, 0);
self::$kv_table["wait_api"]->column("value", Table::TYPE_STRING, 65536);
self::$kv_table["connect"] = new Table(8, 0);
self::$kv_table["connect"]->column("value", Table::TYPE_STRING, 256);
$result = self::$kv_table["wait_api"]->create() && self::$kv_table["connect"]->create();
if ($result === false) {
self::$last_error = '系统内存不足,申请失败';
return false;
} else {
return true;
}
self::createTable("wait_api", 3, 65536); //用于存协程等待的状态内容的
self::createTable("connect", 3, 64); //用于存单机器人模式下的机器人fd的
self::createTable("static_route", 64, 256);//用于存储
//self::createTable("worker_start", 2, 1024);//用于存启动服务器时的状态的
return true;
}
/**
@@ -62,4 +56,17 @@ class LightCacheInside
public static function unset(string $table, string $key) {
return self::$kv_table[$table]->del($key);
}
/**
* @param $name
* @param $size
* @param $str_size
* @throws ZMException
*/
private static function createTable($name, $size, $str_size) {
self::$kv_table[$name] = new Table($size, 0);
self::$kv_table[$name]->column("value", Table::TYPE_STRING, $str_size);
$r = self::$kv_table[$name]->create();
if ($r === false) throw new ZMException("内存不足,创建静态表失败!");
}
}

View File

@@ -31,6 +31,7 @@ class ZMAtomic
self::$atomics["_int_is_reload"] = new Atomic(0);
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 < 10; ++$i) {
self::$atomics["_tmp_" . $i] = new Atomic(0);
}

View File

@@ -0,0 +1,80 @@
<?php
namespace ZM\Utils;
use ZM\API\CQ;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Framework;
use ZM\Requests\ZMRequest;
class MessageUtil
{
/**
* 下载消息中 CQ 码的所有图片,通过 url
* @param $msg
* @param null $path
* @return array|false
*/
public static function downloadCQImage($msg, $path = null) {
$path = $path ?? DataProvider::getDataFolder() . "images/";
if (!is_dir($path)) mkdir($path);
$path = realpath($path);
if ($path === false) {
Console::warning("指定的路径错误不存在!");
return false;
}
$files = [];
$cq = CQ::getAllCQ($msg, true);
foreach ($cq as $v) {
if ($v->type == "image") {
$result = ZMRequest::downloadFile($v->params["url"], $path . "/" . $v->params["file"]);
if ($result === false) {
Console::warning("图片 " . $v->params["url"] . " 下载失败!");
return false;
}
$files[] = $path . "/" . $v->params["file"];
}
}
return $files;
}
/**
* 检查消息中是否含有图片 CQ 码
* @param $msg
* @return bool
*/
public static function containsImage($msg) {
$cq = CQ::getAllCQ($msg, true);
foreach ($cq as $v) {
if ($v->type == "image") {
return true;
}
}
return false;
}
/**
* 通过本地地址返回图片的 CQ 码
* type == 0 : 返回图片的 base64 CQ 码
* type == 1 : 返回图片的 file://路径 CQ 码(路径必须为绝对路径)
* type == 2 : 返回图片的 http://xxx CQ 码(默认为 /images/ 路径就是文件对应所在的目录)
* @param $file
* @param int $type
* @return string
*/
public static function getImageCQFromLocal($file, $type = 0) {
switch ($type) {
case 0:
return CQ::image("base64://" . base64_encode(file_get_contents($file)));
case 1:
return CQ::image("file://" . $file);
case 2:
$info = pathinfo($file);
return CQ::image(ZMConfig::get("global", "http_reverse_link") . "/images/" . $info["basename"]);
}
return "";
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace ZM\Utils;
use ZM\Console\Console;
class TaskManager
{
public static function runTask($task_name, $timeout = -1, ...$params) {
if (!isset(server()->setting["task_worker_num"])) {
Console::warning("未开启 TaskWorker 进程,请先修改 global 配置文件启用!");
return false;
}
$r = server()->taskwait(["task" => $task_name, "params" => $params], $timeout);
return $r === false ? false : $r["result"];
}
}

View File

@@ -20,7 +20,7 @@ class ZMUtil
public static function stop() {
if (SpinLock::tryLock("_stop_signal") === false) return;
Console::warning(Console::setColor("Stopping server...", "red"));
Console::trace();
if (Console::getLevel() >= 4) Console::trace();
LightCache::savePersistence();
if (ZMBuf::$terminal !== null)
Event::del(ZMBuf::$terminal);

View File

@@ -5,7 +5,6 @@ use ZM\Utils\DataProvider;
define("ZM_START_TIME", microtime(true));
define("ZM_DATA", ZMConfig::get("global", "zm_data"));
define("ZM_VERSION", json_decode(file_get_contents(__DIR__ . "/../../composer.json"), true)["version"] ?? "unknown");
define("APP_VERSION", LOAD_MODE == 1 ? (json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true)["version"] ?? "unknown") : "unknown");
define("CRASH_DIR", ZMConfig::get("global", "crash_dir"));
@mkdir(ZM_DATA);

View File

@@ -261,19 +261,33 @@ function zm_yield() { Co::yield(); }
function zm_resume(int $cid) { Co::resume($cid); }
function zm_timer_after($ms, callable $callable) {
go(function () use ($ms, $callable) {
Swoole\Timer::after($ms, $callable);
Swoole\Timer::after($ms, function () use ($callable) {
call_with_catch($callable);
});
}
function call_with_catch($callable) {
try {
$callable();
} catch (Exception $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Uncaught exception " . get_class($e) . " when calling \"message\": " . $error_msg);
Console::trace();
} catch (Error $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Uncaught " . get_class($e) . " when calling \"message\": " . $error_msg);
Console::trace();
}
}
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, $callable);
Swoole\Timer::tick($ms, function () use ($callable) {call_with_catch($callable);});
});
} else {
return Swoole\Timer::tick($ms, $callable);
return Swoole\Timer::tick($ms, function () use ($callable) {call_with_catch($callable);});
}
}