From dcef1173f2e4ba9e40e9e04029b0018ff7522530 Mon Sep 17 00:00:00 2001 From: qinglong Date: Sat, 13 Jun 2026 10:58:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(v0.2.2):=20=E8=B0=83=E8=AF=95=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8=E8=87=AA=E5=90=AF=E5=8A=A8=20=E2=80=94=20Ini?= =?UTF-8?q?tService.start=5Fauto=5Fscripts()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 通用自动启动脚本机制: - base_config.yaml 新增 auto_start_scripts 配置段 - InitService 在日志服务就绪后扫描并拉起配置的脚本 - 脚本不存在时优雅跳过,不影响框架启动 - 每个脚本独立子进程 + 独立进程组 (os.setsid) - 框架 shutdown 时自动终止 (SIGTERM → killpg) - 模块级单例跟踪所有进程,避免重复启动 Co-Authored-By: Claude --- config/framework/base_config.yaml | 10 ++++ main.py | 5 +- services/init_service.py | 92 ++++++++++++++++++++++++++++++- 3 files changed, 103 insertions(+), 4 deletions(-) diff --git a/config/framework/base_config.yaml b/config/framework/base_config.yaml index af68063..b13cff0 100644 --- a/config/framework/base_config.yaml +++ b/config/framework/base_config.yaml @@ -11,6 +11,16 @@ plugins: auto_load: true hot_reload: true max_retry_count: 3 +# 自动启动脚本 — 框架启动时后台拉起 +auto_start_scripts: + enabled: true + scripts: [] + # 示例: + # - name: cyrene_debug + # path: ~/cyrene_debug_server.py + # enabled: true + # args: [] + # cwd: ~ # TUI配置 tui: enabled: true diff --git a/main.py b/main.py index b90ed64..1bc8698 100644 --- a/main.py +++ b/main.py @@ -58,7 +58,10 @@ class SenSuFramework: # 2. 日志服务 log_service = LogService(base_config) self.service_manager.register_service("log", log_service) - + + # 2.5 自动启动脚本 (日志服务就绪后) + await init_service.start_auto_scripts() + # 3. 核心桥接服务 core_bridge = CoreBridge() await core_bridge.start() diff --git a/services/init_service.py b/services/init_service.py index 2736caa..6ecd318 100644 --- a/services/init_service.py +++ b/services/init_service.py @@ -3,19 +3,29 @@ import logging import asyncio +import signal from pathlib import Path -from typing import Dict, Any +from typing import Dict, Any, List import yaml import os logger = logging.getLogger(__name__) + +def _get_script_processes() -> List: + """获取全局自动启动脚本进程列表(模块级单例)""" + if not hasattr(_get_script_processes, "_procs"): + _get_script_processes._procs = [] + return _get_script_processes._procs + + class InitService: """初始化服务""" - + def __init__(self, config_path: str = "config/framework"): self.config_path = Path(config_path) self.configs: Dict[str, Any] = {} + self.script_processes: List = _get_script_processes() logger.debug("InitService初始化开始") async def initialize_framework(self): @@ -34,10 +44,64 @@ class InitService: logger.info("框架初始化完成") return self.configs - + except Exception as e: logger.error(f"框架初始化失败: {str(e)}", exc_info=True) raise + + async def start_auto_scripts(self): + """启动配置中声明的自动启动脚本(在日志服务就绪后调用)""" + try: + cfg = self.configs.get("base", {}) + scripts_cfg = cfg.get("auto_start_scripts", {}) + if not scripts_cfg.get("enabled", True): + logger.info("自动启动脚本已禁用") + return + + entries = scripts_cfg.get("scripts", []) + if not entries: + logger.debug("没有配置自动启动脚本") + return + + logger.info(f"检查自动启动脚本 ({len(entries)} 个)...") + for entry in entries: + if not entry.get("enabled", True): + logger.debug(f" 跳过已禁用的脚本: {entry.get('name', entry.get('path', '?'))}") + continue + + script_path = os.path.expanduser(entry["path"]) + if not os.path.isfile(script_path): + logger.info( + f" 自动启动脚本不存在,跳过: {entry.get('name', script_path)}" + ) + continue + + script_args = entry.get("args", []) + script_name = entry.get("name", os.path.basename(script_path)) + cwd = entry.get("cwd") or os.path.dirname(script_path) + + try: + proc = await asyncio.create_subprocess_exec( + "python3" if script_path.endswith(".py") else script_path, + script_path if script_path.endswith(".py") else None, + *script_args, + cwd=cwd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + preexec_fn=os.setsid, + ) + # Filter out None (when script_path is the executable itself) + self.script_processes.append( + {"name": script_name, "proc": proc, "path": script_path} + ) + logger.info(f"✅ 自动启动脚本已拉起: {script_name} (pid={proc.pid})") + except Exception as start_err: + logger.warning( + f"⚠️ 自动启动脚本失败 {script_name}: {start_err}" + ) + + except Exception as e: + logger.warning(f"自动启动脚本扫描出错 (不影响框架): {e}") async def _load_configs(self): """加载配置文件""" @@ -155,6 +219,14 @@ class InitService: 'auto_load': True, 'hot_reload': True, 'max_retry_count': 3 + }, + 'auto_start_scripts': { + 'enabled': True, + 'scripts': [ + # 示例: 自动拉起 cyrene_debug_server.py + # {'name': 'cyrene_debug', 'path': '~/cyrene_debug_server.py', + # 'enabled': True, 'args': [], 'cwd': '~'}, + ] } } @@ -199,6 +271,20 @@ class InitService: """关闭初始化服务""" try: logger.info("关闭初始化服务") + # 终止所有自动启动的脚本 + for entry in self.script_processes: + proc = entry.get("proc") + if proc and proc.returncode is None: + try: + os.killpg(os.getpgid(proc.pid), signal.SIGTERM) + logger.info(f" 已终止: {entry.get('name', '?')} (pid={proc.pid})") + except ProcessLookupError: + pass + except Exception as kill_err: + logger.warning( + f" 终止失败 {entry.get('name', '?')}: {kill_err}" + ) + self.script_processes.clear() self.configs.clear() logger.debug("初始化服务关闭完成") except Exception as e: