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