feat(v0.2.2): 调试服务器自启动 — InitService.start_auto_scripts()

通用自动启动脚本机制:
- base_config.yaml 新增 auto_start_scripts 配置段
- InitService 在日志服务就绪后扫描并拉起配置的脚本
- 脚本不存在时优雅跳过,不影响框架启动
- 每个脚本独立子进程 + 独立进程组 (os.setsid)
- 框架 shutdown 时自动终止 (SIGTERM → killpg)
- 模块级单例跟踪所有进程,避免重复启动

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
qinglong
2026-06-13 10:58:01 +08:00
parent 9de094ea54
commit dcef1173f2
3 changed files with 103 additions and 4 deletions
+10
View File
@@ -11,6 +11,16 @@ plugins:
auto_load: true auto_load: true
hot_reload: true hot_reload: true
max_retry_count: 3 max_retry_count: 3
# 自动启动脚本 — 框架启动时后台拉起
auto_start_scripts:
enabled: true
scripts: []
# 示例:
# - name: cyrene_debug
# path: ~/cyrene_debug_server.py
# enabled: true
# args: []
# cwd: ~
# TUI配置 # TUI配置
tui: tui:
enabled: true enabled: true
+3
View File
@@ -59,6 +59,9 @@ class SenSuFramework:
log_service = LogService(base_config) log_service = LogService(base_config)
self.service_manager.register_service("log", log_service) self.service_manager.register_service("log", log_service)
# 2.5 自动启动脚本 (日志服务就绪后)
await init_service.start_auto_scripts()
# 3. 核心桥接服务 # 3. 核心桥接服务
core_bridge = CoreBridge() core_bridge = CoreBridge()
await core_bridge.start() await core_bridge.start()
+87 -1
View File
@@ -3,19 +3,29 @@
import logging import logging
import asyncio import asyncio
import signal
from pathlib import Path from pathlib import Path
from typing import Dict, Any from typing import Dict, Any, List
import yaml import yaml
import os import os
logger = logging.getLogger(__name__) 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: class InitService:
"""初始化服务""" """初始化服务"""
def __init__(self, config_path: str = "config/framework"): def __init__(self, config_path: str = "config/framework"):
self.config_path = Path(config_path) self.config_path = Path(config_path)
self.configs: Dict[str, Any] = {} self.configs: Dict[str, Any] = {}
self.script_processes: List = _get_script_processes()
logger.debug("InitService初始化开始") logger.debug("InitService初始化开始")
async def initialize_framework(self): async def initialize_framework(self):
@@ -39,6 +49,60 @@ class InitService:
logger.error(f"框架初始化失败: {str(e)}", exc_info=True) logger.error(f"框架初始化失败: {str(e)}", exc_info=True)
raise 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): async def _load_configs(self):
"""加载配置文件""" """加载配置文件"""
try: try:
@@ -155,6 +219,14 @@ class InitService:
'auto_load': True, 'auto_load': True,
'hot_reload': True, 'hot_reload': True,
'max_retry_count': 3 '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: try:
logger.info("关闭初始化服务") 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() self.configs.clear()
logger.debug("初始化服务关闭完成") logger.debug("初始化服务关闭完成")
except Exception as e: except Exception as e: