add state and ctrl+C support for windows

This commit is contained in:
crazywhalecc 2022-09-26 22:44:41 +08:00
parent cf3f09600b
commit a4f992b9e5
14 changed files with 143 additions and 108 deletions

View File

@ -2,21 +2,23 @@
declare(strict_types=1);
/*
return function () {
$plugin = new \ZM\Plugin\InstantPlugin(__DIR__);
$cmd = \ZM\Annotation\OneBot\BotCommand::make(name: 'test', match: '测试')->withArgument(name: 'arg1')->withMethod(function () {
ctx()->reply('test ok');
});
$event = BotEvent::make(type: 'message')->withMethod(function () {
});
$plugin->addBotEvent($event);
$plugin->addBotCommand($cmd);
$plugin->registerEvent(HttpRequestEvent::getName(), function (HttpRequestEvent $event) {
$event->withResponse(\OneBot\Http\HttpFactory::getInstance()->createResponse(503));
});
return $plugin;
};
/*
* 发送 "测试 123",回复 "你好123"
*/
$cmd1 = BotCommand::make('test', '测试')->withArgument('arg1')->on(fn () => '你好,{{arg1}}');
/*
* 浏览器访问 http://ip:port/index233返回内容
*/
$route1 = Route::make('/index233')->on(fn () => '<h1>Hello world</h1>');
$plugin->addBotCommand($cmd1);
$plugin->addHttpRoute($route1);
return [
'plugin-name' => 'pasd',
'version' => '1.0.0',
'plugin' => $plugin,
];

View File

@ -37,6 +37,16 @@ define('WORKING_DIR', getcwd());
/* 定义源码根目录,如果是 Phar 打包框架运行的话,就是 Phar 文件本身 */
define('SOURCE_ROOT_DIR', Phar::running() !== '' ? Phar::running() : WORKING_DIR);
if (DIRECTORY_SEPARATOR === '\\') {
define('TMP_DIR', 'C:\\Windows\\Temp');
} elseif (!empty(getenv('TMPDIR'))) {
define('TMP_DIR', getenv('TMPDIR'));
} elseif (is_writable('/tmp')) {
define('TMP_DIR', '/tmp');
} else {
define('TMP_DIR', getcwd() . '/.zm-tmp');
}
/* 定义启动模式,这里指的是框架本身的源码目录是通过 composer 加入 vendor 加载的还是直接放到 src 目录加载的,前者为 1后者为 0 */
define('LOAD_MODE', is_dir(zm_dir(SOURCE_ROOT_DIR . '/src/ZM')) ? 0 : 1);
@ -47,10 +57,8 @@ if (Phar::running() !== '') {
define('FRAMEWORK_ROOT_DIR', realpath(zm_dir(__DIR__ . '/../../')));
}
/* 定义用于存放框架运行状态的目录Windows 不可用) */
if (DIRECTORY_SEPARATOR !== '\\') {
define('ZM_PID_DIR', '/tmp/.zm_' . sha1(FRAMEWORK_ROOT_DIR));
}
/* 定义用于存放框架运行状态的目录Windows 可用) */
define('ZM_STATE_DIR', TMP_DIR . '/.zm_' . sha1(FRAMEWORK_ROOT_DIR));
/* 对 global.php 在 Windows 下的兼容性考虑,因为 Windows 或者无 Swoole 环境时候无法运行 */
!defined('SWOOLE_BASE') && define('SWOOLE_BASE', 1) && define('SWOOLE_PROCESS', 2);

View File

