update to 3.0.0-alpha2 (build 610): refactor driver

This commit is contained in:
crazywhalecc
2022-08-01 16:31:54 +08:00
committed by Jerry Ma
parent f2a12634a4
commit 41b058aeaf
186 changed files with 1922 additions and 15444 deletions

View File

@@ -1,90 +0,0 @@
<?php
declare(strict_types=1);
namespace ZM\Utils;
use Exception;
use Swoole\Coroutine;
use Swoole\Timer;
use ZM\Store\LightCacheInside;
use ZM\Store\Lock\SpinLock;
use ZM\Store\ZMAtomic;
use ZM\Utils\Manager\WorkerManager;
class CoMessage
{
/**
* @return mixed
*/
public static function yieldByWS(array $hang, array $compare, int $timeout = 600)
{
$cid = Coroutine::getuid();
$api_id = ZMAtomic::get('wait_msg_id')->add();
$hang['compare'] = $compare;
$hang['coroutine'] = $cid;
$hang['worker_id'] = server()->worker_id;
$hang['result'] = null;
SpinLock::lock('wait_api');
$wait = LightCacheInside::get('wait_api', 'wait_api');
$wait[$api_id] = $hang;
LightCacheInside::set('wait_api', 'wait_api', $wait);
SpinLock::unlock('wait_api');
$id = swoole_timer_after($timeout * 1000, function () use ($api_id) {
$r = LightCacheInside::get('wait_api', 'wait_api')[$api_id] ?? null;
if (is_array($r)) {
Coroutine::resume($r['coroutine']);
}
});
Coroutine::suspend();
SpinLock::lock('wait_api');
$sess = LightCacheInside::get('wait_api', 'wait_api');
$result = $sess[$api_id]['result'] ?? null;
unset($sess[$api_id]);
LightCacheInside::set('wait_api', 'wait_api', $sess);
SpinLock::unlock('wait_api');
Timer::clear($id);
if ($result === null) {
return false;
}
return $result;
}
/**
* @throws Exception
*/
public static function resumeByWS(): bool
{
$dat = ctx()->getData();
$last = null;
SpinLock::lock('wait_api');
$all = LightCacheInside::get('wait_api', 'wait_api') ?? [];
foreach ($all as $k => $v) {
if (!isset($v['compare'])) {
continue;
}
foreach ($v['compare'] as $vs) {
if (!isset($v[$vs], $dat[$vs])) {
continue 2;
}
if ($v[$vs] != $dat[$vs]) {
continue 2;
}
}
$last = $k;
}
if ($last !== null) {
$all[$last]['result'] = $dat;
LightCacheInside::set('wait_api', 'wait_api', $all);
SpinLock::unlock('wait_api');
if ($all[$last]['worker_id'] != server()->worker_id) {
WorkerManager::sendActionToWorker($all[$last]['worker_id'], 'resume_ws_message', $all[$last]);
} else {
Coroutine::resume($all[$last]['coroutine']);
}
return true;
}
SpinLock::unlock('wait_api');
return false;
}
}

View File

@@ -1,221 +0,0 @@
<?php
declare(strict_types=1);
namespace ZM\Utils;
use JetBrains\PhpStorm\ArrayShape;
use ReflectionException;
use ReflectionMethod;
use ZM\Annotation\CQ\CommandArgument;
use ZM\Annotation\CQ\CQCommand;
use ZM\Event\EventManager;
use ZM\Store\WorkerCache;
class CommandInfoUtil
{
/**
* 判断命令信息是否已生成并缓存
*/
public function exists(): bool
{
return WorkerCache::get('commands') !== null;
}
/**
* 获取命令信息
*/
#[ArrayShape([['id' => 'string', 'call' => 'callable', 'descriptions' => ['string'], 'triggers' => ['trigger_name' => ['string']], 'args' => ['arg_name' => ['name' => 'string', 'type' => 'string', 'description' => 'string', 'default' => 'mixed', 'required' => 'bool']]]])]
public function get(): array
{
if (!$this->exists()) {
return $this->generateCommandList();
}
return WorkerCache::get('commands');
}
/**
* 重新生成命令信息
*/
public function regenerate(): void
{
$this->generateCommandList();
}
/**
* 获取命令帮助
*
* @param string $command_id 命令ID为 `class@method` 格式
* @param bool $simple 是否仅输出简易信息(只有命令触发条件和描述)
*/
public function getHelp(string $command_id, bool $simple = false): string
{
$command = $this->get()[$command_id];
$formats = [
'match' => '%s',
'pattern' => '符合”%s“',
'regex' => '匹配“%s”',
'start_with' => '以”%s“开头',
'end_with' => '以”%s“结尾',
'keyword' => '包含“%s”',
'alias' => '%s',
];
$triggers = [];
foreach ($command['triggers'] as $trigger => $conditions) {
if (count($conditions) === 0) {
continue;
}
if (isset($formats[$trigger])) {
$format = $formats[$trigger];
} else {
logger()->warning("未知的命令触发条件:{$trigger}");
continue;
}
foreach ($conditions as $condition) {
$condition = sprintf($format, $condition);
$triggers[] = $condition;
}
}
$name = array_shift($triggers);
if (count($triggers) > 0) {
$name .= '' . implode('', $triggers) . '';
}
if (empty($command['descriptions'])) {
$description = '作者很懒,啥也没说';
} else {
$description = implode('', $command['descriptions']);
}
if ($simple) {
return "{$name}{$description}";
}
$lines = [];
$lines[0][] = $name;
$lines[1][] = $description;
foreach ($command['args'] as $arg_name => $arg_info) {
if ($arg_info['required']) {
$lines[0][] = "<{$arg_name}: {$arg_info['type']}>";
} else {
$buffer = "[{$arg_name}: {$arg_info['type']}";
if (!empty($arg_info['default'])) {
$buffer .= " = {$arg_info['default']}";
}
$lines[0][] = $buffer . ']';
}
$lines[][] = "{$arg_name}{$arg_info['description']}";
}
$buffer = [];
foreach ($lines as $line) {
$buffer[] = implode(' ', $line);
}
return implode("\n", $buffer);
}
/**
* 缓存命令信息
*/
protected function save(array $helps): void
{
WorkerCache::set('commands', $helps);
}
/**
* 根据注解树生成命令信息(内部)
*/
protected function generateCommandList(): array
{
$commands = [];
foreach (EventManager::$events[CQCommand::class] ?? [] as $annotation) {
$id = "{$annotation->class}@{$annotation->method}";
try {
$reflection = new ReflectionMethod($annotation->class, $annotation->method);
} catch (ReflectionException $e) {
logger()->warning('命令 ' . $id . ' 注解解析错误:' . $e->getMessage());
continue;
}
$doc = $reflection->getDocComment();
if ($doc) {
// 匹配出不以@开头,且后接中文或任意非空格字符,并以换行符结尾的字符串,也就是命令描述
preg_match_all('/\*\s((?!@)[\x{4e00}-\x{9fa5}\S]+)(\r\n|\r|\n)/u', $doc, $descriptions);
$descriptions = $descriptions[1];
}
$command = [
'id' => $id,
'call' => [$annotation->class, $annotation->method],
'descriptions' => $descriptions ?? [],
'triggers' => [],
'args' => [],
];
if (empty($command['descriptions'])) {
logger()->warning("命令没有描述信息:{$id}");
}
// 可能的触发条件,顺序会影响命令帮助的生成结果
$possible_triggers = ['match', 'pattern', 'regex', 'start_with', 'end_with', 'keyword', 'alias'];
foreach ($possible_triggers as $trigger) {
if (isset($annotation->{$trigger}) && !empty($annotation->{$trigger})) {
// 部分触发条件可能存在多个
if (is_iterable($annotation->{$trigger})) {
foreach ($annotation->{$trigger} as $item) {
$command['triggers'][$trigger][] = $item;
}
} else {
$command['triggers'][$trigger][] = $annotation->{$trigger};
}
}
}
if (empty($command['triggers'])) {
logger()->warning("命令没有触发条件:{$id}");
continue;
}
$command['args'] = $this->generateCommandArgumentList($id);
$commands[$id] = $command;
}
$this->save($commands);
return $commands;
}
/**
* 生成指定命令的参数列表
*
* @param string $id 命令 ID
*/
protected function generateCommandArgumentList(string $id): array
{
[$class, $method] = explode('@', $id);
$map = EventManager::$event_map[$class][$method];
$args = [];
foreach ($map as $annotation) {
if (!$annotation instanceof CommandArgument) {
continue;
}
$args[$annotation->name] = [
'name' => $annotation->name,
'type' => $annotation->type,
'description' => $annotation->description,
'default' => $annotation->default,
'required' => $annotation->required,
];
}
return $args;
}
}

View File

@@ -1,62 +0,0 @@
<?php
declare(strict_types=1);
namespace ZM\Utils;
use Swoole\Coroutine;
class CoroutinePool
{
private static $cids = [];
private static $default_size = 30;
private static $sizes = [];
private static $yields = [];
public static function go(callable $func, $name = 'default')
{
if (!isset(self::$cids[$name])) {
self::$cids[$name] = [];
}
if (count(self::$cids[$name]) >= (self::$sizes[$name] ?? self::$default_size)) {
self::$yields[] = Coroutine::getCid();
Coroutine::suspend();
}
go(function () use ($func, $name) {
self::$cids[$name][] = Coroutine::getCid();
// logger()->debug("正在执行协程,当前协程池中有 " . count(self::$cids[$name]) . " 个正在运行的协程: ".implode(", ", self::$cids[$name]));
$func();
self::checkCids($name);
});
}
public static function defaultSize(int $size)
{
self::$default_size = $size;
}
public static function setSize($name, int $size)
{
self::$sizes[$name] = $size;
}
public static function getRunningCoroutineCount($name = 'default')
{
return count(self::$cids[$name]);
}
private static function checkCids($name)
{
if (in_array(Coroutine::getCid(), self::$cids[$name])) {
$a = array_search(Coroutine::getCid(), self::$cids[$name]);
array_splice(self::$cids[$name], $a, 1);
$r = array_shift(self::$yields);
if ($r !== null) {
Coroutine::resume($r);
}
}
}
}

View File

