diff --git a/instant-plugin-demo.php b/instant-plugin-demo.php
index 7131fabe..f741912c 100644
--- a/instant-plugin-demo.php
+++ b/instant-plugin-demo.php
@@ -2,21 +2,23 @@
declare(strict_types=1);
+$plugin = new \ZM\Plugin\InstantPlugin(__DIR__);
+
/*
-return function () {
- $plugin = new \ZM\Plugin\InstantPlugin(__DIR__);
+ * 发送 "测试 123",回复 "你好,123"
+ */
+$cmd1 = BotCommand::make('test', '测试')->withArgument('arg1')->on(fn () => '你好,{{arg1}}');
- $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);
+/*
+ * 浏览器访问 http://ip:port/index233,返回内容
+ */
+$route1 = Route::make('/index233')->on(fn () => '
Hello world
');
- $plugin->registerEvent(HttpRequestEvent::getName(), function (HttpRequestEvent $event) {
- $event->withResponse(\OneBot\Http\HttpFactory::getInstance()->createResponse(503));
- });
- return $plugin;
-};
-*/
+$plugin->addBotCommand($cmd1);
+$plugin->addHttpRoute($route1);
+
+return [
+ 'plugin-name' => 'pasd',
+ 'version' => '1.0.0',
+ 'plugin' => $plugin,
+];
diff --git a/src/Globals/global_defines_app.php b/src/Globals/global_defines_app.php
index 676f9b28..12f1cc62 100644
--- a/src/Globals/global_defines_app.php
+++ b/src/Globals/global_defines_app.php
@@ -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);
diff --git a/src/ZM/Annotation/OneBot/BotCommand.php b/src/ZM/Annotation/OneBot/BotCommand.php
index 7ece0f37..3e57583f 100644
--- a/src/ZM/Annotation/OneBot/BotCommand.php
+++ b/src/ZM/Annotation/OneBot/BotCommand.php
@@ -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 = '',
diff --git a/src/ZM/Annotation/OneBot/BotEvent.php b/src/ZM/Annotation/OneBot/BotEvent.php
index cbd839f8..1baffb76 100644
--- a/src/ZM/Annotation/OneBot/BotEvent.php
+++ b/src/ZM/Annotation/OneBot/BotEvent.php
@@ -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,
diff --git a/src/ZM/Annotation/OneBot/CommandArgument.php b/src/ZM/Annotation/OneBot/CommandArgument.php
index 10f216bd..7aaabec3 100644
--- a/src/ZM/Annotation/OneBot/CommandArgument.php
+++ b/src/ZM/Annotation/OneBot/CommandArgument.php
@@ -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 参数名称(可以是中文)
diff --git a/src/ZM/Command/Server/ServerStopCommand.php b/src/ZM/Command/Server/ServerStopCommand.php
index 47158d65..6159b7b0 100644
--- a/src/ZM/Command/Server/ServerStopCommand.php
+++ b/src/ZM/Command/Server/ServerStopCommand.php
@@ -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);
diff --git a/src/ZM/Event/Listener/MasterEventListener.php b/src/ZM/Event/Listener/MasterEventListener.php
index 8e6be230..5fa9f934 100644
--- a/src/ZM/Event/Listener/MasterEventListener.php
+++ b/src/ZM/Event/Listener/MasterEventListener.php
@@ -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);
}
}
}
diff --git a/src/ZM/Event/Listener/SignalListener.php b/src/ZM/Event/Listener/SignalListener.php
index a797c798..299fe6bf 100644
--- a/src/ZM/Event/Listener/SignalListener.php
+++ b/src/ZM/Event/Listener/SignalListener.php
@@ -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);
diff --git a/src/ZM/Event/Listener/WSEventListener.php b/src/ZM/Event/Listener/WSEventListener.php
new file mode 100644
index 00000000..c303a937
--- /dev/null
+++ b/src/ZM/Event/Listener/WSEventListener.php
@@ -0,0 +1,37 @@
+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;
+ }
+ }
+ // 这里下面为连接准入,允许接入反向 WS,TODO
+ }
+ }
+}
diff --git a/src/ZM/Event/Listener/WorkerEventListener.php b/src/ZM/Event/Listener/WorkerEventListener.php
index e9e219c9..b832a67e 100644
--- a/src/ZM/Event/Listener/WorkerEventListener.php
+++ b/src/ZM/Event/Listener/WorkerEventListener.php
@@ -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
diff --git a/src/ZM/Framework.php b/src/ZM/Framework.php
index cb4ec77a..6bf37bba 100644
--- a/src/ZM/Framework.php
+++ b/src/ZM/Framework.php
@@ -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';
diff --git a/src/ZM/Process/ProcessStateManager.php b/src/ZM/Process/ProcessStateManager.php
index 3dd69727..96b1075e 100644
--- a/src/ZM/Process/ProcessStateManager.php
+++ b/src/ZM/Process/ProcessStateManager.php
@@ -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);
}
}
diff --git a/src/ZM/Store/Lock/FileLock.php b/src/ZM/Store/Lock/FileLock.php
index 989362c0..02b6ba87 100644
--- a/src/ZM/Store/Lock/FileLock.php
+++ b/src/ZM/Store/Lock/FileLock.php
@@ -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");
diff --git a/tests_old/ZM/Utils/MessageUtilTest.php b/tests_old/ZM/Utils/MessageUtilTest.php
index 7b45fa70..7aea725b 100644
--- a/tests_old/ZM/Utils/MessageUtilTest.php
+++ b/tests_old/ZM/Utils/MessageUtilTest.php
@@ -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');
}