@ -59,8 +59,7 @@ class BotCommand extends AnnotationBase implements Level
/** @var int */
public $level = 20;
/** @var array */
private $arguments = [];
private array $arguments = [];
public function __construct(
$name = '',

View File

@ -19,23 +19,17 @@ use ZM\Annotation\AnnotationBase;
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class BotEvent extends AnnotationBase
{
/** @var null|string */
public $type;
public ?string $type;
/** @var null|string */
public $detail_type;
public ?string $detail_type;
/** @var null|string */
public $impl;
public ?string $impl;
/** @var null|string */
public $platform;
public ?string $platform;
/** @var null|string */
public $self_id;
public ?string $self_id;
/** @var null|string */
public $sub_type;
public ?string $sub_type;
public function __construct(
?string $type = null,

View File

@ -22,45 +22,23 @@ use ZM\Exception\ZMKnownException;
class CommandArgument extends AnnotationBase implements ErgodicAnnotation
{
/**
* @var string
* @Required()
*/
public $name;
public string $name;
/**
* @var string
*/
public $description = '';
public string $description = '';
/**
* @var string
*/
public $type = 'string';
public string $type = 'string';
/**
* @var bool
*/
public $required = false;
public bool $required = false;
/**
* @var string
*/
public $prompt = '';
public string $prompt = '';
/**
* @var string
*/
public $default = '';
public string $default = '';
/**
* @var int
*/
public $timeout = 60;
public int $timeout = 60;
/**
* @var int
*/
public $error_prompt_policy = 1;
public int $error_prompt_policy = 1;
/**
* @param string $name 参数名称(可以是中文)

View File

@ -26,7 +26,7 @@ class ServerStopCommand extends ServerCommand
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($input->getOption('force') !== false) {
$file_path = ZM_PID_DIR;
$file_path = ZM_STATE_DIR;
$list = FileSystem::scanDirFiles($file_path, false, true);
foreach ($list as $file) {
$name = explode('.', $file);

View File

@ -51,10 +51,10 @@ class MasterEventListener
public function onMasterStop()
{
if (extension_loaded('posix')) {
logger()->debug('正在关闭 Master 进程pid=' . posix_getpid());
logger()->debug('正在关闭 Master 进程pid=' . getmypid());
ProcessStateManager::removeProcessState(ZM_PROCESS_MASTER);
if (FileSystem::scanDirFiles(ZM_PID_DIR) == []) {
rmdir(ZM_PID_DIR);
if (FileSystem::scanDirFiles(ZM_STATE_DIR) == []) {
rmdir(ZM_STATE_DIR);
}
}
}

View File

@ -113,6 +113,22 @@ class SignalListener
}
}
public function signalWindowsCtrlC()
{
if (self::$manager_kill_time > 0) {
if (self::$manager_kill_time >= 5) {
exit(0);
}
echo "\r";
logger()->notice('请再按 {count} 次 Ctrl+C 以强制杀死进程', ['count' => 5 - self::$manager_kill_time]);
return;
}
++self::$manager_kill_time;
if (self::$manager_kill_time === 1) {
Framework::getInstance()->stop();
}
}
/**
* 按5次Ctrl+C后强行杀死框架的处理函数
*/
@ -120,7 +136,7 @@ class SignalListener
{
if (self::$manager_kill_time > 0) {
if (self::$manager_kill_time >= 5) {
$file_path = ZM_PID_DIR;
$file_path = ZM_STATE_DIR;
$flist = FileSystem::scanDirFiles($file_path, false, true);
foreach ($flist as $file) {
$name = explode('.', $file);

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace ZM\Event\Listener;
use OneBot\Driver\Event\WebSocket\WebSocketOpenEvent;
use OneBot\Http\HttpFactory;
use OneBot\Util\Singleton;
use ZM\Container\ContainerServicesProvider;
class WSEventListener
{
use Singleton;
public function onWebSocketOpen(WebSocketOpenEvent $event)
{
// 注册容器
resolve(ContainerServicesProvider::class)->registerServices('connection');
// 判断是不是 OneBot 12 反向 WS 连进来的,通过 Sec-WebSocket-Protocol 头
$line = explode('.', $event->getRequest()->getHeaderLine('Sec-WebSocket-Protocol'), 2);
if ($line[0] === '12') {
// 是 OneBot 12 标准的,准许接入,进行鉴权
$request = $event->getRequest();
if (($stored_token = $event->getSocketConfig()['access_token'] ?? '') !== '') {
$token = $request->getHeaderLine('Authorization');
$token = explode('Bearer ', $token);
if (!isset($token[1]) || $token[1] !== $stored_token) { // 没有 token鉴权失败
$event->withResponse(HttpFactory::getInstance()->createResponse(401, 'Unauthorized'));
return;
}
}
// 这里下面为连接准入,允许接入反向 WSTODO
}
}
}

View File

@ -37,6 +37,13 @@ class WorkerEventListener
if (!Framework::getInstance()->getArgv()['disable-safe-exit'] && PHP_OS_FAMILY !== 'Windows') {
SignalListener::getInstance()->signalWorker();
}
// Windows 环境下,为了监听 Ctrl+C只能开启终端输入
if (PHP_OS_FAMILY === 'Windows') {
sapi_windows_set_ctrl_handler([SignalListener::getInstance(), 'signalWindowsCtrlC']);
Framework::getInstance()->getDriver()->getEventLoop()->addReadEvent(STDIN, function ($x) {});
}
logger()->debug('Worker #' . ProcessManager::getProcessId() . ' started');
// 设置 Worker 进程的状态和 ID 等信息
@ -44,8 +51,8 @@ class WorkerEventListener
/* @phpstan-ignore-next-line */
$server = Framework::getInstance()->getDriver()->getSwooleServer();
ProcessStateManager::saveProcessState(ZM_PROCESS_WORKER, $server->worker_pid, ['worker_id' => $server->worker_id]);
} elseif ($name === 'workerman' && DIRECTORY_SEPARATOR !== '\\' && extension_loaded('posix')) {
ProcessStateManager::saveProcessState(ZM_PROCESS_WORKER, posix_getpid(), ['worker_id' => ProcessManager::getProcessId()]);
} elseif ($name === 'workerman') {
ProcessStateManager::saveProcessState(ZM_PROCESS_WORKER, getmypid(), ['worker_id' => ProcessManager::getProcessId()]);
}
// 打印进程ID

View File

@ -12,6 +12,7 @@ use OneBot\Driver\Event\Process\ManagerStartEvent;
use OneBot\Driver\Event\Process\ManagerStopEvent;
use OneBot\Driver\Event\Process\WorkerStartEvent;
use OneBot\Driver\Event\Process\WorkerStopEvent;
use OneBot\Driver\Event\WebSocket\WebSocketOpenEvent;
use OneBot\Driver\Interfaces\DriverInitPolicy;
use OneBot\Driver\Swoole\SwooleDriver;
use OneBot\Driver\Workerman\Worker;
@ -25,6 +26,7 @@ use ZM\Event\Listener\HttpEventListener;
use ZM\Event\Listener\ManagerEventListener;
use ZM\Event\Listener\MasterEventListener;
use ZM\Event\Listener\WorkerEventListener;
use ZM\Event\Listener\WSEventListener;
use ZM\Exception\ConfigException;
use ZM\Exception\InitException;
use ZM\Exception\ZMKnownException;
@ -292,14 +294,6 @@ class Framework
ob_event_provider()->addEventListener(WorkerStartEvent::getName(), [WorkerEventListener::getInstance(), 'onWorkerStart999'], 999);
ob_event_provider()->addEventListener(WorkerStopEvent::getName(), [WorkerEventListener::getInstance(), 'onWorkerStop999'], 999);
// Http 事件
ob_event_provider()->addEventListener(HttpRequestEvent::getName(), function () {
global $starttime;
$starttime = microtime(true);
}, 1000);
ob_event_provider()->addEventListener(HttpRequestEvent::getName(), function () {
global $starttime;
logger()->error('Finally used ' . round((microtime(true) - $starttime) * 1000, 4) . ' ms');
}, 0);
ob_event_provider()->addEventListener(HttpRequestEvent::getName(), [HttpEventListener::getInstance(), 'onRequest999'], 999);
ob_event_provider()->addEventListener(HttpRequestEvent::getName(), [HttpEventListener::getInstance(), 'onRequest1'], 1);
// manager 事件
@ -307,10 +301,12 @@ class Framework
ob_event_provider()->addEventListener(ManagerStopEvent::getName(), [ManagerEventListener::getInstance(), 'onManagerStop'], 999);
// master 事件
ob_event_provider()->addEventListener(DriverInitEvent::getName(), [MasterEventListener::getInstance(), 'onMasterStart'], 999);
// websocket 事件
ob_event_provider()->addEventListener(WebSocketOpenEvent::getName(), [WSEventListener::getInstance(), 'onWebSocketOpen'], 999);
// 框架多进程依赖
if (defined('ZM_PID_DIR') && !is_dir(ZM_PID_DIR)) {
mkdir(ZM_PID_DIR);
if (defined('ZM_PID_DIR') && !is_dir(ZM_STATE_DIR)) {
mkdir(ZM_STATE_DIR);
}
}
@ -334,10 +330,8 @@ class Framework
$properties['version'] = self::VERSION . (LOAD_MODE === 0 ? (' (build ' . ZM_VERSION_ID . ')') : '');
// 打印 PHP 版本
$properties['php_version'] = PHP_VERSION;
// 非 Windows 操作系统打印 master 进程的 pid
if (PHP_OS_FAMILY !== 'Windows') {
$properties['master_pid'] = posix_getpid();
}
// 打印 master 进程的 pid
$properties['master_pid'] = getmypid();
// 打印进程模型
if ($this->driver->getName() === 'swoole') {
$properties['process_mode'] = 'MST1';

View File

@ -20,13 +20,13 @@ class ProcessStateManager
{
switch ($type) {
case ZM_PROCESS_MASTER:
$file = ZM_PID_DIR . '/master.json';
$file = zm_dir(ZM_STATE_DIR . '/master.json');
if (file_exists($file)) {
unlink($file);
}
return;
case ZM_PROCESS_MANAGER:
$file = ZM_PID_DIR . '/manager.pid';
$file = zm_dir(ZM_STATE_DIR . '/manager.pid');
if (file_exists($file)) {
unlink($file);
}
@ -35,7 +35,7 @@ class ProcessStateManager
if (!is_int($id_or_name)) {
throw new ZMKnownException('E99999', 'worker_id必须为整数');
}
$file = ZM_PID_DIR . '/worker.' . $id_or_name . '.pid';
$file = zm_dir(ZM_STATE_DIR . '/worker.' . $id_or_name . '.pid');
if (file_exists($file)) {
unlink($file);
}
@ -44,7 +44,7 @@ class ProcessStateManager
if (!is_string($id_or_name)) {
throw new ZMKnownException('E99999', 'process_name必须为字符串');
}
$file = ZM_PID_DIR . '/user.' . $id_or_name . '.pid';
$file = zm_dir(ZM_STATE_DIR . '/user.' . $id_or_name . '.pid');
if (file_exists($file)) {
unlink($file);
}
@ -53,7 +53,7 @@ class ProcessStateManager
if (!is_int($id_or_name)) {
throw new ZMKnownException('E99999', 'worker_id必须为整数');
}
$file = ZM_PID_DIR . '/taskworker.' . $id_or_name . '.pid';
$file = zm_dir(ZM_STATE_DIR . '/taskworker.' . $id_or_name . '.pid');
if (file_exists($file)) {
unlink($file);
}
@ -71,46 +71,46 @@ class ProcessStateManager
*/
public static function getProcessState(int $type, $id_or_name = null)
{
$file = ZM_PID_DIR;
$file = ZM_STATE_DIR;
switch ($type) {
case ZM_PROCESS_MASTER:
if (!file_exists($file . '/master.json')) {
if (!file_exists(zm_dir($file . '/master.json'))) {
return false;
}
$json = json_decode(file_get_contents($file . '/master.json'), true);
$json = json_decode(file_get_contents(zm_dir($file . '/master.json')), true);
if ($json !== null) {
return $json;
}
return false;
case ZM_PROCESS_MANAGER:
if (!file_exists($file . '/manager.pid')) {
if (!file_exists(zm_dir($file . '/manager.pid'))) {
return false;
}
return intval(file_get_contents($file . '/manager.pid'));
return intval(file_get_contents(zm_dir($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')) {
if (!file_exists(zm_dir($file . '/worker.' . $id_or_name . '.pid'))) {
return false;
}
return intval(file_get_contents($file . '/worker.' . $id_or_name . '.pid'));
return intval(file_get_contents(zm_dir($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')) {
if (!file_exists(zm_dir($file . '/user.' . $id_or_name . '.pid'))) {
return false;
}
return intval(file_get_contents($file . '/user.' . $id_or_name . '.pid'));
return intval(file_get_contents(zm_dir($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')) {
if (!file_exists(zm_dir($file . '/taskworker.' . $id_or_name . '.pid'))) {
return false;
}
return intval(file_get_contents($file . '/taskworker.' . $id_or_name . '.pid'));
return intval(file_get_contents(zm_dir($file . '/taskworker.' . $id_or_name . '.pid')));
default:
return false;
}
@ -126,7 +126,7 @@ class ProcessStateManager
{
switch ($type) {
case ZM_PROCESS_MASTER:
$file = ZM_PID_DIR . '/master.json';
$file = zm_dir(ZM_STATE_DIR . '/master.json');
$json = [
'pid' => intval($pid),
'stdout' => $data['stdout'],
@ -135,19 +135,19 @@ class ProcessStateManager
file_put_contents($file, json_encode($json, JSON_UNESCAPED_UNICODE));
return;
case ZM_PROCESS_MANAGER:
$file = ZM_PID_DIR . '/manager.pid';
$file = zm_dir(ZM_STATE_DIR . '/manager.pid');
file_put_contents($file, strval($pid));
return;
case ZM_PROCESS_WORKER:
$file = ZM_PID_DIR . '/worker.' . $data['worker_id'] . '.pid';
$file = zm_dir(ZM_STATE_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 = zm_dir(ZM_STATE_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 = zm_dir(ZM_STATE_DIR . '/taskworker.' . $data['worker_id'] . '.pid');
file_put_contents($file, strval($pid));
return;
}
@ -155,7 +155,7 @@ class ProcessStateManager
public static function isStateEmpty(): bool
{
$ls = FileSystem::scanDirFiles(ZM_PID_DIR, false, true);
$ls = FileSystem::scanDirFiles(ZM_STATE_DIR, false, true);
return empty($ls);
}
}

View File

@ -20,7 +20,7 @@ class FileLock
public static function lock(string $name)
{
self::$name_hash[$name] = self::$name_hash[$name] ?? md5($name);
$lock_file = is_dir('/tmp') ? '/tmp' : WORKING_DIR . '.zm_' . zm_instance_id() . self::$name_hash[$name] . '.lock';
$lock_file = zm_dir(TMP_DIR . '/.zm_' . zm_instance_id() . self::$name_hash[$name] . '.lock');
self::$lock_file_handle[$name] = fopen($lock_file, 'w');
if (self::$lock_file_handle[$name] === false) {
logger()->critical("Can not create lock file {$lock_file}\n");

View File

@ -102,7 +102,7 @@ class MessageUtilTest extends TestCase
public function testGetImageCQFromLocal(): void
{
file_put_contents('/tmp/test.jpg', 'test');
file_put_contents(TMP_DIR . '/test.jpg', 'test');
$this->assertEquals('[CQ:image,file=base64://' . base64_encode('test') . ']', MessageUtil::getImageCQFromLocal('/tmp/test.jpg'));
unlink('/tmp/test.jpg');
}