fix: add SIGHUP/SIGTERM handling, modernize PHP support and CI

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
This commit is contained in:
crazywhalecc
2026-06-17 15:11:57 +08:00
parent a249fb5bfb
commit 3a57294e48
16 changed files with 45 additions and 41 deletions

View File

@@ -12,14 +12,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
ref: ${{ github.base_ref }} ref: ${{ github.base_ref }}
- name: Setup PHP - name: Setup PHP
uses: sunxyw/workflows/setup-environment@main uses: sunxyw/workflows/setup-environment@main
with: with:
php-version: 8.1 php-version: 8.3
php-extensions: swoole, posix, json php-extensions: swoole, posix, json
operating-system: ubuntu-latest operating-system: ubuntu-latest
use-cache: true use-cache: true
@@ -40,14 +40,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
ref: ${{ github.base_ref }} ref: ${{ github.base_ref }}
- name: Setup PHP - name: Setup PHP
uses: sunxyw/workflows/setup-environment@main uses: sunxyw/workflows/setup-environment@main
with: with:
php-version: 8.1 php-version: 8.3
php-extensions: swoole, posix, json php-extensions: swoole, posix, json
operating-system: ubuntu-latest operating-system: ubuntu-latest
use-cache: true use-cache: true

View File

@@ -27,14 +27,14 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup PHP - name: Setup PHP
uses: sunxyw/workflows/setup-environment@main uses: sunxyw/workflows/setup-environment@main
with: with:
php-version: 8.1 php-version: 8.3
php-extensions: swoole, posix, json php-extensions: swoole, posix, json
operating-system: ubuntu-latest operating-system: ubuntu-latest
use-cache: true use-cache: true

View File

@@ -16,14 +16,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
ref: ${{ github.base_ref }} ref: ${{ github.base_ref }}
- name: Setup PHP - name: Setup PHP
uses: sunxyw/workflows/setup-environment@main uses: sunxyw/workflows/setup-environment@main
with: with:
php-version: 8.1 php-version: 8.3
php-extensions: swoole, posix, json php-extensions: swoole, posix, json
operating-system: ubuntu-latest operating-system: ubuntu-latest
use-cache: true use-cache: true

View File

@@ -27,14 +27,14 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup PHP - name: Setup PHP
uses: sunxyw/workflows/setup-environment@main uses: sunxyw/workflows/setup-environment@main
with: with:
php-version: 8.1 php-version: 8.3
php-extensions: swoole, posix, json php-extensions: swoole, posix, json
operating-system: ubuntu-latest operating-system: ubuntu-latest
use-cache: true use-cache: true

View File

@@ -39,7 +39,7 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout master - name: Checkout master
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Deploy docs to GitHub Pages - name: Deploy docs to GitHub Pages
uses: jenkey2011/vuepress-deploy@master uses: jenkey2011/vuepress-deploy@master

View File