@@ -1,197 +0,0 @@
<?php
declare(strict_types=1);
/** @noinspection PhpUnused */
namespace ZM\Utils;
use Iterator;
use JsonSerializable;
use RuntimeException;
use Traversable;
use ZM\Config\ZMConfig;
use ZM\Exception\ConfigException;
class DataProvider
{
public static $buffer_list = [];
/**
* 返回资源目录
*/
public static function getResourceFolder(): string
{
return self::getWorkingDir() . '/resources/';
}
/**
* 返回工作目录,不带最右边文件夹的斜杠(/
*
* @return false|string
*/
public static function getWorkingDir()
{
return WORKING_DIR;
}
/**
* 获取框架所在根目录
*
* @return false|string
*/
public static function getFrameworkRootDir()
{
return FRAMEWORK_ROOT_DIR;
}
/**
* 获取源码根目录除Phar模式外均与工作目录相同
*
* @return false|string
*/
public static function getSourceRootDir()
{
return defined('SOURCE_ROOT_DIR') ? SOURCE_ROOT_DIR : WORKING_DIR;
}
/**
* 获取框架反代链接
*
* @throws ConfigException
* @return null|array|false|mixed
*/
public static function getFrameworkLink()
{
return ZMConfig::get('global', 'http_reverse_link');
}
/**
* 获取zm_data数据目录如果二级目录不为空则自动创建目录并返回
*
* @return null|array|false|mixed|string
*/
public static function getDataFolder(string $second = '')
{
if ($second !== '') {
if (!is_dir(ZM_DATA . $second)) {
@mkdir(ZM_DATA . $second);
}
if (!is_dir(ZM_DATA . $second)) {
return false;
}
return realpath(ZM_DATA . $second) . '/';
}
return ZM_DATA;
}
/**
* 将变量保存在zm_data下的数据目录传入数组
*
* @param string $filename 文件名
* @param array|int|Iterator|JsonSerializable|string|Traversable $file_array 文件内容数组
* @throws ConfigException
* @return false|int 返回文件大小或false
*/
public static function saveToJson(string $filename, $file_array)
{
$path = ZMConfig::get('global', 'config_dir');
$r = explode('/', $filename);
if (count($r) == 2) {
$path = $path . $r[0] . '/';
if (!is_dir($path)) {
mkdir($path);
}
$name = $r[1];
} elseif (count($r) != 1) {
logger()->warning(zm_internal_errcode('E00057') . '存储失败,文件名只能有一级目录');
return false;
} else {
$name = $r[0];
}
return file_put_contents($path . $name . '.json', json_encode($file_array, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
}
/**
* 从json加载变量到内存
*
* @param string $filename 文件名
* @throws ConfigException
* @return null|mixed 返回文件内容数据或null
*/
public static function loadFromJson(string $filename)
{
$path = ZMConfig::get('global', 'config_dir');
if (file_exists($path . $filename . '.json')) {
return json_decode(file_get_contents($path . $filename . '.json'), true);
}
return null;
}
/**
* 递归或非递归扫描目录,可返回相对目录的文件列表或绝对目录的文件列表
*
* @param string $dir 目录
* @param bool $recursive 是否递归扫描子目录
* @param bool|string $relative 是否返回相对目录如果为true则返回相对目录如果为false则返回绝对目录
* @return array|false
* @since 2.5
*/
public static function scanDirFiles(string $dir, bool $recursive = true, $relative = false)
{
$dir = rtrim($dir, '/');
if (!is_dir($dir)) {
return false;
}
$r = scandir($dir);
if ($r === false) {
return false;
}
$list = [];
if ($relative === true) {
$relative = $dir;
}
foreach ($r as $v) {
if ($v == '.' || $v == '..') {
continue;
}
$sub_file = $dir . '/' . $v;
if (is_dir($sub_file) && $recursive) {
$list = array_merge($list, self::scanDirFiles($sub_file, $recursive, $relative));
} elseif (is_file($sub_file)) {
if (is_string($relative) && mb_strpos($sub_file, $relative) === 0) {
$list[] = ltrim(mb_substr($sub_file, mb_strlen($relative)), '/');
} elseif ($relative === false) {
$list[] = $sub_file;
} else {
logger()->warning(zm_internal_errcode('E00058') . "Relative path is not generated: wrong base directory ({$relative})");
return false;
}
}
}
return $list;
}
/**
* 检查路径是否为相对路径(根据第一个字符是否为"/"来判断)
*
* @param string $path 路径
* @return bool 返回结果
* @since 2.5
*/
public static function isRelativePath(string $path): bool
{
return strlen($path) > 0 && $path[0] !== '/';
}
/**
* 创建目录(如果不存在)
*
* @param string $path 目录路径
*/
public static function createIfNotExists(string $path): void
{
if (!is_dir($path) && !mkdir($path, 0777, true) && !is_dir($path)) {
throw new RuntimeException(sprintf('无法建立目录:%s', $path));
}
}
}

View File

@@ -1,110 +0,0 @@
<?php
declare(strict_types=1);
namespace ZM\Utils;
use Swoole\Coroutine;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use ZM\Config\ZMConfig;
use ZM\Http\Response;
use ZM\Utils\Manager\RouteManager;
class HttpUtil
{
/** @noinspection PhpMissingReturnTypeInspection */
public static function parseUri($request, $response, $uri, &$node, &$params)
{
$context = new RequestContext();
$context->setMethod($request->server['request_method']);
try {
$matcher = new UrlMatcher(RouteManager::$routes ?? new RouteCollection(), $context);
$matched = $matcher->match($uri);
} catch (ResourceNotFoundException $e) {
if (ZMConfig::get('global', 'static_file_server')['status']) {
HttpUtil::handleStaticPage($request->server['request_uri'], $response);
return null;
}
$matched = null;
} catch (MethodNotAllowedException $e) {
$matched = null;
}
if ($matched !== null) {
$node = [
'route' => RouteManager::$routes->get($matched['_route'])->getPath(),
'class' => $matched['_class'],
'method' => $matched['_method'],
'request_method' => $request->server['request_method'],
];
unset($matched['_class'], $matched['_method']);
$params = $matched;
return true;
}
return false;
}
public static function getHttpCodePage(int $http_code)
{
if (isset(ZMConfig::get('global', 'http_default_code_page')[$http_code])) {
return Coroutine::readFile(DataProvider::getResourceFolder() . 'html/' . ZMConfig::get('global', 'http_default_code_page')[$http_code]);
}
return null;
}
/**
* @param Response|\Swoole\Http\Response $response
*/
public static function handleStaticPage(string $uri, $response, array $settings = []): bool
{
$base_dir = $settings['document_root'] ?? ZMConfig::get('global', 'static_file_server')['document_root'];
$base_index = $settings['document_index'] ?? ZMConfig::get('global', 'static_file_server')['document_index'];
$path = realpath($base_dir . urldecode($uri));
if ($path !== false) {
if (is_dir($path)) {
$path = $path . '/';
}
$work = realpath($base_dir) . '/';
if (strpos($path, $work) !== 0) {
logger()->info('[403] ' . $uri);
self::responseCodePage($response, 403);
return true;
}
if (is_dir($path)) {
if (mb_substr($uri, -1, 1) != '/') {
logger()->info('[302] ' . $uri);
$response->redirect($uri . '/', 302);
return true;
}
foreach ($base_index as $vp) {
if (is_file($path . '/' . $vp)) {
logger()->info('[200] ' . $uri);
$exp = strtolower(pathinfo($path . $vp)['extension'] ?? 'unknown');
$response->setHeader('Content-Type', ZMConfig::get('file_header')[$exp] ?? 'application/octet-stream');
$response->end(file_get_contents($path . $vp));
return true;
}
}
} elseif (is_file($path)) {
logger()->info('[200] ' . $uri);
$exp = strtolower(pathinfo($path)['extension'] ?? 'unknown');
$response->setHeader('Content-Type', ZMConfig::get('file_header')[$exp] ?? 'application/octet-stream');
$response->end(file_get_contents($path));
return true;
}
}
logger()->info('[404] ' . $uri);
self::responseCodePage($response, 404);
return true;
}
public static function responseCodePage($response, $code)
{
$response->status($code);
$response->end(self::getHttpCodePage($code));
}
}

View File

@@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
namespace ZM\Utils;
use Closure;
use ZM\Exception\MethodNotFoundException;
trait Macroable
{
protected static $macros = [];
public static function __callStatic($method, $parameters)
{
if (!static::hasMacro($method)) {
throw new MethodNotFoundException("Method {$method} does not exist.");
}
if (static::$macros[$method] instanceof Closure) {
return call_user_func_array(Closure::bind(static::$macros[$method], null, static::class), $parameters);
}
return call_user_func_array(static::$macros[$method], $parameters);
}
public function __call($method, $parameters)
{
if (!static::hasMacro($method)) {
throw new MethodNotFoundException("Method {$method} does not exist.");
}
if (static::$macros[$method] instanceof Closure) {
return call_user_func_array(static::$macros[$method]->bindTo($this, static::class), $parameters);
}
return call_user_func_array(static::$macros[$method], $parameters);
}
public static function macro($name, callable $macro)
{
static::$macros[$name] = $macro;
}
public static function hasMacro($name)
{
return isset(static::$macros[$name]);
}
}

View File

@@ -1,110 +0,0 @@
<?php
declare(strict_types=1);
namespace ZM\Utils\Manager;
use Cron\CronExpression;
use Doctrine\Common\Annotations\AnnotationException;
use Error;
use Exception;
use InvalidArgumentException;
use Swoole\Timer;
use ZM\Annotation\Cron\Cron;
use ZM\Console\Console;
use ZM\Event\EventDispatcher;
use ZM\Event\EventManager;
use ZM\Exception\InterruptException;
use ZM\Store\ZMAtomic;
class CronManager
{
/**
* 初始化 Cron 注解
* 必须在 WorkerStart 事件中调用
*
* @throws Exception
* @internal
*/
public static function initCronTasks()
{
$dispatcher = new EventDispatcher(Cron::class);
$all = EventManager::$events[Cron::class] ?? [];
foreach ($all as $v) {
/** @var Cron $v */
if (server()->worker_id !== $v->worker_id && $v->worker_id != -1) {
return;
}
try {
if (strpos($v->expression, '\\') !== 0) {
$v->expression = str_replace('\\', '/', $v->expression);
}
$cron = new CronExpression($v->expression);
$cron->setMaxIterationCount($v->max_iteration_count);
$plain_class = $v->class;
logger()->debug("Cron task checker starting {$plain_class}:{$v->method}, next run at {$cron->getNextRunDate()->format('Y-m-d H:i:s')}");
if ($v->check_delay_time > 60000 || $v->check_delay_time < 1000) {
logger()->warning(zm_internal_errcode('E00076') . 'Delay time must be between 1000 and 60000, reset to 20000');
$v->check_delay_time = 20000;
}
} catch (InvalidArgumentException $e) {
logger()->error(zm_internal_errcode('E00075') . 'Invalid cron expression or arguments, please check it!');
throw $e;
}
Timer::tick($v->check_delay_time, static function () use ($v, $dispatcher, $cron) {
set_coroutine_params([]);
if (ZMAtomic::get('stop_signal')->get() != 0) {
Timer::clearAll();
return;
}
try {
logger()->debug('Cron: ' . ($cron->isDue() ? 'true' : 'false') . ', last: ' . $cron->getPreviousRunDate()->format('Y-m-d H:i:s') . ', next: ' . $cron->getNextRunDate()->format('Y-m-d H:i:s'));
if ($cron->isDue()) {
if ($v->getStatus() === 0) {
self::startExecute($v, $dispatcher, $cron);
} elseif ($v->getStatus() === 2) {
if ($v->getRecordNextTime() !== $cron->getNextRunDate()->getTimestamp()) {
self::startExecute($v, $dispatcher, $cron);
}
}
} else {
if ($v->getStatus() === 2 && $v->getRecordNextTime()) {
$v->setStatus(0);
}
}
} catch (Exception $e) {
Console::error(zm_internal_errcode('E00034') . 'Uncaught error from Cron: ' . $e->getMessage() . ' at ' . $e->getFile() . "({$e->getLine()})");
} catch (Error $e) {
Console::error(zm_internal_errcode('E00034') . 'Uncaught fatal error from Cron: ' . $e->getMessage());
echo Console::setColor($e->getTraceAsString(), 'gray');
Console::error('Please check your code!');
}
});
}
}
/**
* @throws InterruptException
* @throws AnnotationException
* @throws Exception
*/
private static function startExecute(Cron $v, EventDispatcher $dispatcher, CronExpression $cron)
{
logger()->debug("Cron task {$v->class}:{$v->method} is due, running at " . date('Y-m-d H:i:s') . ($v->getRecordNextTime() === 0 ? '' : (', offset ' . (time() - $v->getRecordNextTime()) . 's')));
$v->setStatus(1);
$starttime = microtime(true);
$pre_next_time = $cron->getNextRunDate()->getTimestamp();
$dispatcher->dispatchEvent($v, null, $cron);
logger()->debug("Cron task {$v->class}:{$v->method} is done, using " . round(microtime(true) - $starttime, 3) . 's');
if ($pre_next_time !== $cron->getNextRunDate()->getTimestamp()) { // 这一步用于判断运行的Cron是否已经覆盖到下一个运行区间
if (time() + round($v->check_delay_time / 1000) >= $pre_next_time) { // 假设检测到下一个周期运行时间已经要超过了预计的时间,则警告运行超时
logger()->warning(zm_internal_errcode('E00077') . 'Cron task ' . $v->class . ':' . $v->method . ' is timeout');
}
} else {
logger()->debug('Next run at ' . date('Y-m-d H:i:s', $cron->getNextRunDate()->getTimestamp()));
}
$v->setRecordNextTime($pre_next_time);
$v->setStatus(2);
}
}

View File

@@ -1,219 +0,0 @@
<?php
declare(strict_types=1);
namespace ZM\Utils\Manager;
use Iterator;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Exception\ModulePackException;
use ZM\Exception\ZMException;
use ZM\Exception\ZMKnownException;
use ZM\Module\ModulePacker;
use ZM\Module\ModuleUnpacker;
use ZM\Utils\DataProvider;
/**
* 模块管理器,负责打包解包模块
* Class ModuleManager
* @since 2.5
*/
class ModuleManager
{
public static function getComposer()
{
return json_decode(file_get_contents(DataProvider::getSourceRootDir() . '/composer.json'), true);
}
/**
* 扫描src目录下的所有已经被标注的模块
* @throws ZMException
*/
public static function getConfiguredModules(): array
{
$composer = self::getComposer();
$dir = DataProvider::getSourceRootDir() . '/src/';
$ls = DataProvider::scanDirFiles($dir, true, true);
$modules = [];
foreach ($ls as $v) {
$pathinfo = pathinfo($v);
if ($pathinfo['basename'] == 'zm.json') {
$json = json_decode(file_get_contents(realpath($dir . '/' . $v)), true);
if ($json === null) {
continue;
}
if (!isset($json['name'])) {
continue;
}
if ($pathinfo['dirname'] == '.') {
throw new ZMKnownException('E00052', '在/src/目录下不可以直接标记为模块(zm.json),因为命名空间不能为根空间!');
}
$json['module-path'] = realpath($dir . '/' . $pathinfo['dirname']);
$relative_path = str_replace(DataProvider::getSourceRootDir() . '/', '', $json['module-path']);
foreach (array_merge($composer['autoload']['psr-4'] ?? [], $composer['autoload-dev']['psr-4'] ?? []) as $ks => $vs) {
if (strpos($relative_path, $vs) === 0) {
$remain = trim(substr($relative_path, strlen($vs)), '/');
$remain = str_replace('/', '\\', $remain);
$json['namespace'] = $ks . $remain;
break;
}
}
// $json['namespace'] = str_replace('/', '\\', $pathinfo['dirname']);
if (isset($modules[$json['name']])) {
throw new ZMKnownException('E00053', '重名模块:' . $json['name']);
}
$modules[$json['name']] = $json;
}
}
return $modules;
}
public static function getPackedModules(): array
{
$dir = ZMConfig::get('global', 'module_loader')['load_path'] ?? (ZM_DATA . 'modules');
$ls = DataProvider::scanDirFiles($dir, true, false);
if ($ls === false) {
return [];
}
$modules = [];
foreach ($ls as $v) {
$pathinfo = pathinfo($v);
if (($pathinfo['extension'] ?? '') != 'phar') {
continue;
}
$file = 'phar://' . $v;
if (!is_file($file . '/module_entry.php') || !is_file($file . '/zmplugin.json')) {
continue;
}
$module_config = json_decode(file_get_contents($file . '/zmplugin.json'), true);
if ($module_config === null) {
continue;
}
if (!is_file($file . '/' . $module_config['module-root-path'] . '/zm.json')) {
logger()->warning(zm_internal_errcode('E00054') . '模块(插件)文件 ' . $pathinfo['basename'] . ' 无法找到模块配置文件zm.json');
continue;
}
$module_file = json_decode(file_get_contents($file . '/' . $module_config['module-root-path'] . '/zm.json'), true);
if ($module_file === null) {
logger()->warning(zm_internal_errcode('E000555') . '模块(插件)文件 ' . $pathinfo['basename'] . ' 无法正常读取模块配置文件zm.json');
continue;
}
$module_config['phar-path'] = $v;
$module_config['name'] = $module_file['name'] ?? null;
if ($module_config['name'] === null) {
continue;
}
$module_config['module-config'] = $module_file;
$modules[$module_config['name']] = $module_config;
}
return $modules;
}
public static function getComposerModules()
{
$vendor_file = DataProvider::getSourceRootDir() . '/vendor/composer/installed.json';
$obj = json_decode(file_get_contents($vendor_file), true);
if ($obj === null) {
return [];
}
$modules = [];
foreach ($obj['packages'] as $v) {
if (isset($v['extra']['zm']['module-path'])) {
if (is_array($v['extra']['zm']['module-path'])) {
foreach ($v['extra']['zm']['module-path'] as $module_path) {
$m = self::getComposerModuleInfo($v, $module_path);
if ($m !== null) {
$modules[$m['name']] = $m;
}
}
} elseif (is_string($v['extra']['zm']['module-path'])) {
$m = self::getComposerModuleInfo($v, $v['extra']['zm']['module-path']);
if ($m !== null) {
$modules[$m['name']] = $m;
}
}
}
}
return $modules;
}
/**
* 打包模块
* @param array $module 模块信息
* @param string $target 目标路径
* @throws ZMException
*/
public static function packModule(array $module, string $target): bool
{
try {
$packer = new ModulePacker($module);
if (!is_dir(DataProvider::getDataFolder())) {
throw new ModulePackException(zm_internal_errcode('E00070') . 'zm_data dir not found!');
}
$path = realpath($target);
if ($path === false) {
mkdir($path = $target, 0755, true);
}
$packer->setOutputPath($path);
$packer->setOverride();
$packer->pack();
return true;
} catch (ModulePackException $e) {
Console::error($e->getMessage());
return false;
}
}
/**
* 解包模块
* @param array|Iterator $module 模块信息
* @return array|false 返回解包的信息或false
*/
public static function unpackModule($module, array $options = [])
{
try {
$packer = new ModuleUnpacker($module);
return $packer->unpack((bool) $options['overwrite-light-cache'], (bool) $options['overwrite-zm-data'], (bool) $options['overwrite-source'], (bool) $options['ignore-depends']);
} catch (ZMException $e) {
Console::error($e->getMessage());
return false;
}
}
private static function getComposerModuleInfo($v, $module_path)
{
$module_root_path = realpath(DataProvider::getSourceRootDir() . '/vendor/composer/' . $v['install-path'] . '/' . $module_path);
if ($module_root_path === false) {
logger()->warning(zm_internal_errcode('E00055') . '无法找到Composer发布的插件配置路径在包 `' . $v['name'] . '` 中!');
return null;
}
$json = json_decode(file_get_contents($module_root_path . '/zm.json'), true);
if ($json === null) {
logger()->warning(zm_internal_errcode('E00054') . 'Composer包内无法正常读取 ' . $v['name'] . ' 的内的配置文件zm.json');
return null;
}
if (!isset($json['name'])) {
return null;
}
$json['composer-name'] = $v['name'];
$json['module-root-path'] = realpath(DataProvider::getSourceRootDir() . '/vendor/composer/' . $v['install-path']);
$json['module-path'] = realpath($json['module-root-path'] . '/' . $module_path);
if (isset($v['autoload']['psr-4'])) {
foreach ($v['autoload']['psr-4'] as $ks => $vs) {
$vs = trim($vs, '/');
if (strpos($module_path, $vs) === 0) {
$json['namespace'] = trim($ks . str_replace('/', '\\', trim(substr($module_path, strlen($vs)), '/')), '\\');
break;
}
}
}
if (!isset($json['namespace'])) {
logger()->warning(zm_internal_errcode('E00055') . '无法获取Composer发布的模块命名空间');
return null;
}
return $json;
}
}

View File

@@ -1,175 +0,0 @@
<?php
/** @noinspection PhpUnused */
declare(strict_types=1);
namespace ZM\Utils\Manager;
use Swoole\Process;
use ZM\Exception\ZMKnownException;
use ZM\Utils\DataProvider;
class ProcessManager
{
/** @var Process[] */
public static $user_process = [];
public static function createUserProcess(string $name, callable $callable): Process
{
return self::$user_process[$name] = new Process($callable);
}
public static function getUserProcess(string $string): ?Process
{
return self::$user_process[$string] ?? null;
}
/**
* @param null|int|string $id_or_name
* @throws ZMKnownException
* @internal
*/
public static function removeProcessState(int $type, $id_or_name = null)
{
switch ($type) {
case ZM_PROCESS_MASTER:
$file = _zm_pid_dir() . '/master.json';
if (file_exists($file)) {
unlink($file);
}
return;
case ZM_PROCESS_MANAGER:
$file = _zm_pid_dir() . '/manager.pid';
if (file_exists($file)) {
unlink($file);
}
return;
case ZM_PROCESS_WORKER:
if (!is_int($id_or_name)) {
throw new ZMKnownException('E99999', 'worker_id必须为整数');
}
$file = _zm_pid_dir() . '/worker.' . $id_or_name . '.pid';
if (file_exists($file)) {
unlink($file);
}
return;
case ZM_PROCESS_USER:
if (!is_string($id_or_name)) {
throw new ZMKnownException('E99999', 'process_name必须为字符串');
}
$file = _zm_pid_dir() . '/user.' . $id_or_name . '.pid';
if (file_exists($file)) {
unlink($file);
}
return;
case ZM_PROCESS_TASKWORKER:
if (!is_int($id_or_name)) {
throw new ZMKnownException('E99999', 'worker_id必须为整数');
}
$file = _zm_pid_dir() . '/taskworker.' . $id_or_name . '.pid';
if (file_exists($file)) {
unlink($file);
}
return;
}
}
/**
* 用于框架内部获取多进程运行状态的函数
*
* @param mixed $id_or_name
* @throws ZMKnownException
* @return false|int|mixed
* @internal
*/
public static function getProcessState(int $type, $id_or_name = null)
{
$file = _zm_pid_dir();
switch ($type) {
case ZM_PROCESS_MASTER:
if (!file_exists($file . '/master.json')) {
return false;
}
$json = json_decode(file_get_contents($file . '/master.json'), true);
if ($json !== null) {
return $json;
}
return false;
case ZM_PROCESS_MANAGER:
if (!file_exists($file . '/manager.pid')) {
return false;
}
return intval(file_get_contents($file . '/manager.pid'));
case ZM_PROCESS_WORKER:
if (!is_int($id_or_name)) {
throw new ZMKnownException('E99999', 'worker_id必须为整数');
}
if (!file_exists($file . '/worker.' . $id_or_name . '.pid')) {
return false;
}
return intval(file_get_contents($file . '/worker.' . $id_or_name . '.pid'));
case ZM_PROCESS_USER:
if (!is_string($id_or_name)) {
throw new ZMKnownException('E99999', 'process_name必须为字符串');
}
if (!file_exists($file . '/user.' . $id_or_name . '.pid')) {
return false;
}
return intval(file_get_contents($file . '/user.' . $id_or_name . '.pid'));
case ZM_PROCESS_TASKWORKER:
if (!is_int($id_or_name)) {
throw new ZMKnownException('E99999', 'worker_id必须为整数');
}
if (!file_exists($file . '/taskworker.' . $id_or_name . '.pid')) {
return false;
}
return intval(file_get_contents($file . '/taskworker.' . $id_or_name . '.pid'));
default:
return false;
}
}
/**
* 将各进程的pid写入文件以备后续崩溃及僵尸进程处理使用
*
* @param int|string $pid
* @internal
*/
public static function saveProcessState(int $type, $pid, array $data = [])
{
switch ($type) {
case ZM_PROCESS_MASTER:
$file = _zm_pid_dir() . '/master.json';
$json = [
'pid' => intval($pid),
'stdout' => $data['stdout'],
'daemon' => $data['daemon'],
];
file_put_contents($file, json_encode($json, JSON_UNESCAPED_UNICODE));
return;
case ZM_PROCESS_MANAGER:
$file = _zm_pid_dir() . '/manager.pid';
file_put_contents($file, strval($pid));
return;
case ZM_PROCESS_WORKER:
$file = _zm_pid_dir() . '/worker.' . $data['worker_id'] . '.pid';
file_put_contents($file, strval($pid));
return;
case ZM_PROCESS_USER:
$file = _zm_pid_dir() . '/user.' . $data['process_name'] . '.pid';
file_put_contents($file, strval($pid));
return;
case ZM_PROCESS_TASKWORKER:
$file = _zm_pid_dir() . '/taskworker.' . $data['worker_id'] . '.pid';
file_put_contents($file, strval($pid));
return;
}
}
public static function isStateEmpty(): bool
{
$ls = DataProvider::scanDirFiles(_zm_pid_dir(), false, true);
return empty($ls);
}
}

View File

@@ -1,66 +0,0 @@
<?php
declare(strict_types=1);
namespace ZM\Utils\Manager;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use ZM\Annotation\Http\Controller;
use ZM\Annotation\Http\RequestMapping;
use ZM\Http\StaticFileHandler;
/**
* 路由管理器2.5版本更改了命名空间
* Class RouteManager
* @since 2.3.0
*/
class RouteManager
{
/** @var null|RouteCollection */
public static $routes;
public static function importRouteByAnnotation(RequestMapping $vss, $method, $class, $methods_annotations)
{
if (self::$routes === null) {
self::$routes = new RouteCollection();
}
// 拿到所属方法的类上面有没有控制器的注解
$prefix = '';
foreach ($methods_annotations as $annotation) {
if ($annotation instanceof Controller) {
$prefix = $annotation->prefix;
break;
}
}
$tail = trim($vss->route, '/');
$route_name = $prefix . ($tail === '' ? '' : '/') . $tail;
logger()->debug('添加路由:' . $route_name);
$route = new Route($route_name, ['_class' => $class, '_method' => $method]);
$route->setMethods($vss->request_method);
self::$routes->add(md5($route_name), $route);
}
public static function addStaticFileRoute($route, $path)
{
$tail = trim($route, '/');
$route_name = ($tail === '' ? '' : '/') . $tail . '/{filename}';
logger()->debug('添加静态文件路由:' . $route_name);
$route = new Route($route_name, ['_class' => __CLASS__, '_method' => 'onStaticRoute'], [], compact('path'));
self::$routes->add(md5($route_name), $route);
}
public function onStaticRoute(array $params)
{
if (($path = self::$routes->get($params['_route'])->getOption('path')) === null) {
ctx()->getResponse()->endWithStatus(404);
return false;
}
unset($params['_route']);
$obj = array_shift($params);
return new StaticFileHandler($obj, $path);
}
}

View File

@@ -1,26 +0,0 @@
<?php
/** @noinspection PhpUnused */
declare(strict_types=1);
namespace ZM\Utils\Manager;
class TaskManager
{
/**
* @param string $task_name 任务名称
* @param int $timeout 超时时间
* @param mixed ...$params 传递参数
* @return false|mixed 执行结果如果执行失败返回false否则为执行结果
*/
public static function runTask(string $task_name, int $timeout = -1, ...$params)
{
if (!isset(server()->setting['task_worker_num'])) {
logger()->warning(zm_internal_errcode('E00056') . '未开启 TaskWorker 进程,请先修改 global 配置文件启用!');
return false;
}
$r = server()->taskwait(['task' => $task_name, 'params' => $params], $timeout);
return $r === false ? false : $r['result'];
}
}

View File

@@ -1,144 +0,0 @@
<?php
declare(strict_types=1);
namespace ZM\Utils\Manager;
use Exception;
use Swoole\Coroutine;
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\Swoole\OnPipeMessageEvent;
use ZM\Event\EventDispatcher;
use ZM\Event\EventManager;
use ZM\Store\LightCache;
use ZM\Store\LightCacheInside;
use ZM\Store\WorkerCache;
class WorkerManager
{
/**
* Worker 进程间通信触发的动作类型函数
* @param int $src_worker_id 源 Worker 进程 ID
* @param array $data 数据
* @throws Exception
*/
public static function workerAction(int $src_worker_id, array $data)
{
$server = server();
switch ($data['action'] ?? '') {
case 'add_short_command':
logger()->debug('Adding short command ' . $data['data'][0]);
$obj = new CQCommand();
$obj->method = quick_reply_closure($data['data'][1]);
$obj->match = $data['data'][0];
$obj->class = '';
EventManager::addEvent(CQCommand::class, $obj);
break;
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'];
Coroutine::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;
}
}
/**
* 给 Worker 进程发送动作指令(包括自身,自身将直接执行)
* @param int $worker_id 进程ID
* @param string $action 动作
* @param mixed $data 参数
* @throws Exception
*/
public static function sendActionToWorker(int $worker_id, string $action, $data)
{
$obj = ['action' => $action, 'data' => $data];
if (server()->worker_id === -1 && server()->getManagerPid() != posix_getpid()) {
logger()->warning(zm_internal_errcode('E00022') . '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);
}
}
/**
* 向所有 Worker 进程发送动作指令
*/
public static function resumeAllWorkerCoroutines()
{
if (server()->worker_id === -1) {
logger()->warning("Cannot call '" . __FUNCTION__ . "' in non-worker process!");
return;
}
foreach ((LightCacheInside::get('wait_api', 'wait_api') ?? []) as $v) {
if (isset($v['coroutine'], $v['worker_id'])) {
if (server()->worker_id == $v['worker_id']) {
Coroutine::resume($v['coroutine']);
}
}
}
}
}

View File

@@ -1,348 +0,0 @@
<?php
declare(strict_types=1);
namespace ZM\Utils;
use Exception;
use Iterator;
use ZM\Annotation\CQ\CommandArgument;
use ZM\Annotation\CQ\CQCommand;
use ZM\API\CQ;
use ZM\Config\ZMConfig;
use ZM\Entity\InputArguments;
use ZM\Entity\MatchResult;
use ZM\Event\EventManager;
use ZM\Event\EventMapIterator;
use ZM\Exception\WaitTimeoutException;
use ZM\Requests\ZMRequest;
use ZM\Utils\Manager\WorkerManager;
class MessageUtil
{
/**
* 下载消息中 CQ 码的所有图片,通过 url
* @param array|string $msg 消息或消息数组
* @param null|string $path 保存路径
* @return array|false 返回图片信息或失败返回false
*/
public static function downloadCQImage($msg, ?string $path = null)
{
if ($path === null) {
$path = DataProvider::getDataFolder() . 'images/';
}
if (!is_dir($path)) {
@mkdir($path);
}
$path = realpath($path);
if ($path === false) {
logger()->warning(zm_internal_errcode('E00059') . '指定的路径错误不存在!');
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) {
logger()->warning(zm_internal_errcode('E00060') . '图片 ' . $v->params['url'] . ' 下载失败!');
return false;
}
$files[] = $path . '/' . $v->params['file'];
}
}
return $files;
}
/**
* 检查消息中是否含有图片 CQ 码
* @param array|string $msg 消息或消息数组
*/
public static function containsImage($msg): bool
{
$cq = CQ::getAllCQ($msg, true);
foreach ($cq as $v) {
if ($v->type == 'image') {
return true;
}
}
return false;
}
public static function isAtMe($msg, $me_id): bool
{
return strpos($msg, CQ::at($me_id)) !== false;
}
/**
* 通过本地地址返回图片的 CQ 码
* type == 0 : 返回图片的 base64 CQ 码
* type == 1 : 返回图片的 file://路径 CQ 码(路径必须为绝对路径)
* type == 2 : 返回图片的 http://xxx CQ 码(默认为 /images/ 路径就是文件对应所在的目录)
* @param string $file 文件数据
* @param int $type 文件类型012可选默认为0
*/
public static function getImageCQFromLocal(string $file, int $type = 0): string
{
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 '';
}
/**
* 分割字符,将用户消息通过空格或换行分割为数组
* @param string $msg 消息内容
* @return array|string[]
*/
public static function splitCommand(string $msg): array
{
$word = explode_msg(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($v);
}
}
return $word;
}
/**
* 根据CQCommand的规则匹配消息获取是否匹配到对应的注解事件
* @param array|string $msg 消息内容
* @param array|Iterator $obj 数据对象
*/
public static function matchCommand($msg, $obj): MatchResult
{
$ls = EventManager::$events[CQCommand::class] ?? [];
if (is_array($msg)) {
$msg = self::arrayToStr($msg);
}
$word = self::splitCommand($msg);
$matched = new MatchResult();
foreach ($ls as $v) {
if (array_diff([$v->match, $v->pattern, $v->regex, $v->keyword, $v->end_with, $v->start_with], ['']) == []) {
continue;
}
if (($v->user_id == 0 || ($v->user_id == $obj['user_id']))
&& ($v->group_id == 0 || ($v->group_id == ($obj['group_id'] ?? 0)))
&& ($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;
}
if ($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;
}
if ($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;
}
if ($v->keyword != '' && mb_strpos($msg, $v->keyword) !== false) {
$matched->match = explode($v->keyword, $msg);
$matched->object = $v;
$matched->status = true;
break;
}
if ($v->pattern != '') {
$match = match_args($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;
}
/**
* @param string $command 命令内容
* @param string $reply 回复内容
* @throws Exception
*/
public static function addShortCommand(string $command, string $reply)
{
for ($i = 0; $i < ZM_WORKER_NUM; ++$i) {
WorkerManager::sendActionToWorker($i, 'add_short_command', [$command, $reply]);
}
}
/**
* 字符串转数组
* @param string $msg 消息内容
* @param bool $ignore_space 是否忽略空行
* @param bool $trim_text 是否去除空格
* @return array 返回数组
*/
public static function strToArray(string $msg, bool $ignore_space = true, bool $trim_text = false): array
{
$arr = [];
while (($rear = mb_strstr($msg, '[CQ:')) !== false && ($end = mb_strstr($rear, ']', true)) !== false) {
// 把 [CQ: 前面的文字生成段落
$front = mb_strstr($msg, '[CQ:', true);
// 如果去掉空格都还有文字,或者不去掉空格有字符,且不忽略空格,则生成段落,否则不生成
if (($trim_front = trim($front)) !== '' || ($front !== '' && !$ignore_space)) {
$arr[] = ['type' => 'text', 'data' => ['text' => CQ::decode($trim_text ? $trim_front : $front)]];
}
// 处理 CQ 码
$content = mb_substr($end, 4);
$cq = explode(',', $content);
$object_type = array_shift($cq);
$object_params = [];
foreach ($cq as $v) {
$key = mb_strstr($v, '=', true);
$object_params[$key] = CQ::decode(mb_substr(mb_strstr($v, '='), 1), true);
}
$arr[] = ['type' => $object_type, 'data' => $object_params];
$msg = mb_substr(mb_strstr($rear, ']'), 1);
}
if (($trim_msg = trim($msg)) !== '' || ($msg !== '' && !$ignore_space)) {
$arr[] = ['type' => 'text', 'data' => ['text' => CQ::decode($trim_text ? $trim_msg : $msg)]];
}
return $arr;
}
/**
* 数组转字符串
* 纪念一下这段代码完全由AI生成没有人知道它是怎么写的这句话是我自己写的不知道是不是有人知道的
* @author Copilot
*/
public static function arrayToStr(array $array): string
{
$str = '';
foreach ($array as $v) {
if ($v['type'] == 'text') {
$str .= $v['data']['text'];
} else {
$str .= '[CQ:' . $v['type'];
foreach ($v['data'] as $key => $value) {
$str .= ',' . $key . '=' . CQ::encode($value, true);
}
$str .= ']';
}
}
return $str;
}
/**
* @throws WaitTimeoutException
*/
public static function checkArguments(string $class, string $method, array &$match): array
{
$iterator = new EventMapIterator($class, $method, CommandArgument::class);
$offset = 0;
$arguments = [];
foreach ($iterator as $annotation) {
/** @var CommandArgument $annotation */
switch ($annotation->type) {
case 'string':
case 'any':
if (isset($match[$offset])) {
$arguments[$annotation->name] = $match[$offset++];
} else {
if ($annotation->required) {
$value = ctx()->waitMessage($annotation->prompt === '' ? ('请输入' . $annotation->name) : $annotation->prompt, $annotation->timeout);
$arguments[$annotation->name] = $value;
} else {
$arguments[$annotation->name] = $annotation->default;
}
}
break;
case 'number':
for ($k = $offset; $k < count($match); ++$k) {
$v = $match[$k];
if (is_numeric($v)) {
array_splice($match, $k, 1);
$arguments[$annotation->name] = $v / 1;
break 2;
}
}
if (!$annotation->required) {
if (is_numeric($annotation->default)) {
$arguments[$annotation->name] = $annotation->default / 1;
}
}
if (!isset($arguments[$annotation->name])) {
$value = ctx()->waitMessage($annotation->prompt === '' ? ('请输入' . $annotation->name) : $annotation->prompt, $annotation->timeout);
if (!is_numeric($value)) {
if ($annotation->error_prompt_policy === 1) {
$value = ctx()->waitMessage($annotation->getTypeErrorPrompt(), $annotation->timeout);
if (!is_numeric($value)) {
throw new WaitTimeoutException(ctx(), $annotation->getErrorQuitPrompt());
}
} else {
throw new WaitTimeoutException(ctx(), $annotation->getErrorQuitPrompt());
}
}
$arguments[$annotation->name] = $value / 1;
}
break;
case 'bool':
for ($k = $offset; $k < count($match); ++$k) {
$v = strtolower($match[$k]);
if (in_array(strtolower($v), TRUE_LIST)) {
array_splice($match, $k, 1);
$arguments[$annotation->name] = true;
break 2;
}
if (in_array(strtolower($v), FALSE_LIST)) {
array_splice($match, $k, 1);
$arguments[$annotation->name] = false;
break 2;
}
}
if (!$annotation->required) {
$default = $annotation->default === '' ? true : 'true';
$arguments[$annotation->name] = in_array($default, TRUE_LIST);
}
if (!isset($arguments[$annotation->name])) {
$value = strtolower(ctx()->waitMessage($annotation->prompt === '' ? ('请输入' . $annotation->name) : $annotation->prompt, $annotation->timeout));
if (!in_array($value, array_merge(TRUE_LIST, FALSE_LIST))) {
if ($annotation->error_prompt_policy === 1) {
$value = strtolower(ctx()->waitMessage($annotation->getTypeErrorPrompt(), $annotation->timeout));
if (!in_array($value, array_merge(TRUE_LIST, FALSE_LIST))) {
throw new WaitTimeoutException(ctx(), $annotation->getErrorQuitPrompt());
}
} else {
throw new WaitTimeoutException(ctx(), $annotation->getErrorQuitPrompt());
}
}
$arguments[$annotation->name] = in_array($value, TRUE_LIST);
}
break;
}
}
container()->instance(InputArguments::class, new InputArguments($arguments));
return $arguments;
}
}

View File

@@ -1,123 +0,0 @@
<?php
declare(strict_types=1);
namespace ZM\Utils;
use Closure;
use ReflectionException;
use ReflectionFunction;
use ReflectionFunctionAbstract;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionParameter;
class ReflectionUtil
{
/**
* 获取参数的类名(如有)
*
* @param ReflectionParameter $parameter 参数
* @return null|string 类名,如果参数不是类,返回 null
*/
public static function getParameterClassName(ReflectionParameter $parameter): ?string
{
// 获取参数类型
$type = $parameter->getType();
// 没有声明类型或为基本类型
if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) {
return null;
}
// 获取类名
$class_name = $type->getName();
// 如果存在父类
if (!is_null($class = $parameter->getDeclaringClass())) {
if ($class_name === 'self') {
return $class->getName();
}
if ($class_name === 'parent' && $parent = $class->getParentClass()) {
return $parent->getName();
}
}
return $class_name;
}
/**
* 将传入变量转换为字符串
*
* @param mixed $var
*/
public static function variableToString($var): string
{
switch (true) {
case is_callable($var):
if (is_array($var)) {
if (is_object($var[0])) {
return get_class($var[0]) . '@' . $var[1];
}
return $var[0] . '::' . $var[1];
}
return 'closure';
case is_string($var):
return $var;
case is_array($var):
return 'array' . json_encode($var);
case is_object($var):
return get_class($var);
case is_resource($var):
return 'resource(' . get_resource_type($var) . ')';
case is_null($var):
return 'null';
case is_bool($var):
return $var ? 'true' : 'false';
case is_float($var):
case is_int($var):
return (string) $var;
default:
return 'unknown';
}
}
/**
* 判断传入的回调是否为任意类的非静态方法
*
* @param callable|string $callback 回调
* @throws ReflectionException
*/
public static function isNonStaticMethod($callback): bool
{
if (is_array($callback) && is_string($callback[0])) {
$reflection = new ReflectionMethod($callback[0], $callback[1]);
return !$reflection->isStatic();
}
return false;
}
/**
* 获取传入的回调的反射实例
*
* 如果传入的是类方法,则会返回 {@link ReflectionMethod} 实例
* 否则将返回 {@link ReflectionFunction} 实例
*
* 可传入实现了 __invoke 的类
*
* @param callable|string $callback 回调
* @throws ReflectionException
*/
public static function getCallReflector($callback): ReflectionFunctionAbstract
{
if (is_string($callback) && str_contains($callback, '::')) {
$callback = explode('::', $callback);
} elseif (is_object($callback) && !$callback instanceof Closure) {
$callback = [$callback, '__invoke'];
}
return is_array($callback)
? new ReflectionMethod($callback[0], $callback[1])
: new ReflectionFunction($callback);
}
}

View File

@@ -1,110 +0,0 @@
<?php
declare(strict_types=1);
namespace ZM\Utils;
use Swoole\Process;
use Swoole\Server;
/**
* 炸毛框架的Linux signal管理类
* Class SignalListener
*
* @since 2.5
*/
class SignalListener
{
private static $manager_kill_time = 0;
/**
* 监听Master进程的Ctrl+C
*/
public static function signalMaster(Server $server)
{
logger()->debug('正在监听 Master 进程 SIGINT');
Process::signal(SIGINT, function () use ($server) {
if (zm_atomic('_int_is_reload')->get() === 1) {
zm_atomic('_int_is_reload')->set(0);
$server->reload();
} else {
echo "\r";
logger()->notice('Master 进程收到中断信号 SIGINT');
logger()->notice('正在停止服务器');
Process::kill($server->master_pid, SIGTERM);
}
});
}
/**
* 监听Manager进程的Ctrl+C
*/
public static function signalManager()
{
$func = function () {
if (\server()->master_pid == \server()->manager_pid) {
echo "\r";
logger()->notice('Manager 进程收到中断信号 SIGINT');
swoole_timer_after(2, function () {
Process::kill(posix_getpid(), SIGTERM);
});
} else {
logger()->debug('Manager 已中断');
}
self::processKillerPrompt();
};
logger()->debug('正在监听 Manager 进程 SIGINT');
if (version_compare(SWOOLE_VERSION, '4.6.7') >= 0) {
Process::signal(SIGINT, $func);
} elseif (extension_loaded('pcntl')) {
pcntl_signal(SIGINT, $func);
}
}
/**
* 监听Worker/TaskWorker进程的Ctrl+C
*
* @param int $worker_id 当前进程的ID
*/
public static function signalWorker(Server $server, int $worker_id)
{
logger()->debug('正在监听 Worker#{worker_id} 进程 SIGINT', compact('worker_id'));
Process::signal(SIGINT, function () use ($server) {
if ($server->master_pid === $server->worker_pid) { // 当Swoole以单进程模型运行的时候Worker需要监听杀死的信号
echo "\r";
logger()->notice('Worker 进程收到中断信号 SIGINT');
swoole_timer_after(2, function () {
Process::kill(posix_getpid(), SIGTERM);
});
self::processKillerPrompt();
}
// logger()->debug("Interrupted in worker");
// do nothing
});
}
/**
* 按5次Ctrl+C后强行杀死框架的处理函数
*/
private static function processKillerPrompt()
{
if (self::$manager_kill_time > 0) {
if (self::$manager_kill_time >= 5) {
$file_path = _zm_pid_dir();
$flist = DataProvider::scanDirFiles($file_path, false, true);
foreach ($flist as $file) {
$name = explode('.', $file);
if (end($name) == 'pid' && $name[0] !== 'manager') {
$pid = file_get_contents($file_path . '/' . $file);
Process::kill((int) $pid, SIGKILL);
}
unlink($file_path . '/' . $file);
}
} else {
echo "\r";
logger()->notice('请再按 {count} 次 Ctrl+C 以强制杀死所有 Worker 进程', ['count' => 5 - self::$manager_kill_time]);
}
}
++self::$manager_kill_time;
}
}

View File

@@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
namespace ZM\Utils;
trait SingletonTrait
{
/**
* @deprecated 将会于未来版本移除
*
* @var array
*/
protected static $cached = [];
/**
* @var null|static
*/
protected static $instance;
/**
* 获取类实例
*
* @return static
*/
public static function getInstance()
{
if (is_null(static::$instance)) {
static::$instance = new static();
}
return static::$instance;
}
}

View File

@@ -1,200 +0,0 @@
<?php
/** @noinspection PhpUnused */
declare(strict_types=1);
namespace ZM\Utils;
use Doctrine\Common\Annotations\AnnotationReader;
use Exception;
use ReflectionClass;
use Swoole\Process;
use Throwable;
use ZM\Annotation\Command\TerminalCommand;
use ZM\ConnectionManager\ManagerGM;
use ZM\Console\Console;
use ZM\Event\EventDispatcher;
use ZM\Event\EventManager;
class Terminal
{
public static $default_commands = false;
/**
* @throws Throwable
* @return bool
* @noinspection PhpMissingReturnTypeInspection
* @noinspection PhpUnused
*/
public static function executeCommand(string $cmd)
{
if (self::$default_commands === false) {
self::init();
}
$it = explode_msg($cmd);
$dispatcher = new EventDispatcher(TerminalCommand::class);
$dispatcher->setRuleFunction(function ($v) use ($it) {
/* @var TerminalCommand $v */
return !empty($it) && ($v->command == $it[0] || $v->alias == $it[0]);
});
$dispatcher->setReturnFunction(function () {
EventDispatcher::interrupt('none');
});
$dispatcher->dispatchEvents($it);
if ($dispatcher->store !== 'none' && $cmd !== '') {
logger()->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 $v) {
server()->send($v->getFd(), "\r" . $r);
server()->send($v->getFd(), '>>> ');
}
}
public static function init()
{
logger()->debug('Initializing Terminal...');
foreach ((EventManager::$events[TerminalCommand::class] ?? []) as $v) {
if ($v->command == 'help') {
self::$default_commands = true;
break;
}
}
$class = new Terminal();
$reader = new AnnotationReader();
$reflection = new ReflectionClass($class);
foreach ($reflection->getMethods() as $v) {
$r = $reader->getMethodAnnotation($v, TerminalCommand::class);
if ($r !== null) {
logger()->debug('adding command ' . $r->command);
$r->class = Terminal::class;
$r->method = $v->getName();
EventManager::addEvent(TerminalCommand::class, $r);
}
}
self::$default_commands = true;
}
/**
* @TerminalCommand(command="help",alias="h",description="显示帮助菜单")
*/
public function help()
{
$help = [];
foreach ((EventManager::$events[TerminalCommand::class] ?? []) as $v) {
/** @var TerminalCommand $v */
$cmd = $v->command . ($v->alias !== '' ? (' | ' . $v->alias) : '');
$painted = Console::setColor($v->command, 'green') . ($v->alias !== '' ? (' | ' . Console::setColor($v->alias, 'green')) : '');
$help[] = $painted . ':' . str_pad('', 16 - strlen($cmd) - 1) . ($v->description === '' ? '<无描述>' : $v->description);
}
echo implode("\n", $help) . PHP_EOL;
}
/**
* @TerminalCommand(command="status",description="显示Swoole Server运行状态需要安装league/climate组件")
*/
public function status()
{
if (class_exists('\\League\\CLImate\\CLImate')) {
$class = '\\League\\CLImate\\CLImate';
$climate = new $class();
$climate->output->addDefault('buffer');
$objs = server()->stats();
$climate->columns($objs);
$obj = $climate->output->get('buffer')->get();
$climate->output->get('buffer')->clean();
echo $obj;
return;
}
logger()->warning('你还没有安装 league/climate 组件,无法使用此功能!');
}
/**
* @TerminalCommand(command="logtest",description="测试log的显示等级")
*/
public function testlog()
{
foreach (['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'] as $level) {
logger()->log($level, 'This is a {level} message.', compact('level'));
}
}
/**
* @TerminalCommand(command="call",description="用于执行不需要参数的动态函数,比如 `call \Module\Example\Hello hitokoto`")
*/
public function call(array $it)
{
$class_name = $it[1];
$function_name = $it[2];
$class = new $class_name([]);
$r = $class->{$function_name}();
if (is_string($r)) {
Console::success($r);
}
}
/**
* @TerminalCommand(command="level",description="设置log等级例如 `level 0|1|2|3|4`")
*/
public function level(array $it)
{
logger()->warning('Sorry, this function is not available yet.');
// $level = intval(is_numeric($it[1] ?? 99) ? ($it[1] ?? 99) : 99);
// if ($level > 4 || $level < 0) {
// Console::warning("Usage: 'level 0|1|2|3|4'");
// } else {
// Console::setLevel($level) || Console::success('Success!!');
// }
}
/**
* @TerminalCommand(command="bc",description="eval执行代码但输入必须是将代码base64之后的如 `bc em1faW5mbygn5L2g5aW9Jyk7`")
*/
public function bc(array $it)
{
$code = base64_decode($it[1] ?? '', true);
try {
eval($code);
} catch (Exception $e) {
}
}
/**
* @TerminalCommand(command="echo",description="输出内容,用法:`echo hello`")
*/
public function echoI(array $it)
{
logger()->info($it[1]);
}
/**
* @TerminalCommand(command="stop",description="停止框架")
*/
public function stop()
{
posix_kill(server()->master_pid, SIGTERM);
}
/**
* @TerminalCommand(command="reload",alias="r",description="重启框架(重载用户代码)")
*/
public function reload()
{
Process::kill(server()->master_pid, SIGUSR1);
}
}

View File

@@ -4,133 +4,6 @@ declare(strict_types=1);
namespace ZM\Utils;
use Exception;
use Psr\Log\LogLevel;
use Swoole\Process;
use ZM\Framework;
use ZM\Store\Lock\SpinLock;
use ZM\Store\ZMAtomic;
use function file_get_contents;
use function get_included_files;
use function is_callable;
use function is_string;
use function json_decode;
use function mb_substr;
use function md5_file;
use function pathinfo;
use function server;
use function str_replace;
class ZMUtil
{
/**
* @param mixed $error_exit
* @throws Exception
*/
public static function stop($error_exit = false)
{
if (SpinLock::tryLock('_stop_signal') === false) {
return;
}
logger()->notice('正在停止服务器...');
if (zm_config('logging.level') === LogLevel::DEBUG) {
debug_print_backtrace();
}
ZMAtomic::get('stop_signal')->set($error_exit ? 2 : 1);
server()->shutdown();
}
/**
* @throws Exception
*/
public static function reload()
{
Process::kill(server()->master_pid, SIGUSR1);
}
public static function getModInstance($class)
{
return resolve($class);
}
/**
* 在工作进程中返回可以通过reload重新加载的php文件列表
* @return string[]|string[][]
*/
public static function getReloadableFiles(): array
{
$array_map = [];
foreach (array_diff(
get_included_files(),
Framework::$loaded_files
) as $key => $x) {
$array_map[$key] = str_replace(DataProvider::getSourceRootDir() . '/', '', $x);
}
return $array_map;
}
/**
* 使用Psr-4标准获取目录下的所有类
* @param string $dir 目录
* @param string $base_namespace 基础命名空间
* @param null|mixed $rule 规则
* @param bool|string $return_path_value 是否返回文件路径,返回文件路径的话传入字符串
* @return string[]
*/
public static function getClassesPsr4(string $dir, string $base_namespace, $rule = null, $return_path_value = false): array
{
// 预先读取下composer的file列表
$composer = json_decode(file_get_contents(DataProvider::getSourceRootDir() . '/composer.json'), true);
$classes = [];
// 扫描目录使用递归模式相对路径模式因为下面此路径要用作转换成namespace
$files = DataProvider::scanDirFiles($dir, true, true);
foreach ($files as $v) {
$pathinfo = pathinfo($v);
if (($pathinfo['extension'] ?? '') == 'php') {
$path = rtrim($dir, '/') . '/' . rtrim($pathinfo['dirname'], './') . '/' . $pathinfo['basename'];
// 过滤不包含类的文件
$tokens = token_get_all(file_get_contents($path));
$found = false;
foreach ($tokens as $token) {
if (!is_array($token)) {
continue;
}
if ($token[0] === T_CLASS) {
$found = true;
}
}
if (!$found) {
continue;
}
if ($rule === null) { // 规则未设置回调时候,使用默认的识别过滤规则
/*if (substr(file_get_contents($dir . '/' . $v), 6, 6) == '#plain') {
continue;
}*/
if (file_exists($dir . '/' . $pathinfo['basename'] . '.plain')) {
continue;
}
if (mb_substr($pathinfo['basename'], 0, 7) == 'global_' || mb_substr($pathinfo['basename'], 0, 7) == 'script_') {
continue;
}
foreach (($composer['autoload']['files'] ?? []) as $fi) {
if (md5_file(DataProvider::getSourceRootDir() . '/' . $fi) == md5_file($dir . '/' . $v)) {
continue 2;
}
}
} elseif (is_callable($rule) && !($rule($dir, $pathinfo))) {
continue;
}
$dirname = $pathinfo['dirname'] == '.' ? '' : (str_replace('/', '\\', $pathinfo['dirname']) . '\\');
$class_name = $base_namespace . '\\' . $dirname . $pathinfo['filename'];
if (is_string($return_path_value)) {
$classes[$class_name] = $return_path_value . '/' . $v;
} else {
$classes[] = $class_name;
}
}
}
return $classes;
}
}