From d188936c17761992cb08cddb9a765c249dae2b8a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 17 Jun 2026 15:11:57 +0800 Subject: [PATCH] fix: add SIGHUP/SIGTERM handling, modernize PHP support and CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signal handling fixes: - SignalListener: add SIGHUP/SIGTERM handling for both Swoole and Workerman drivers in master and worker processes - Prevent 100% CPU when IDE terminal is closed by ensuring graceful shutdown on terminal hangup PHP version support: - Widen PHP constraint to 8.3, 8.4, 8.5 - Bump doctrine/dbal from ^2.13.1 to ^4.4 - Bump php-cs-fixer to ^3.64, phpstan to ^1.12 - Bump swoole/ide-helper to ^5.0 - Drop phpunit ^8.5 (EOL), keep ^9.0 CI updates: - actions/checkout@v3 → @v4 (Node.js 20 deprecated) - Bump static analysis/code style PHP from 8.1 to 8.3 --- .github/workflows/build-release-artifacts.yml | 8 +++--- .github/workflows/coding-style.yml | 4 +-- .github/workflows/increment-build-number.yml | 4 +-- .github/workflows/static-analysis.yml | 4 +-- .github/workflows/test.yml | 2 +- .github/workflows/vuepress-deploy.yml | 2 +- composer.json | 10 +++---- src/Globals/global_functions.php | 2 +- src/ZM/Annotation/OneBot/CommandArgument.php | 2 +- src/ZM/Context/BotContext.php | 4 +-- src/ZM/Event/Listener/SignalListener.php | 28 +++++++++++-------- src/ZM/Framework.php | 4 +-- src/ZM/Process/ProcessStateManager.php | 2 +- src/ZM/Store/KV/LightCache.php | 4 +-- src/ZM/Store/KV/Redis/KVRedis.php | 4 +-- src/ZM/Utils/MessageUtil.php | 2 +- 16 files changed, 45 insertions(+), 41 deletions(-) diff --git a/.github/workflows/build-release-artifacts.yml b/.github/workflows/build-release-artifacts.yml index 7f746675..fda313a4 100644 --- a/.github/workflows/build-release-artifacts.yml +++ b/.github/workflows/build-release-artifacts.yml @@ -12,14 +12,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.base_ref }} - name: Setup PHP uses: sunxyw/workflows/setup-environment@main with: - php-version: 8.1 + php-version: 8.3 php-extensions: swoole, posix, json operating-system: ubuntu-latest use-cache: true @@ -40,14 +40,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.base_ref }} - name: Setup PHP uses: sunxyw/workflows/setup-environment@main with: - php-version: 8.1 + php-version: 8.3 php-extensions: swoole, posix, json operating-system: ubuntu-latest use-cache: true diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index 4add0e61..d5ce787e 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -27,14 +27,14 @@ jobs: timeout-minutes: 10 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup PHP uses: sunxyw/workflows/setup-environment@main with: - php-version: 8.1 + php-version: 8.3 php-extensions: swoole, posix, json operating-system: ubuntu-latest use-cache: true diff --git a/.github/workflows/increment-build-number.yml b/.github/workflows/increment-build-number.yml index bbb4bb8b..015fdba6 100644 --- a/.github/workflows/increment-build-number.yml +++ b/.github/workflows/increment-build-number.yml @@ -16,14 +16,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.base_ref }} - name: Setup PHP uses: sunxyw/workflows/setup-environment@main with: - php-version: 8.1 + php-version: 8.3 php-extensions: swoole, posix, json operating-system: ubuntu-latest use-cache: true diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 617c691d..d0d7996f 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -27,14 +27,14 @@ jobs: timeout-minutes: 10 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup PHP uses: sunxyw/workflows/setup-environment@main with: - php-version: 8.1 + php-version: 8.3 php-extensions: swoole, posix, json operating-system: ubuntu-latest use-cache: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d251b00f..c6d42ab3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/vuepress-deploy.yml b/.github/workflows/vuepress-deploy.yml index b7dd1741..63243a92 100644 --- a/.github/workflows/vuepress-deploy.yml +++ b/.github/workflows/vuepress-deploy.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout master - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Deploy docs to GitHub Pages uses: jenkey2011/vuepress-deploy@master diff --git a/composer.json b/composer.json index bca05730..8171c16f 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ } ], "require": { - "php": "^8.0 || ^8.1 || ^8.2", + "php": "^8.0 || ^8.1 || ^8.2 || ^8.3 || ^8.4 || ^8.5", "ext-json": "*", "ext-tokenizer": "*", "doctrine/dbal": "^2.13.1", @@ -35,18 +35,18 @@ "require-dev": { "captainhook/captainhook": "^5.10", "captainhook/plugin-composer": "^5.3", - "friendsofphp/php-cs-fixer": "^3.2 != 3.7.0", + "friendsofphp/php-cs-fixer": "^3.64", "jangregor/phpstan-prophecy": "^1.0", "jetbrains/phpstorm-attributes": "^1.0", "mikey179/vfsstream": "^1.6", "phpspec/prophecy-phpunit": "^2.3", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.1", + "phpstan/phpstan": "^1.12", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^8.5 || ^9.0", + "phpunit/phpunit": "^9.0", "roave/security-advisories": "dev-latest", - "swoole/ide-helper": "^4.5" + "swoole/ide-helper": "^5.0" }, "replace": { "symfony/polyfill-php80": "*" diff --git a/src/Globals/global_functions.php b/src/Globals/global_functions.php index 3971044d..3adab140 100644 --- a/src/Globals/global_functions.php +++ b/src/Globals/global_functions.php @@ -301,7 +301,7 @@ function redis(string $name = 'default'): RedisWrapper * @param null|mixed $default 默认值 * @return mixed|void|ZMConfig */ -function config(null|array|string $key = null, mixed $default = null) +function config(array|string|null $key = null, mixed $default = null) { $config = ZMConfig::getInstance(); if (is_null($key)) { diff --git a/src/ZM/Annotation/OneBot/CommandArgument.php b/src/ZM/Annotation/OneBot/CommandArgument.php index 7b50efbe..740b3857 100644 --- a/src/ZM/Annotation/OneBot/CommandArgument.php +++ b/src/ZM/Annotation/OneBot/CommandArgument.php @@ -41,7 +41,7 @@ class CommandArgument extends AnnotationBase implements ErgodicAnnotation string $type = 'string', public bool $required = false, public string $prompt = '', - public null|array|\Closure|float|int|string $default = '', + public array|\Closure|float|int|string|null $default = '', public int $timeout = 60, public int $error_prompt_policy = 1 ) { diff --git a/src/ZM/Context/BotContext.php b/src/ZM/Context/BotContext.php index 897d857b..03dbc0ea 100644 --- a/src/ZM/Context/BotContext.php +++ b/src/ZM/Context/BotContext.php @@ -87,7 +87,7 @@ class BotContext implements ContextInterface * @noinspection PhpDocMissingThrowsInspection * @noinspection PhpUnhandledExceptionInspection */ - public function prompt(array|MessageSegment|string|\Stringable $prompt = '', int $timeout = 600, array|MessageSegment|string|\Stringable $timeout_prompt = '', int $option = ZM_PROMPT_NONE): null|array|OneBotEvent|string + public function prompt(array|MessageSegment|string|\Stringable $prompt = '', int $timeout = 600, array|MessageSegment|string|\Stringable $timeout_prompt = '', int $option = ZM_PROMPT_NONE): array|OneBotEvent|string|null { if (!container()->has('bot.event')) { throw new OneBot12Exception('bot()->prompt() can only be used in message event'); @@ -268,7 +268,7 @@ class BotContext implements ContextInterface * @return null|array|OneBotEvent|string 根据不同匹配类型返回不同的东西 * @throws OneBot12Exception */ - private function applyPromptReturn(mixed $result, int $option): null|array|OneBotEvent|string + private function applyPromptReturn(mixed $result, int $option): array|OneBotEvent|string|null { // 必须是 OneBotEvent 且是消息类型 if (!$result instanceof OneBotEvent || $result->type !== 'message') { diff --git a/src/ZM/Event/Listener/SignalListener.php b/src/ZM/Event/Listener/SignalListener.php index 2bd2055d..a8d77eb9 100644 --- a/src/ZM/Event/Listener/SignalListener.php +++ b/src/ZM/Event/Listener/SignalListener.php @@ -30,10 +30,13 @@ class SignalListener switch (Framework::getInstance()->getDriver()->getName()) { case 'swoole': Process::signal(SIGINT, [$this, 'onWorkerInt']); + Process::signal(SIGTERM, [$this, 'onWorkerInt']); + Process::signal(SIGHUP, [$this, 'onWorkerInt']); break; case 'workerman': Worker::$globalEvent->add(SIGINT, EventInterface::EV_SIGNAL, [$this, 'onWorkerInt']); Worker::$globalEvent->add(SIGTERM, EventInterface::EV_SIGNAL, fn () => Worker::stopAll(15)); + Worker::$globalEvent->add(SIGHUP, EventInterface::EV_SIGNAL, fn () => Worker::stopAll(15)); if (function_exists('pcntl_signal')) { pcntl_signal(SIGUSR1, SIG_IGN, false); } @@ -51,10 +54,9 @@ class SignalListener { $driver = Framework::getInstance()->getDriver()->getName(); if ($driver === 'swoole') { - Process::signal(SIGINT, function () { + $stopHandler = function () { echo "\r"; - logger()->notice('Master 进程收到中断信号 SIGINT'); - logger()->notice('正在停止服务器'); + logger()->notice('Master 进程收到中断信号,正在停止服务器'); Framework::getInstance()->stop(); if (extension_loaded('posix')) { Process::kill(posix_getpid(), SIGTERM); @@ -62,10 +64,13 @@ class SignalListener /* @phpstan-ignore-next-line */ Process::kill(Framework::getInstance()->getDriver()->getSwooleServer()->master_pid, SIGTERM); } - }); + }; + Process::signal(SIGINT, $stopHandler); + Process::signal(SIGTERM, $stopHandler); + Process::signal(SIGHUP, $stopHandler); } elseif ($driver === 'workerman') { if (!extension_loaded('pcntl') || !extension_loaded('posix')) { - logger()->error('请安装 pcntl 和 posix 扩展以支持 SIGINT 监听'); + logger()->error('请安装 pcntl 和 posix 扩展以支持信号监听'); return; } @@ -73,15 +78,14 @@ class SignalListener logger()->warning('重启ing'); Worker::reloadSelf(); }, false); - pcntl_signal(SIGTERM, function () { - Worker::stopAll(); - }, false); - pcntl_signal(SIGINT, function () { + $stopMaster = function () { echo "\r"; - logger()->notice('Master 进程收到中断信号 SIGINT'); - logger()->notice('正在停止服务器'); + logger()->notice('Master 进程收到中断信号,正在停止服务器'); Worker::stopAll(); - }, false); + }; + pcntl_signal(SIGTERM, $stopMaster, false); + pcntl_signal(SIGINT, $stopMaster, false); + pcntl_signal(SIGHUP, $stopMaster, false); } } diff --git a/src/ZM/Framework.php b/src/ZM/Framework.php index 07c2ef29..387b9e17 100644 --- a/src/ZM/Framework.php +++ b/src/ZM/Framework.php @@ -50,7 +50,7 @@ class Framework public const VERSION_ID = 726; /** @var string 版本名称 */ - public const VERSION = '3.2.6'; + public const VERSION = '3.2.7'; /** * @var RuntimePreferences 运行时偏好(环境信息&参数) @@ -61,7 +61,7 @@ class Framework protected array $argv; /** @var null|Driver|SwooleDriver|WorkermanDriver OneBot驱动 */ - protected null|Driver|SwooleDriver|WorkermanDriver $driver = null; + protected Driver|SwooleDriver|WorkermanDriver|null $driver = null; /** @var array> 启动注解列表 */ protected array $setup_annotations = []; diff --git a/src/ZM/Process/ProcessStateManager.php b/src/ZM/Process/ProcessStateManager.php index d9c3a036..3292a992 100644 --- a/src/ZM/Process/ProcessStateManager.php +++ b/src/ZM/Process/ProcessStateManager.php @@ -31,7 +31,7 @@ class ProcessStateManager * @throws ZMKnownException * @internal */ - public static function removeProcessState(int $type, null|int|string $id_or_name = null): void + public static function removeProcessState(int $type, int|string|null $id_or_name = null): void { switch ($type) { case ZM_PROCESS_MASTER: diff --git a/src/ZM/Store/KV/LightCache.php b/src/ZM/Store/KV/LightCache.php index 743bdc0e..0532c51c 100644 --- a/src/ZM/Store/KV/LightCache.php +++ b/src/ZM/Store/KV/LightCache.php @@ -104,7 +104,7 @@ class LightCache implements KVInterface /** * @throws InvalidArgumentException */ - public function set(string $key, mixed $value, null|\DateInterval|int $ttl = null): bool + public function set(string $key, mixed $value, \DateInterval|int|null $ttl = null): bool { $this->validateKey($key); self::$caches[$this->name][$key] = $value; @@ -139,7 +139,7 @@ class LightCache implements KVInterface } } - public function setMultiple(iterable $values, null|\DateInterval|int $ttl = null): bool + public function setMultiple(iterable $values, \DateInterval|int|null $ttl = null): bool { foreach ($values as $k => $v) { if (!$this->set($k, $v, $ttl)) { diff --git a/src/ZM/Store/KV/Redis/KVRedis.php b/src/ZM/Store/KV/Redis/KVRedis.php index 56d39458..d4425b1d 100644 --- a/src/ZM/Store/KV/Redis/KVRedis.php +++ b/src/ZM/Store/KV/Redis/KVRedis.php @@ -35,7 +35,7 @@ class KVRedis implements KVInterface return $ret; } - public function set(string $key, mixed $value, null|\DateInterval|int $ttl = null): bool + public function set(string $key, mixed $value, \DateInterval|int|null $ttl = null): bool { /** @var ZMRedis $redis */ $redis = RedisPool::pool($this->pool_name)->get(); @@ -78,7 +78,7 @@ class KVRedis implements KVInterface RedisPool::pool($this->pool_name)->put($redis); } - public function setMultiple(iterable $values, null|\DateInterval|int $ttl = null): bool + public function setMultiple(iterable $values, \DateInterval|int|null $ttl = null): bool { /** @var ZMRedis $redis */ $redis = RedisPool::pool($this->pool_name)->get(); diff --git a/src/ZM/Utils/MessageUtil.php b/src/ZM/Utils/MessageUtil.php index dcfb00b9..c4719af6 100644 --- a/src/ZM/Utils/MessageUtil.php +++ b/src/ZM/Utils/MessageUtil.php @@ -99,7 +99,7 @@ class MessageUtil return $ls; } - public static function getAltMessage(null|array|MessageSegment|string $message): string + public static function getAltMessage(array|MessageSegment|string|null $message): string { if ($message === null) { return '';