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'); }