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
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
+4 -1
View File
@@ -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()
+89 -3
View File
@@ -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: