VWED_server/services/script_engine_service.py

414 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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