@@ -13,7 +13,7 @@
} }
], ],
"require": { "require": {
"php": "^8.0 || ^8.1 || ^8.2", "php": "^8.0 || ^8.1 || ^8.2 || ^8.3 || ^8.4 || ^8.5",
"ext-json": "*", "ext-json": "*",
"ext-tokenizer": "*", "ext-tokenizer": "*",
"doctrine/dbal": "^2.13.1", "doctrine/dbal": "^2.13.1",
@@ -35,18 +35,18 @@
"require-dev": { "require-dev": {
"captainhook/captainhook": "^5.10", "captainhook/captainhook": "^5.10",
"captainhook/plugin-composer": "^5.3", "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", "jangregor/phpstan-prophecy": "^1.0",
"jetbrains/phpstorm-attributes": "^1.0", "jetbrains/phpstorm-attributes": "^1.0",
"mikey179/vfsstream": "^1.6", "mikey179/vfsstream": "^1.6",
"phpspec/prophecy-phpunit": "^2.3", "phpspec/prophecy-phpunit": "^2.3",
"phpstan/extension-installer": "^1.1", "phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.1", "phpstan/phpstan": "^1.12",
"phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.1", "phpstan/phpstan-phpunit": "^1.1",
"phpunit/phpunit": "^8.5 || ^9.0", "phpunit/phpunit": "^9.0",
"roave/security-advisories": "dev-latest", "roave/security-advisories": "dev-latest",
"swoole/ide-helper": "^4.5" "swoole/ide-helper": "^5.0"
}, },
"replace": { "replace": {
"symfony/polyfill-php80": "*" "symfony/polyfill-php80": "*"

View File

@@ -301,7 +301,7 @@ function redis(string $name = 'default'): RedisWrapper
* @param null|mixed $default 默认值 * @param null|mixed $default 默认值
* @return mixed|void|ZMConfig * @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(); $config = ZMConfig::getInstance();
if (is_null($key)) { if (is_null($key)) {

View File

@@ -41,7 +41,7 @@ class CommandArgument extends AnnotationBase implements ErgodicAnnotation
string $type = 'string', string $type = 'string',
public bool $required = false, public bool $required = false,
public string $prompt = '', 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 $timeout = 60,
public int $error_prompt_policy = 1 public int $error_prompt_policy = 1
) { ) {

View File

@@ -87,7 +87,7 @@ class BotContext implements ContextInterface
* @noinspection PhpDocMissingThrowsInspection * @noinspection PhpDocMissingThrowsInspection
* @noinspection PhpUnhandledExceptionInspection * @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')) { if (!container()->has('bot.event')) {
throw new OneBot12Exception('bot()->prompt() can only be used in message 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 根据不同匹配类型返回不同的东西 * @return null|array|OneBotEvent|string 根据不同匹配类型返回不同的东西
* @throws OneBot12Exception * @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 且是消息类型 // 必须是 OneBotEvent 且是消息类型
if (!$result instanceof OneBotEvent || $result->type !== 'message') { if (!$result instanceof OneBotEvent || $result->type !== 'message') {

View File

@@ -30,10 +30,13 @@ class SignalListener
switch (Framework::getInstance()->getDriver()->getName()) { switch (Framework::getInstance()->getDriver()->getName()) {
case 'swoole': case 'swoole':
Process::signal(SIGINT, [$this, 'onWorkerInt']); Process::signal(SIGINT, [$this, 'onWorkerInt']);
Process::signal(SIGTERM, [$this, 'onWorkerInt']);
Process::signal(SIGHUP, [$this, 'onWorkerInt']);
break; break;
case 'workerman': case 'workerman':
Worker::$globalEvent->add(SIGINT, EventInterface::EV_SIGNAL, [$this, 'onWorkerInt']); Worker::$globalEvent->add(SIGINT, EventInterface::EV_SIGNAL, [$this, 'onWorkerInt']);
Worker::$globalEvent->add(SIGTERM, EventInterface::EV_SIGNAL, fn () => Worker::stopAll(15)); 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')) { if (function_exists('pcntl_signal')) {
pcntl_signal(SIGUSR1, SIG_IGN, false); pcntl_signal(SIGUSR1, SIG_IGN, false);
} }
@@ -51,10 +54,9 @@ class SignalListener
{ {
$driver = Framework::getInstance()->getDriver()->getName(); $driver = Framework::getInstance()->getDriver()->getName();
if ($driver === 'swoole') { if ($driver === 'swoole') {
Process::signal(SIGINT, function () { $stopHandler = function () {
echo "\r"; echo "\r";
logger()->notice('Master 进程收到中断信号 SIGINT'); logger()->notice('Master 进程收到中断信号,正在停止服务器');
logger()->notice('正在停止服务器');
Framework::getInstance()->stop(); Framework::getInstance()->stop();
if (extension_loaded('posix')) { if (extension_loaded('posix')) {
Process::kill(posix_getpid(), SIGTERM); Process::kill(posix_getpid(), SIGTERM);
@@ -62,10 +64,13 @@ class SignalListener
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */
Process::kill(Framework::getInstance()->getDriver()->getSwooleServer()->master_pid, SIGTERM); Process::kill(Framework::getInstance()->getDriver()->getSwooleServer()->master_pid, SIGTERM);
} }
}); };
Process::signal(SIGINT, $stopHandler);
Process::signal(SIGTERM, $stopHandler);
Process::signal(SIGHUP, $stopHandler);
} elseif ($driver === 'workerman') { } elseif ($driver === 'workerman') {
if (!extension_loaded('pcntl') || !extension_loaded('posix')) { if (!extension_loaded('pcntl') || !extension_loaded('posix')) {
logger()->error('请安装 pcntl 和 posix 扩展以支持 SIGINT 监听'); logger()->error('请安装 pcntl 和 posix 扩展以支持信号监听');
return; return;
} }
@@ -73,15 +78,14 @@ class SignalListener
logger()->warning('重启ing'); logger()->warning('重启ing');
Worker::reloadSelf(); Worker::reloadSelf();
}, false); }, false);
pcntl_signal(SIGTERM, function () { $stopMaster = function () {
Worker::stopAll();
}, false);
pcntl_signal(SIGINT, function () {
echo "\r"; echo "\r";
logger()->notice('Master 进程收到中断信号 SIGINT'); logger()->notice('Master 进程收到中断信号,正在停止服务器');
logger()->notice('正在停止服务器');
Worker::stopAll(); Worker::stopAll();
}, false); };
pcntl_signal(SIGTERM, $stopMaster, false);
pcntl_signal(SIGINT, $stopMaster, false);
pcntl_signal(SIGHUP, $stopMaster, false);
} }
} }

View File

@@ -50,7 +50,7 @@ class Framework
public const VERSION_ID = 726; public const VERSION_ID = 726;
/** @var string 版本名称 */ /** @var string 版本名称 */
public const VERSION = '3.2.6'; public const VERSION = '3.2.7';
/** /**
* @var RuntimePreferences 运行时偏好(环境信息&参数) * @var RuntimePreferences 运行时偏好(环境信息&参数)
@@ -61,7 +61,7 @@ class Framework
protected array $argv; protected array $argv;
/** @var null|Driver|SwooleDriver|WorkermanDriver OneBot驱动 */ /** @var null|Driver|SwooleDriver|WorkermanDriver OneBot驱动 */
protected null|Driver|SwooleDriver|WorkermanDriver $driver = null; protected Driver|SwooleDriver|WorkermanDriver|null $driver = null;
/** @var array<array<string, string>> 启动注解列表 */ /** @var array<array<string, string>> 启动注解列表 */
protected array $setup_annotations = []; protected array $setup_annotations = [];

View File

@@ -31,7 +31,7 @@ class ProcessStateManager
* @throws ZMKnownException * @throws ZMKnownException
* @internal * @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) { switch ($type) {
case ZM_PROCESS_MASTER: case ZM_PROCESS_MASTER:

View File

@@ -104,7 +104,7 @@ class LightCache implements KVInterface
/** /**
* @throws InvalidArgumentException * @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); $this->validateKey($key);
self::$caches[$this->name][$key] = $value; 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) { foreach ($values as $k => $v) {
if (!$this->set($k, $v, $ttl)) { if (!$this->set($k, $v, $ttl)) {

View File

@@ -35,7 +35,7 @@ class KVRedis implements KVInterface
return $ret; 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 */ /** @var ZMRedis $redis */
$redis = RedisPool::pool($this->pool_name)->get(); $redis = RedisPool::pool($this->pool_name)->get();
@@ -78,7 +78,7 @@ class KVRedis implements KVInterface
RedisPool::pool($this->pool_name)->put($redis); 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 */ /** @var ZMRedis $redis */
$redis = RedisPool::pool($this->pool_name)->get(); $redis = RedisPool::pool($this->pool_name)->get();

View File

@@ -99,7 +99,7 @@ class MessageUtil
return $ls; 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) { if ($message === null) {
return ''; return '';