mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-03-17 20:54:52 +08:00
439 lines
17 KiB
PHP
439 lines
17 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace ZM;
|
||
|
||
use OneBot\Driver\Driver;
|
||
use OneBot\Driver\Event\DriverInitEvent;
|
||
use OneBot\Driver\Event\Http\HttpRequestEvent;
|
||
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\WebSocketCloseEvent;
|
||
use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent;
|
||
use OneBot\Driver\Event\WebSocket\WebSocketOpenEvent;
|
||
use OneBot\Driver\Interfaces\DriverInitPolicy;
|
||
use OneBot\Driver\Swoole\SwooleDriver;
|
||
use OneBot\Driver\Workerman\Worker;
|
||
use OneBot\Driver\Workerman\WorkermanDriver;
|
||
use OneBot\Util\Singleton;
|
||
use ZM\Command\Server\ServerStartCommand;
|
||
use ZM\Config\ZMConfig;
|
||
use ZM\Container\ContainerBindingListener;
|
||
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\SingletonViolationException;
|
||
use ZM\Exception\ZMKnownException;
|
||
use ZM\Logger\TablePrinter;
|
||
use ZM\Process\ProcessStateManager;
|
||
use ZM\Store\FileSystem;
|
||
use ZM\Utils\EasterEgg;
|
||
|
||
/**
|
||
* 框架入口类
|
||
* @since 3.0
|
||
*/
|
||
class Framework
|
||
{
|
||
use Singleton;
|
||
|
||
/** @var int 版本ID */
|
||
public const VERSION_ID = 661;
|
||
|
||
/** @var string 版本名称 */
|
||
public const VERSION = '3.0.0-beta5';
|
||
|
||
/** @var array 传入的参数 */
|
||
protected array $argv;
|
||
|
||
/** @var null|Driver|SwooleDriver|WorkermanDriver OneBot驱动 */
|
||
protected SwooleDriver|Driver|WorkermanDriver|null $driver = null;
|
||
|
||
/** @var array<array<string, string>> 启动注解列表 */
|
||
protected array $setup_annotations = [];
|
||
|
||
/** @var array|string[] 框架启动前置的内容,由上到下执行 */
|
||
protected array $bootstrappers = [
|
||
Bootstrap\LoadConfiguration::class, // 加载配置文件
|
||
Bootstrap\LoadGlobalDefines::class, // 加载框架级别的全局常量声明
|
||
Bootstrap\RegisterLogger::class, // 加载 Logger
|
||
Bootstrap\HandleExceptions::class, // 注册异常处理器
|
||
Bootstrap\RegisterEventProvider::class, // 绑定框架的 EventProvider 到 libob 的 Driver 上
|
||
Bootstrap\SetInternalTimezone::class, // 设置时区
|
||
];
|
||
|
||
/**
|
||
* 框架初始化文件
|
||
*
|
||
* @param array<string, null|bool|string> $argv 传入的参数(见 ServerStartCommand)
|
||
* @throws \Exception
|
||
*/
|
||
public function __construct(array $argv = [])
|
||
{
|
||
// 单例化整个Framework类
|
||
if (self::$instance !== null) {
|
||
throw new SingletonViolationException(self::class);
|
||
}
|
||
self::$instance = $this;
|
||
|
||
// 初始化必需的args参数,如果没有传入的话,使用默认值
|
||
$this->argv = empty($argv) ? ServerStartCommand::exportOptionArray() : $argv;
|
||
}
|
||
|
||
/**
|
||
* 初始化框架
|
||
*
|
||
* @throws \Exception
|
||
*/
|
||
public function init(): Framework
|
||
{
|
||
// 顺序执行引导器
|
||
foreach ($this->bootstrappers as $bootstrapper) {
|
||
resolve($bootstrapper)->bootstrap($this->argv);
|
||
}
|
||
|
||
// 初始化 @OnSetup 事件
|
||
$this->initSetupAnnotations();
|
||
|
||
// 初始化 Driver 及框架内部需要监听的事件
|
||
$this->initDriver();
|
||
|
||
// 初始化框架的交互以及框架部分自己要监听的事件
|
||
$this->initFramework();
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* 启动框架
|
||
*/
|
||
public function start(): void
|
||
{
|
||
// 对多进程有效,记录当前已经加载的所有文件,最后在 Worker 进程中比较可重载的文件,用于排错
|
||
global $zm_loaded_files;
|
||
$zm_loaded_files = get_included_files();
|
||
// 跑!
|
||
$this->driver->run();
|
||
}
|
||
|
||
/**
|
||
* 停止框架运行
|
||
*
|
||
* 未测试
|
||
* @param int $retcode 退出码
|
||
* @throws ZMKnownException
|
||
*/
|
||
public function stop(int $retcode = 0): void
|
||
{
|
||
switch ($this->driver->getName()) {
|
||
case 'swoole':
|
||
/* @phpstan-ignore-next-line */
|
||
$this->driver->getSwooleServer()->shutdown();
|
||
break;
|
||
case 'workerman':
|
||
if (extension_loaded('posix') && isset(ProcessStateManager::getProcessState(ZM_PROCESS_MASTER)['pid'])) {
|
||
posix_kill(ProcessStateManager::getProcessState(ZM_PROCESS_MASTER)['pid'], SIGTERM);
|
||
} else {
|
||
Worker::stopAll($retcode);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重载框架的 worker 进程,重新加载模块及代码
|
||
*
|
||
* 此方法仅限于 Unix 环境下的多进程模式(即存在 Worker 进程的模式)使用,Windows 环境、单进程模式使用无效
|
||
*
|
||
* 未测试,需要对单进程等特殊情况做判断,因为单进程等模式无法重启
|
||
*/
|
||
public function reload()
|
||
{
|
||
switch ($this->driver->getName()) {
|
||
case 'swoole':
|
||
/* @phpstan-ignore-next-line */
|
||
$this->driver->getSwooleServer()->reload();
|
||
break;
|
||
case 'workerman':
|
||
Worker::reloadSelf();
|
||
break;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取传入的参数
|
||
*/
|
||
public function getArgv(): array
|
||
{
|
||
return $this->argv;
|
||
}
|
||
|
||
/**
|
||
* 获取驱动
|
||
*/
|
||
public function getDriver(): Driver
|
||
{
|
||
if ($this->driver === null) {
|
||
$this->driver = new WorkermanDriver();
|
||
}
|
||
return $this->driver;
|
||
}
|
||
|
||
/**
|
||
* 初始化驱动及相关事件
|
||
* 实例化 Driver 对象
|
||
*
|
||
* @throws \Exception
|
||
*/
|
||
public function initDriver(): void
|
||
{
|
||
switch ($driver = config('global.driver')) {
|
||
case 'swoole':
|
||
if (DIRECTORY_SEPARATOR === '\\') {
|
||
logger()->emergency('Windows does not support swoole driver!');
|
||
exit(1);
|
||
}
|
||
config(['global.swoole_options.driver_init_policy' => DriverInitPolicy::MULTI_PROCESS_INIT_IN_MASTER]);
|
||
$this->driver = new SwooleDriver(config('global.swoole_options'));
|
||
$this->driver->initDriverProtocols(config('global.servers'));
|
||
break;
|
||
case 'workerman':
|
||
config(['global.workerman_options.driver_init_policy' => DriverInitPolicy::MULTI_PROCESS_INIT_IN_MASTER]);
|
||
$this->driver = new WorkermanDriver(config('global.workerman_options'));
|
||
$this->driver->initDriverProtocols(config('global.servers'));
|
||
break;
|
||
default:
|
||
logger()->error(zm_internal_errcode('E00081') . '未知的驱动类型 ' . $driver . ' !');
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化框架并输出一些信息
|
||
*
|
||
* 绑定、注册框架本身的事件到 Driver 的 EventProvider 中
|
||
*/
|
||
public function initFramework(): void
|
||
{
|
||
// private-mode 模式下,不输出任何内容
|
||
if (!$this->argv['private-mode']) {
|
||
$this->printProperties();
|
||
$this->printMotd();
|
||
}
|
||
|
||
ContainerBindingListener::listenForEvents();
|
||
|
||
// 添加框架需要监听的顶层事件监听器
|
||
// worker 事件
|
||
ob_event_provider()->addEventListener(WorkerStartEvent::getName(), [WorkerEventListener::getInstance(), 'onWorkerStart999'], 999);
|
||
ob_event_provider()->addEventListener(WorkerStartEvent::getName(), [WorkerEventListener::getInstance(), 'onWorkerStart1'], 1);
|
||
ob_event_provider()->addEventListener(WorkerStopEvent::getName(), [WorkerEventListener::getInstance(), 'onWorkerStop999'], 999);
|
||
ob_event_provider()->addEventListener(WorkerStopEvent::getName(), [WorkerEventListener::getInstance(), 'onWorkerStop1'], 1);
|
||
// Http 事件
|
||
ob_event_provider()->addEventListener(HttpRequestEvent::getName(), [HttpEventListener::getInstance(), 'onRequest999'], 999);
|
||
ob_event_provider()->addEventListener(HttpRequestEvent::getName(), [HttpEventListener::getInstance(), 'onRequest1'], 1);
|
||
// manager 事件
|
||
ob_event_provider()->addEventListener(ManagerStartEvent::getName(), [ManagerEventListener::getInstance(), 'onManagerStart'], 999);
|
||
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);
|
||
ob_event_provider()->addEventListener(WebSocketCloseEvent::getName(), [WSEventListener::getInstance(), 'onWebSocketClose'], 999);
|
||
ob_event_provider()->addEventListener(WebSocketMessageEvent::getName(), [WSEventListener::getInstance(), 'onWebSocketMessage'], 999);
|
||
|
||
// 框架多进程依赖
|
||
if (defined('ZM_STATE_DIR') && !is_dir(ZM_STATE_DIR)) {
|
||
FileSystem::createDir(ZM_STATE_DIR);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 打印属性表格
|
||
*/
|
||
private function printProperties(): void
|
||
{
|
||
$properties = [];
|
||
// 打印工作目录
|
||
$properties['working_dir'] = WORKING_DIR;
|
||
// 打印环境信息
|
||
$properties['environment'] = ZMConfig::getInstance()->getEnvironment();
|
||
// 打印驱动
|
||
$properties['driver'] = config('global.driver');
|
||
// 打印logger显示等级
|
||
$properties['log_level'] = $this->argv['log-level'] ?? config('global.log_level') ?? 'info';
|
||
// 打印框架版本
|
||
$properties['version'] = self::VERSION . (LOAD_MODE === 0 ? (' (build ' . ZM_VERSION_ID . ')') : '');
|
||
// 打印 PHP 版本
|
||
$properties['php_version'] = PHP_VERSION;
|
||
// 打印 master 进程的 pid
|
||
$properties['master_pid'] = getmypid();
|
||
// 打印进程模型
|
||
if ($this->driver->getName() === 'swoole') {
|
||
$properties['process_mode'] = 'MST1';
|
||
ProcessStateManager::$process_mode['master'] = 1;
|
||
if (config('global.swoole_options.swoole_server_mode') === SWOOLE_BASE) {
|
||
$worker_num = config('global.swoole_options.swoole_set.worker_num');
|
||
if ($worker_num === null || $worker_num === 1) {
|
||
$properties['process_mode'] .= 'MAN0#0';
|
||
ProcessStateManager::$process_mode['manager'] = 0;
|
||
ProcessStateManager::$process_mode['worker'] = 0;
|
||
} elseif ($worker_num === 0) {
|
||
$properties['process_mode'] .= 'MAN0#' . swoole_cpu_num();
|
||
ProcessStateManager::$process_mode['manager'] = 0;
|
||
ProcessStateManager::$process_mode['worker'] = swoole_cpu_num();
|
||
} else {
|
||
$properties['process_mode'] .= 'MAN0#' . ($worker = config('global.swoole_options.swoole_set.worker_num') ?? swoole_cpu_num());
|
||
ProcessStateManager::$process_mode['manager'] = 0;
|
||
ProcessStateManager::$process_mode['worker'] = $worker;
|
||
}
|
||
} else {
|
||
$worker = config('global.swoole_options.swoole_set.worker_num') === 0 ? swoole_cpu_num() : config('global.swoole_options.swoole_set.worker_num') ?? swoole_cpu_num();
|
||
$properties['process_mode'] .= 'MAN1#' . $worker;
|
||
ProcessStateManager::$process_mode['manager'] = 1;
|
||
ProcessStateManager::$process_mode['worker'] = $worker;
|
||
}
|
||
} elseif ($this->driver->getName() === 'workerman') {
|
||
$properties['process_mode'] = 'MST1';
|
||
ProcessStateManager::$process_mode['master'] = 1;
|
||
$worker_num = config('global.workerman_options.workerman_worker_num');
|
||
if (DIRECTORY_SEPARATOR === '\\') {
|
||
$properties['process_mode'] .= '#0';
|
||
ProcessStateManager::$process_mode['manager'] = 0;
|
||
ProcessStateManager::$process_mode['worker'] = 0;
|
||
} else {
|
||
$worker_num = $worker_num === 0 ? 1 : ($worker_num ?? 1);
|
||
$properties['process_mode'] .= '#' . $worker_num;
|
||
ProcessStateManager::$process_mode['manager'] = 0;
|
||
ProcessStateManager::$process_mode['worker'] = $worker_num;
|
||
}
|
||
}
|
||
// 打印监听端口
|
||
foreach (config('global.servers') as $k => $v) {
|
||
$properties['listen_' . $k] = $v['type'] . '://' . $v['host'] . ':' . $v['port'];
|
||
}
|
||
// 打印 database 连接信息
|
||
foreach (config('global.database') as $name => $db) {
|
||
if (!$db['enable']) {
|
||
continue;
|
||
}
|
||
$properties['db[' . $name . ']'] = match ($db['type']) {
|
||
'sqlite' => $db['type'] . '://' . $db['dbname'],
|
||
'mysql' => $db['type'] . '://' . $db['host'] . ':' . $db['port'] . '/' . $db['dbname'],
|
||
default => '未知数据库类型',
|
||
};
|
||
}
|
||
// 打印 redis 连接信息
|
||
foreach (config('global.redis') as $name => $redis) {
|
||
if ($redis['enable']) {
|
||
$properties['redis[' . $name . ']'] = $redis['host'] . ':' . $redis['port'];
|
||
}
|
||
}
|
||
if (LOAD_MODE === 0) {
|
||
logger()->info('框架正以源码模式启动');
|
||
}
|
||
logger()->debug('Starting framework with properties: ' . json_encode($properties, JSON_UNESCAPED_SLASHES));
|
||
$this->translateProperties($properties);
|
||
$printer = new TablePrinter($properties);
|
||
$printer->setValueColor('random')->printAll();
|
||
}
|
||
|
||
/**
|
||
* 翻译属性
|
||
*/
|
||
private function translateProperties(array &$properties): void
|
||
{
|
||
$translations = [
|
||
'working_dir' => '工作目录',
|
||
'worker' => '工作进程数',
|
||
'environment' => '环境类型',
|
||
'log_level' => '日志级别',
|
||
'version' => '框架版本',
|
||
'master_pid' => '主进程 PID',
|
||
'app_version' => '应用版本',
|
||
'task_worker' => '任务进程数',
|
||
'mysql_pool' => '数据库',
|
||
'mysql' => '数据库',
|
||
'redis_pool' => 'Redis',
|
||
'php_version' => 'PHP 版本',
|
||
'process_mode' => '进程模型',
|
||
'driver' => '驱动类型',
|
||
'listen_0' => '监听端口1',
|
||
'listen_1' => '监听端口2',
|
||
'listen_2' => '监听端口3',
|
||
'listen_3' => '监听端口4',
|
||
];
|
||
// 更换数组键名
|
||
foreach ($properties as $k => $v) {
|
||
if (isset($translations[$k])) {
|
||
$keys = array_keys($properties);
|
||
$keys[array_search($k, $keys, false)] = $translations[$k];
|
||
$properties = ($t = array_combine($keys, $properties)) ? $t : $properties;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 打印 MOTD
|
||
*/
|
||
private function printMotd(): void
|
||
{
|
||
// 先获取终端宽度,防止超过边界换行
|
||
$tty_width = (new TablePrinter([]))->fetchTerminalSize();
|
||
if ($s = EasterEgg::checkFrameworkPermissionCall()) {
|
||
echo $s;
|
||
return;
|
||
}
|
||
// 从源码目录、框架本身的初始目录寻找 MOTD 文件
|
||
if (file_exists(SOURCE_ROOT_DIR . '/config/motd.txt')) {
|
||
$motd = file_get_contents(SOURCE_ROOT_DIR . '/config/motd.txt');
|
||
} else {
|
||
$motd = file_get_contents(FRAMEWORK_ROOT_DIR . '/config/motd.txt');
|
||
}
|
||
$motd = explode("\n", $motd);
|
||
foreach ($motd as $k => $v) {
|
||
$motd[$k] = substr($v, 0, $tty_width);
|
||
}
|
||
$motd = implode("\n", $motd);
|
||
echo $motd;
|
||
}
|
||
|
||
/**
|
||
* 初始化 OnSetup 注解
|
||
*/
|
||
private function initSetupAnnotations(): void
|
||
{
|
||
if (\Phar::running() !== '') {
|
||
// 在 Phar 下,不需要新启动进程了,因为 Phar 没办法重载,自然不需要考虑多进程的加载 reload 问题
|
||
require FRAMEWORK_ROOT_DIR . '/src/Globals/script_setup_loader.php';
|
||
$r = _zm_setup_loader();
|
||
$result_code = 0;
|
||
} else {
|
||
// 其他情况下,需要启动一个新的进程执行结果后退出内存中加载的内容,以便重载模块
|
||
$r = exec(PHP_BINARY . ' ' . FRAMEWORK_ROOT_DIR . '/src/Globals/script_setup_loader.php', $output, $result_code);
|
||
}
|
||
if ($result_code !== 0) {
|
||
logger()->emergency('代码解析错误!');
|
||
exit(1);
|
||
}
|
||
$json = json_decode($r, true, 512, JSON_THROW_ON_ERROR);
|
||
if (!is_array($json)) {
|
||
logger()->error(zm_internal_errcode('E00012') . '解析 @OnSetup 时发生错误,请检查代码!');
|
||
return;
|
||
}
|
||
$this->setup_annotations = $json['setup'];
|
||
foreach (($this->setup_annotations) as $v) {
|
||
logger()->debug('Calling @OnSetup: ' . $v['class']);
|
||
$cname = $v['class'];
|
||
$c = new $cname();
|
||
$method = $v['method'];
|
||
$c->{$method}();
|
||
}
|
||
}
|
||
}
|