'connect_id', 'type' => 'string', 'size' => 30, ], [ 'key' => 'type', 'type' => 'int', ], ]); } catch (ConnectionManager\TableException $e) { logger()->emergency(zm_internal_errcode('E00008') . $e->getMessage()); exit(1); } try { // 初始化日志 Console::init( ZMConfig::get('global', 'info_level') ?? 2, self::$server, $args['log-theme'] ?? 'default', ($o = ZMConfig::get('console_color')) === false ? [] : $o ); // 是否同步输出到文件 if ((ZMConfig::get('global', 'runtime')['save_console_log_file'] ?? false) !== false) { Console::setOutputFile(ZMConfig::get('global', 'runtime')['save_console_log_file']); } // 设置默认时区 $timezone = ZMConfig::get('global', 'timezone') ?? 'Asia/Shanghai'; date_default_timezone_set($timezone); // 读取 Swoole 配置 $this->swoole_server_config = ZMConfig::get('global', 'swoole'); $this->swoole_server_config['log_level'] = SWOOLE_LOG_DEBUG; // 是否启用远程终端 $remote_terminal = ZMConfig::get('global', 'remote_terminal')['status'] ?? false; // 加载服务器事件 if (!$instant_mode) { $this->loadServerEvents(); } // 解析命令行参数 [$coroutine_mode, $terminal_id, $remote_terminal] = $this->parseCliArgs(self::$argv, compact('remote_terminal')); // 设置默认最长等待时间 if (!isset($this->swoole_server_config['max_wait_time'])) { $this->swoole_server_config['max_wait_time'] = 5; } // 设置最大 worker 进程数 $worker = $this->swoole_server_config['worker_num'] ?? swoole_cpu_num(); define('ZM_WORKER_NUM', $worker); // 初始化原子计数器 ZMAtomic::init(); // 非静默模式下打印启动信息 if (!self::$argv['private-mode']) { $this->printProperties($remote_terminal, $args); } // 预览模式则直接提出 if ($args['preview'] ?? false) { exit(); } // 初始化服务器 self::$server = new Server( ZMConfig::get('global', 'host'), ZMConfig::get('global', 'port'), ZMConfig::get('global', 'runtime')['swoole_server_mode'] ?? SWOOLE_PROCESS ); // 监听远程终端 if ($remote_terminal) { $conf = ZMConfig::get('global', 'remote_terminal') ?? [ 'status' => true, 'host' => '127.0.0.1', 'port' => 20002, 'token' => '', ]; $welcome_msg = capture_output([logger('trm'), 'info'], ['欢迎使用炸毛远程终端!输入 `help` 查看帮助!']); $motd = capture_output(function () { $this->printMotd(); }); /** @var Port $port */ $port = self::$server->listen($conf['host'], $conf['port'], SWOOLE_SOCK_TCP); $port->set([ 'open_http_protocol' => false, ]); $port->on('connect', function (\Swoole\Server $serv, $fd) use ($welcome_msg, $conf, $motd) { ManagerGM::pushConnect($fd, 'terminal'); // 推送欢迎信息 $serv->send($fd, $motd); // 要求输入令牌 if (!empty($conf['token'])) { $serv->send($fd, '请输入令牌:'); } else { $serv->send($fd, $welcome_msg . "\n>>> "); } }); $port->on('receive', function ($serv, $fd, $reactor_id, $data) use ($welcome_msg, $conf) { ob_start(); try { $arr = LightCacheInside::get('light_array', 'input_token') ?? []; if (empty($arr[$fd] ?? '') && $conf['token'] !== '') { $token = trim($data); if ($token === $conf['token']) { SpinLock::transaction('input_token', static function () use ($fd, $token) { $arr = LightCacheInside::get('light_array', 'input_token'); $arr[$fd] = $token; LightCacheInside::set('light_array', 'input_token', $arr); }); $serv->send($fd, capture_output([logger('trm'), 'info'], ['令牌验证成功!'])); $serv->send($fd, $welcome_msg . "\n>>> "); } else { $serv->send($fd, capture_output([logger('trm'), 'error'], ['令牌验证失败!'])); $serv->close($fd); } return; } if (trim($data) === 'exit' || trim($data) === 'q') { $serv->send($fd, capture_output([logger('trm'), 'info'], ['再见!'])); $serv->close($fd); return; } Terminal::executeCommand(trim($data)); } catch (Exception $e) { $error_msg = $e->getMessage() . ' at ' . $e->getFile() . '(' . $e->getLine() . ')'; logger('trm')->error(zm_internal_errcode('E00009') . 'Uncaught exception ' . get_class($e) . ' when calling "open": ' . $error_msg); logger('trm')->error($e->getTraceAsString()); } catch (Error $e) { $error_msg = $e->getMessage() . ' at ' . $e->getFile() . '(' . $e->getLine() . ')'; logger('trm')->error(zm_internal_errcode('E00009') . 'Uncaught ' . get_class($e) . ' when calling "open": ' . $error_msg); logger('trm')->error($e->getTraceAsString()); } $r = ob_get_clean(); if (!empty($r)) { $serv->send($fd, $r); } if (!in_array(trim($data), ['r', 'reload'])) { $serv->send($fd, '>>> '); } }); $port->on('close', function ($serv, $fd) { ManagerGM::popConnect($fd); }); } // 设置服务器配置 self::$server->set($this->swoole_server_config); Console::setServer(self::$server); // 非静默模式下,打印欢迎信息 if (!self::$argv['private-mode']) { $this->printMotd(); } $global_hook = ZMConfig::get('global', 'runtime')['swoole_coroutine_hook_flags'] ?? (SWOOLE_HOOK_ALL & (~SWOOLE_HOOK_CURL)); if ($coroutine_mode) { Runtime::enableCoroutine(true, $global_hook); } else { Runtime::enableCoroutine(false, SWOOLE_HOOK_ALL); } // 注册 Swoole Server 的事件 $this->registerServerEvents(); // 初始化缓存 $r = ZMConfig::get('global', 'light_cache') ?? [ 'size' => 512, // 最多允许储存的条数(需要2的倍数) 'max_strlen' => 32768, // 单行字符串最大长度(需要2的倍数) 'hash_conflict_proportion' => 0.6, // Hash冲突率(越大越好,但是需要的内存更多) 'persistence_path' => DataProvider::getDataFolder() . '_cache.json', 'auto_save_interval' => 900, ]; LightCache::init($r); LightCacheInside::init(); // 初始化自旋锁 SpinLock::init($r['size']); // 注册全局错误处理器 set_error_handler(static function ($error_no, $error_msg, $error_file, $error_line) { $tips = [ E_WARNING => ['PHP Warning: ', 'warning'], E_NOTICE => ['PHP Notice: ', 'notice'], E_USER_ERROR => ['PHP Error: ', 'error'], E_USER_WARNING => ['PHP Warning: ', 'warning'], E_USER_NOTICE => ['PHP Notice: ', 'notice'], E_STRICT => ['PHP Strict: ', 'notice'], E_RECOVERABLE_ERROR => ['PHP Recoverable Error: ', 'error'], E_DEPRECATED => ['PHP Deprecated: ', 'notice'], E_USER_DEPRECATED => ['PHP User Deprecated: ', 'notice'], ]; $level_tip = $tips[$error_no] ?? ['PHP Unknown: ', 'error']; $error = $level_tip[0] . $error_msg . ' in ' . $error_file . ' on ' . $error_line; logger()->{$level_tip[1]}($error); // 如果 return false 则错误会继续递交给 PHP 标准错误处理 return true; }, E_ALL | E_STRICT); } catch (Exception $e) { logger()->emergency('框架初始化失败,请检查!'); logger()->emergency(zm_internal_errcode('E00010') . $e->getMessage()); if (strpos($e->getMessage(), 'Address already in use') !== false) { if (!ProcessManager::isStateEmpty()) { logger()->alert('检测到可能残留框架的工作进程,请先通过命令杀死:server:stop --force'); } } logger()->debug($e); exit; } } public static function loadFrameworkState() { if (!file_exists(DataProvider::getDataFolder() . '.state.json')) { return []; } $r = json_decode(file_get_contents(DataProvider::getDataFolder() . '.state.json'), true); if ($r === null) { $r = []; } return $r; } public static function saveFrameworkState($data) { return file_put_contents(DataProvider::getDataFolder() . '.state.json', json_encode($data, 64 | 128 | 256)); } public function start() { try { self::$loaded_files = get_included_files(); LightCacheInside::set('tmp_kv', 'start_time', microtime(true)); self::$server->start(); zm_atomic('server_is_stopped')->set(1); if (!self::$argv['private-mode']) { logger()->info('炸毛框架已停止!'); } } catch (Throwable $e) { exit(zm_internal_errcode('E00011') . '框架发生未捕获的异常:' . get_class($e) . ': ' . $e->getMessage() . PHP_EOL); } } /** * 打印 MOTD */ private function printMotd(): void { $tty_width = (new TablePrinter([]))->fetchTerminalSize(); if (file_exists(DataProvider::getSourceRootDir() . '/config/motd.txt')) { $motd = file_get_contents(DataProvider::getSourceRootDir() . '/config/motd.txt'); } else { $motd = file_get_contents(__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; } /** * 翻译属性 */ private function translateProperties(array &$properties): void { $translations = [ 'working_dir' => '工作目录', 'listen' => '监听地址', 'worker' => '工作进程数', 'environment' => '环境类型', 'log_level' => '日志级别', 'version' => '框架版本', 'master_pid' => '主进程 PID', 'app_version' => '应用版本', 'task_worker' => '任务进程数', 'mysql_pool' => '数据库', 'mysql' => '数据库', 'redis_pool' => 'Redis', 'static_file_server' => '静态文件托管', 'php_version' => 'PHP 版本', 'swoole_version' => 'Swoole 版本', ]; foreach ($properties as $k => $v) { if (isset($translations[$k])) { $this->changeArrayKey($properties, $k, $translations[$k]); } } } private function loadServerEvents() { if (Phar::running() !== '') { ob_start(); include_once DataProvider::getFrameworkRootDir() . '/src/ZM/script_setup_loader.php'; $r = ob_get_clean(); $result_code = 0; } else { $r = exec(PHP_BINARY . ' ' . DataProvider::getFrameworkRootDir() . '/src/ZM/script_setup_loader.php', $output, $result_code); } if ($result_code !== 0) { logger()->emergency('代码解析错误!'); exit(1); } $json = json_decode($r, true); if (!is_array($json)) { logger()->error(zm_internal_errcode('E00012') . '解析 @SwooleHandler 及 @OnSetup 时发生错误,请检查代码!'); } $this->setup_events = $json; } /** * 从全局配置文件里读取注入系统事件的类 * * @throws ReflectionException * @throws ReflectionException */ private function registerServerEvents() { $reader = new AnnotationReader(); $all = ZMUtil::getClassesPsr4(FRAMEWORK_ROOT_DIR . '/src/ZM/Event/SwooleEvent/', 'ZM\\Event\\SwooleEvent'); foreach ($all as $v) { $class = new $v(); $reflection_class = new ReflectionClass($class); $anno_class = $reader->getClassAnnotation($reflection_class, SwooleHandler::class); if ($anno_class !== null) { // 类名形式的注解 $this->setup_events['event'][] = [ 'class' => $v, 'method' => 'onCall', 'event' => $anno_class->event, ]; } } foreach (($this->setup_events['setup'] ?? []) as $v) { logger()->debug('Calling @OnSetup: ' . $v['class']); $c = ZMUtil::getModInstance($v['class']); $method = $v['method']; $c->{$method}(); } foreach ($this->setup_events['event'] as $v) { self::$server->on($v['event'], function (...$param) use ($v) { ZMUtil::getModInstance($v['class'])->{$v['method']}(...$param); }); } } /** * 解析命令行的 $argv 参数 * * @param array $args 命令行参数 * @throws InvalidArgumentException 参数不正确,框架需要捕获并终止启动 * @return array 解析后并需要处理的参数 */ private function parseCliArgs(array $args, array $defaults): array { $coroutine_mode = true; global $terminal_id; $terminal_id = uuidgen(); $error_occur = false; $remote_terminal = $defaults['remote_terminal']; foreach ($args as $x => $y) { if ($y === false || is_null($y)) { continue; } switch ($x) { case 'worker-num': if ((int) $y >= 1 && (int) $y <= 1024) { $this->swoole_server_config['worker_num'] = (int) $y; } else { logger()->emergency(zm_internal_errcode('E00013') . '传入的 worker_num 参数不合法!必须在 1-1024 之间!'); $error_occur = true; } break; case 'task-worker-num': if ((int) $y >= 1 && (int) $y <= 1024) { $this->swoole_server_config['task_worker_num'] = (int) $y; $this->swoole_server_config['task_enable_coroutine'] = true; } else { logger()->emergency(zm_internal_errcode('E00013') . '传入的 task_worker_num 参数不合法!必须在 1-1024 之间!'); $error_occur = true; } break; case 'disable-coroutine': $coroutine_mode = false; break; case 'debug-mode': self::$argv['disable-safe-exit'] = true; $coroutine_mode = false; $terminal_id = null; self::$argv['watch'] = true; logger()->notice('已进入调试模式,请勿在生产环境中使用!'); break; case 'daemon': $this->swoole_server_config['daemonize'] = 1; Console::$theme = 'no-color'; Console::log('已启用守护进程,输出重定向到 ' . $this->swoole_server_config['log_file']); $terminal_id = null; break; case 'disable-console-input': case 'no-interaction': $terminal_id = null; break; case 'log-error': Console::setLevel(0); break; case 'log-warning': Console::setLevel(1); break; case 'log-info': Console::setLevel(2); break; case 'log-verbose': case 'verbose': Console::setLevel(3); break; case 'log-debug': Console::setLevel(4); break; case 'audit-mode': logger()->notice('审计模式已开启,请正常执行需要审计的流程,然后Ctrl+C正常结束框架'); logger()->notice('审计的日志文件将存放到:' . DataProvider::getWorkingDir() . '/audit.log'); if (file_exists(DataProvider::getWorkingDir() . '/audit.log')) { unlink(DataProvider::getWorkingDir() . '/audit.log'); } logger()->notice('框架将于5秒后开始启动...'); Console::setOutputFile(DataProvider::getWorkingDir() . '/audit.log'); Console::setLevel(4); sleep(5); break; case 'log-theme': Console::$theme = $y; break; case 'remote-terminal': $remote_terminal = true; break; case 'show-php-ver': default: break; } } if ($error_occur) { throw new InvalidArgumentException('命令行参数解析错误,请参阅上方日志!'); } return [$coroutine_mode, $terminal_id, $remote_terminal]; } /** * 更换数组键名 * * @param mixed $old_key * @param mixed $new_key */ private function changeArrayKey(array &$arr, $old_key, $new_key): void { $keys = array_keys($arr); $keys[array_search($old_key, $keys, false)] = $new_key; $arr = ($t = array_combine($keys, $arr)) ? $t : $arr; } /** * 打印属性表格 */ private function printProperties(bool $remote_terminal, array $args): void { $properties = []; $properties['working_dir'] = DataProvider::getWorkingDir(); $properties['listen'] = ZMConfig::get('global', 'host') . ':' . ZMConfig::get('global', 'port'); if (!isset($this->swoole_server_config['worker_num'])) { if ((ZMConfig::get('global', 'runtime')['swoole_server_mode'] ?? SWOOLE_PROCESS) === SWOOLE_PROCESS) { $properties['worker'] = swoole_cpu_num() . ' (auto)'; } else { $properties['single_proc_mode'] = 'true'; } } else { $properties['worker'] = $this->swoole_server_config['worker_num']; } $properties['environment'] = ($args['env'] ?? null) === null ? 'default' : $args['env']; $properties['log_level'] = strtoupper(zm_config('logging.level')); $properties['version'] = self::VERSION . (LOAD_MODE === 0 ? (' (build ' . ZM_VERSION_ID . ')') : ''); $properties['master_pid'] = posix_getpid(); if (APP_VERSION !== 'unknown') { $properties['app_version'] = APP_VERSION; } if (isset($this->swoole_server_config['task_worker_num'])) { $properties['task_worker'] = $this->swoole_server_config['task_worker_num']; } if ((ZMConfig::get('global', 'sql_config')['sql_host'] ?? '') !== '') { $conf = ZMConfig::get('global', 'sql_config'); $properties['mysql_pool'] = $conf['sql_database'] . '@' . $conf['sql_host'] . ':' . $conf['sql_port']; } if ((ZMConfig::get('global', 'mysql_config')['host'] ?? '') !== '') { $conf = ZMConfig::get('global', 'mysql_config'); $properties['mysql'] = $conf['dbname'] . '@' . $conf['host'] . ':' . $conf['port']; } if (ZMConfig::get('global', 'redis_config')['host'] !== '') { $conf = ZMConfig::get('global', 'redis_config'); $properties['redis_pool'] = $conf['host'] . ':' . $conf['port']; } if (ZMConfig::get('global', 'static_file_server')['status'] !== false) { $properties['static_file_server'] = 'enabled'; } if (self::$argv['show-php-ver'] !== false) { $properties['php_version'] = PHP_VERSION; $properties['swoole_version'] = SWOOLE_VERSION; } if ($remote_terminal) { $conf = ZMConfig::get('global', 'remote_terminal'); $properties['terminal'] = $conf['host'] . ':' . $conf['port']; } if (LOAD_MODE === 0) { logger()->info('框架正以源码模式启动'); } $this->translateProperties($properties); $printer = new TablePrinter($properties); $printer->setValueColor('random')->printAll(); } }