414 lines
17 KiB
Python
414 lines
17 KiB
Python
|
#!/usr/bin/env python
|
|||
|
# -*- coding: utf-8 -*-
|
|||
|
|
|||
|
"""
|
|||
|
脚本引擎服务
|
|||
|
核心脚本执行引擎,支持多脚本并发运行和生命周期管理
|
|||
|
"""
|
|||
|
|
|||
|
import os
|
|||
|
import sys
|
|||
|
import time
|
|||
|
import uuid
|
|||
|
import importlib.util
|
|||
|
import traceback
|
|||
|
from datetime import datetime
|
|||
|
from typing import Dict, Any, Optional, List
|
|||
|
from pathlib import Path
|
|||
|
|
|||
|
from sqlalchemy.orm import Session
|
|||
|
from data.session import get_session, get_async_session
|
|||
|
# from data.models.script_file import VWEDScriptFile
|
|||
|
from data.models.script_registry import VWEDScriptRegistry
|
|||
|
from data.models.script_execution_log import VWEDScriptExecutionLog
|
|||
|
from services.script_registry_service import get_global_registry
|
|||
|
from utils.logger import get_logger
|
|||
|
|
|||
|
logger = get_logger("services.script_engine")
|
|||
|
|
|||
|
|
|||
|
class ScriptExecutionContext:
|
|||
|
"""脚本执行上下文"""
|
|||
|
|
|||
|
def __init__(self, script_id: str, file_path: str, registry_service):
|
|||
|
self.script_id = script_id
|
|||
|
self.file_path = file_path
|
|||
|
self.registry = registry_service
|
|||
|
self.start_time = datetime.now()
|
|||
|
self.variables = {}
|
|||
|
self.logs = []
|
|||
|
|
|||
|
def log(self, level: str, message: str, **kwargs):
|
|||
|
"""记录日志"""
|
|||
|
log_entry = {
|
|||
|
"timestamp": datetime.now().isoformat(),
|
|||
|
"level": level,
|
|||
|
"message": message,
|
|||
|
"script_id": self.script_id,
|
|||
|
**kwargs
|
|||
|
}
|
|||
|
self.logs.append(log_entry)
|
|||
|
logger.log(getattr(logger, level.lower(), logger.info), f"[{self.script_id}] {message}")
|
|||
|
|
|||
|
|
|||
|
class MultiScriptEngine:
|
|||
|
"""多脚本服务引擎"""
|
|||
|
|
|||
|
def __init__(self):
|
|||
|
self.registry = get_global_registry()
|
|||
|
self.running_scripts: Dict[str, Dict[str, Any]] = {} # {script_id: script_info}
|
|||
|
self.script_contexts: Dict[str, ScriptExecutionContext] = {}
|
|||
|
|
|||
|
async def start_script_service(self, script_path: str, start_params: Dict = None) -> Dict[str, Any]:
|
|||
|
"""启动脚本服务 - 支持多脚本并发"""
|
|||
|
try:
|
|||
|
# 1. 验证脚本文件存在
|
|||
|
full_script_path = f"data/VWED/python_script/{script_path}"
|
|||
|
if not os.path.exists(full_script_path):
|
|||
|
return {"success": False, "error": f"脚本文件不存在: {script_path}"}
|
|||
|
|
|||
|
# 2. 检查是否已有同名脚本在运行
|
|||
|
existing_script = self.find_running_script_by_path(script_path)
|
|||
|
if existing_script:
|
|||
|
return {
|
|||
|
"success": False,
|
|||
|
"error": f"脚本 {script_path} 已在运行中,script_id: {existing_script['script_id']}"
|
|||
|
}
|
|||
|
|
|||
|
# 3. 生成唯一脚本ID
|
|||
|
script_id = f"{script_path}_{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}"
|
|||
|
|
|||
|
# 4. 创建执行上下文
|
|||
|
context = ScriptExecutionContext(script_id, script_path, self.registry)
|
|||
|
self.script_contexts[script_id] = context
|
|||
|
|
|||
|
# 5. 设置全局注册中心上下文
|
|||
|
self.registry.set_current_script(script_id)
|
|||
|
self.registry.add_active_script(script_id, script_path)
|
|||
|
|
|||
|
# 6. 创建数据库记录
|
|||
|
async with get_async_session() as session:
|
|||
|
# 查找脚本文件记录
|
|||
|
script_file = None # 这里需要根据script_path查找文件记录
|
|||
|
|
|||
|
# 创建注册记录
|
|||
|
registry_record = VWEDScriptRegistry(
|
|||
|
script_id=script_id,
|
|||
|
file_id=script_file.id if script_file else None,
|
|||
|
script_path=script_path,
|
|||
|
status='starting',
|
|||
|
start_params=start_params or {},
|
|||
|
host_info={
|
|||
|
"hostname": os.uname().nodename,
|
|||
|
"platform": sys.platform,
|
|||
|
"python_version": sys.version
|
|||
|
}
|
|||
|
)
|
|||
|
session.add(registry_record)
|
|||
|
await session.commit()
|
|||
|
|
|||
|
# 7. 加载并执行脚本
|
|||
|
result = await self._load_and_execute_script(script_id, full_script_path, context)
|
|||
|
|
|||
|
if result["success"]:
|
|||
|
# 8. 调用boot函数注册服务
|
|||
|
boot_result = await self._execute_boot_function(script_id, context)
|
|||
|
if not boot_result["success"]:
|
|||
|
await self._cleanup_failed_script(script_id)
|
|||
|
return boot_result
|
|||
|
|
|||
|
# 9. 更新运行状态
|
|||
|
self.running_scripts[script_id] = {
|
|||
|
"script_path": script_path,
|
|||
|
"started_at": context.start_time.isoformat(),
|
|||
|
"status": "running",
|
|||
|
"context": context
|
|||
|
}
|
|||
|
|
|||
|
# 10. 更新数据库状态
|
|||
|
await self._update_script_status(script_id, "running")
|
|||
|
|
|||
|
# 11. 获取注册统计
|
|||
|
registrations = self.registry.get_script_registrations(script_id)
|
|||
|
|
|||
|
logger.info(f"脚本服务启动成功: {script_id}")
|
|||
|
return {
|
|||
|
"success": True,
|
|||
|
"script_id": script_id,
|
|||
|
"message": f"脚本服务启动成功,已注册 {registrations['apis']} 个接口,{registrations['functions']} 个函数,{registrations['events']} 个事件监听器",
|
|||
|
"registrations": registrations
|
|||
|
}
|
|||
|
else:
|
|||
|
await self._cleanup_failed_script(script_id)
|
|||
|
return result
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"启动脚本服务失败: {e}", exc_info=True)
|
|||
|
if 'script_id' in locals():
|
|||
|
await self._cleanup_failed_script(script_id)
|
|||
|
return {"success": False, "error": f"脚本服务启动失败: {str(e)}"}
|
|||
|
|
|||
|
async def stop_script_service(self, script_id: str) -> Dict[str, Any]:
|
|||
|
"""停止特定脚本服务 - 关键功能"""
|
|||
|
try:
|
|||
|
if script_id not in self.running_scripts:
|
|||
|
return {"success": False, "error": f"脚本 {script_id} 未运行"}
|
|||
|
|
|||
|
# 获取注册统计
|
|||
|
registrations = self.registry.get_script_registrations(script_id)
|
|||
|
|
|||
|
# 清理该脚本的所有注册项 - 这是关键步骤
|
|||
|
self.registry.clear_script_registrations(script_id)
|
|||
|
|
|||
|
# 从运行列表移除
|
|||
|
script_info = self.running_scripts.pop(script_id)
|
|||
|
|
|||
|
# 清理上下文
|
|||
|
if script_id in self.script_contexts:
|
|||
|
del self.script_contexts[script_id]
|
|||
|
|
|||
|
# 更新数据库状态
|
|||
|
await self._update_script_status(script_id, "stopped")
|
|||
|
|
|||
|
logger.info(f"脚本服务已停止: {script_id}")
|
|||
|
return {
|
|||
|
"success": True,
|
|||
|
"message": f"脚本服务已停止 ({script_info['script_path']}),已清理 {registrations['apis']} 个接口、{registrations['functions']} 个函数、{registrations['events']} 个事件监听器",
|
|||
|
"registrations": registrations
|
|||
|
}
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"停止脚本服务失败: {e}", exc_info=True)
|
|||
|
return {"success": False, "error": f"停止脚本服务失败: {str(e)}"}
|
|||
|
|
|||
|
def get_running_scripts(self) -> List[Dict[str, Any]]:
|
|||
|
"""获取所有运行中的脚本"""
|
|||
|
result = []
|
|||
|
for script_id, script_info in self.running_scripts.items():
|
|||
|
registrations = self.registry.get_script_registrations(script_id)
|
|||
|
result.append({
|
|||
|
"script_id": script_id,
|
|||
|
"script_path": script_info["script_path"],
|
|||
|
"started_at": script_info["started_at"],
|
|||
|
"status": script_info["status"],
|
|||
|
"registrations": registrations
|
|||
|
})
|
|||
|
return result
|
|||
|
|
|||
|
def find_running_script_by_path(self, script_path: str) -> Optional[Dict[str, Any]]:
|
|||
|
"""根据脚本路径查找运行中的脚本"""
|
|||
|
for script_id, script_info in self.running_scripts.items():
|
|||
|
if script_info["script_path"] == script_path:
|
|||
|
return {"script_id": script_id, **script_info}
|
|||
|
return None
|
|||
|
|
|||
|
async def execute_script_function(self, script_id: str, function_name: str,
|
|||
|
function_args: Any) -> Dict[str, Any]:
|
|||
|
"""执行脚本中的指定函数"""
|
|||
|
try:
|
|||
|
if script_id not in self.running_scripts:
|
|||
|
return {"success": False, "error": f"脚本 {script_id} 未运行"}
|
|||
|
|
|||
|
# 从注册中心获取函数
|
|||
|
func_info = self.registry.get_function_info(function_name)
|
|||
|
if not func_info:
|
|||
|
return {"success": False, "error": f"函数 {function_name} 未注册"}
|
|||
|
|
|||
|
if func_info["script_id"] != script_id:
|
|||
|
return {"success": False, "error": f"函数 {function_name} 不属于脚本 {script_id}"}
|
|||
|
|
|||
|
context = self.script_contexts.get(script_id)
|
|||
|
start_time = time.time()
|
|||
|
|
|||
|
try:
|
|||
|
handler = func_info["handler"]
|
|||
|
|
|||
|
# 处理参数
|
|||
|
if func_info["is_async"]:
|
|||
|
result = await handler(function_args)
|
|||
|
else:
|
|||
|
result = handler(function_args)
|
|||
|
|
|||
|
execution_time_ms = int((time.time() - start_time) * 1000)
|
|||
|
|
|||
|
# 更新统计
|
|||
|
self.registry.update_function_call_stats(function_name, execution_time_ms, True)
|
|||
|
|
|||
|
# 记录执行日志
|
|||
|
await self._log_execution(script_id, "function_call", function_name,
|
|||
|
function_args, result, "success", execution_time_ms)
|
|||
|
|
|||
|
return {
|
|||
|
"success": True,
|
|||
|
"result": result,
|
|||
|
"execution_time_ms": execution_time_ms
|
|||
|
}
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
execution_time_ms = int((time.time() - start_time) * 1000)
|
|||
|
error_message = str(e)
|
|||
|
error_traceback = traceback.format_exc()
|
|||
|
|
|||
|
# 更新统计
|
|||
|
self.registry.update_function_call_stats(function_name, execution_time_ms, False)
|
|||
|
|
|||
|
# 记录执行日志
|
|||
|
await self._log_execution(script_id, "function_call", function_name,
|
|||
|
function_args, None, "failed", execution_time_ms,
|
|||
|
error_message, error_traceback)
|
|||
|
|
|||
|
return {
|
|||
|
"success": False,
|
|||
|
"error": error_message,
|
|||
|
"traceback": error_traceback,
|
|||
|
"execution_time_ms": execution_time_ms
|
|||
|
}
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"执行脚本函数失败: {e}", exc_info=True)
|
|||
|
return {"success": False, "error": f"执行脚本函数失败: {str(e)}"}
|
|||
|
|
|||
|
async def _load_and_execute_script(self, script_id: str, script_path: str,
|
|||
|
context: ScriptExecutionContext) -> Dict[str, Any]:
|
|||
|
"""加载并执行脚本"""
|
|||
|
try:
|
|||
|
# 读取脚本内容
|
|||
|
with open(script_path, 'r', encoding='utf-8') as f:
|
|||
|
script_code = f.read()
|
|||
|
|
|||
|
# 准备脚本全局环境
|
|||
|
from services.script_vwed_objects import create_vwed_object
|
|||
|
|
|||
|
script_globals = {
|
|||
|
'__name__': '__main__',
|
|||
|
'__file__': script_path,
|
|||
|
'VWED': create_vwed_object(script_id), # 注入VWED统一对象
|
|||
|
'context': context
|
|||
|
}
|
|||
|
|
|||
|
# 执行脚本
|
|||
|
exec(script_code, script_globals)
|
|||
|
|
|||
|
# 保存脚本全局环境以供后续使用
|
|||
|
context.globals = script_globals
|
|||
|
|
|||
|
return {"success": True, "message": "脚本加载成功"}
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
error_message = str(e)
|
|||
|
error_traceback = traceback.format_exc()
|
|||
|
|
|||
|
logger.error(f"加载脚本失败: {error_message}")
|
|||
|
await self._log_execution(script_id, "load_script", None, None, None,
|
|||
|
"failed", 0, error_message, error_traceback)
|
|||
|
|
|||
|
return {"success": False, "error": error_message, "traceback": error_traceback}
|
|||
|
|
|||
|
async def _execute_boot_function(self, script_id: str,
|
|||
|
context: ScriptExecutionContext) -> Dict[str, Any]:
|
|||
|
"""执行boot函数注册服务"""
|
|||
|
try:
|
|||
|
if 'boot' not in context.globals:
|
|||
|
return {"success": False, "error": "脚本缺少boot()入口函数"}
|
|||
|
|
|||
|
boot_function = context.globals['boot']
|
|||
|
start_time = time.time()
|
|||
|
|
|||
|
# 执行boot函数
|
|||
|
if callable(boot_function):
|
|||
|
boot_function()
|
|||
|
else:
|
|||
|
return {"success": False, "error": "boot不是一个可调用函数"}
|
|||
|
|
|||
|
execution_time_ms = int((time.time() - start_time) * 1000)
|
|||
|
|
|||
|
# 记录执行日志
|
|||
|
await self._log_execution(script_id, "boot_function", "boot", None, None,
|
|||
|
"success", execution_time_ms)
|
|||
|
|
|||
|
return {"success": True, "execution_time_ms": execution_time_ms}
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
error_message = str(e)
|
|||
|
error_traceback = traceback.format_exc()
|
|||
|
|
|||
|
logger.error(f"执行boot函数失败: {error_message}")
|
|||
|
await self._log_execution(script_id, "boot_function", "boot", None, None,
|
|||
|
"failed", 0, error_message, error_traceback)
|
|||
|
|
|||
|
return {"success": False, "error": error_message, "traceback": error_traceback}
|
|||
|
|
|||
|
async def _cleanup_failed_script(self, script_id: str):
|
|||
|
"""清理失败的脚本"""
|
|||
|
try:
|
|||
|
# 清理注册项
|
|||
|
self.registry.clear_script_registrations(script_id)
|
|||
|
|
|||
|
# 清理运行记录
|
|||
|
if script_id in self.running_scripts:
|
|||
|
del self.running_scripts[script_id]
|
|||
|
|
|||
|
# 清理上下文
|
|||
|
if script_id in self.script_contexts:
|
|||
|
del self.script_contexts[script_id]
|
|||
|
|
|||
|
# 更新数据库状态
|
|||
|
await self._update_script_status(script_id, "error")
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"清理失败脚本出错: {e}")
|
|||
|
|
|||
|
async def _update_script_status(self, script_id: str, status: str, error_message: str = None):
|
|||
|
"""更新脚本状态"""
|
|||
|
try:
|
|||
|
async with get_async_session() as session:
|
|||
|
registry_record = await session.get(VWEDScriptRegistry, script_id)
|
|||
|
if registry_record:
|
|||
|
registry_record.status = status
|
|||
|
if error_message:
|
|||
|
registry_record.error_message = error_message
|
|||
|
if status in ['stopped', 'error']:
|
|||
|
registry_record.stopped_at = datetime.now()
|
|||
|
|
|||
|
# 更新注册统计
|
|||
|
registrations = self.registry.get_script_registrations(script_id)
|
|||
|
registry_record.update_registration_counts(**registrations)
|
|||
|
|
|||
|
await session.commit()
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"更新脚本状态失败: {e}")
|
|||
|
|
|||
|
async def _log_execution(self, script_id: str, execution_type: str, function_name: str,
|
|||
|
input_params: Any, output_result: Any, status: str,
|
|||
|
duration_ms: int, error_message: str = None,
|
|||
|
error_traceback: str = None):
|
|||
|
"""记录执行日志"""
|
|||
|
try:
|
|||
|
async with get_async_session() as session:
|
|||
|
log_entry = VWEDScriptExecutionLog(
|
|||
|
script_id=script_id,
|
|||
|
file_id=None, # 需要根据script_id查找file_id
|
|||
|
execution_type=execution_type,
|
|||
|
function_name=function_name,
|
|||
|
input_params=input_params,
|
|||
|
output_result=output_result,
|
|||
|
status=status,
|
|||
|
error_message=error_message,
|
|||
|
error_traceback=error_traceback,
|
|||
|
duration_ms=duration_ms
|
|||
|
)
|
|||
|
session.add(log_entry)
|
|||
|
await session.commit()
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"记录执行日志失败: {e}")
|
|||
|
|
|||
|
|
|||
|
# 全局脚本引擎实例
|
|||
|
_script_engine = MultiScriptEngine()
|
|||
|
|
|||
|
|
|||
|
def get_script_engine() -> MultiScriptEngine:
|
|||
|
"""获取脚本引擎实例"""
|
|||
|
return _script_engine
|