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:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user