447 lines
19 KiB
Python
447 lines
19 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 .script_registry_service import get_global_registry
|
||
from .device_handler_service import get_device_service
|
||
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, file_id: int = None):
|
||
self.script_id = script_id
|
||
self.file_path = file_path
|
||
self.file_id = file_id
|
||
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 = None, script_id: int = None, start_params: Dict = None) -> Dict[str, Any]:
|
||
"""启动脚本服务 - 支持多脚本并发"""
|
||
try:
|
||
script_file = None
|
||
|
||
# 1. 根据参数类型获取脚本信息
|
||
if script_id is not None:
|
||
# 通过script_id查找脚本文件
|
||
async with get_async_session() as session:
|
||
from sqlalchemy import select
|
||
from sqlalchemy.orm import selectinload
|
||
result = await session.execute(
|
||
select(VWEDScriptFile)
|
||
.options(selectinload(VWEDScriptFile.project))
|
||
.where(VWEDScriptFile.id == script_id)
|
||
)
|
||
script_file = result.scalar_one_or_none()
|
||
|
||
if not script_file:
|
||
return {"success": False, "error": f"脚本文件不存在: script_id={script_id}"}
|
||
|
||
script_path = script_file.file_path
|
||
full_script_path = Path(script_path)
|
||
|
||
elif script_path is not None:
|
||
# 传统方式,通过script_path查找
|
||
full_script_path = Path(script_path)
|
||
|
||
else:
|
||
return {"success": False, "error": "必须提供script_id或script_path参数"}
|
||
# 2. 验证脚本文件存在
|
||
# print("full_script_path::::::::::::::::::", full_script_path, "============")
|
||
if not os.path.exists(full_script_path):
|
||
return {"success": False, "error": f"脚本文件不存在: {script_path}"}
|
||
|
||
# 3. 检查是否已有同名脚本在运行
|
||
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']}"
|
||
}
|
||
|
||
# 4. 生成唯一脚本ID
|
||
script_service_id = f"{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}"
|
||
|
||
# 5. 创建执行上下文
|
||
context = ScriptExecutionContext(script_service_id, script_path, self.registry,
|
||
script_file.id if script_file else None)
|
||
self.script_contexts[script_service_id] = context
|
||
|
||
# 6. 设置全局注册中心上下文
|
||
self.registry.set_current_script(script_service_id)
|
||
self.registry.add_active_script(script_service_id, script_path)
|
||
|
||
# 7. 创建数据库记录
|
||
async with get_async_session() as session:
|
||
# 创建注册记录
|
||
registry_record = VWEDScriptRegistry(
|
||
script_id=script_service_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.environ.get('COMPUTERNAME', os.environ.get('HOSTNAME', 'unknown')),
|
||
"platform": sys.platform,
|
||
"python_version": sys.version
|
||
}
|
||
)
|
||
session.add(registry_record)
|
||
await session.commit()
|
||
# 8. 加载并执行脚本
|
||
result = await self._load_and_execute_script(script_service_id, full_script_path, context)
|
||
|
||
if result["success"]:
|
||
# 8. 调用boot函数注册服务
|
||
|
||
boot_result = await self._execute_boot_function(script_service_id, context)
|
||
if not boot_result["success"]:
|
||
await self._cleanup_failed_script(script_service_id)
|
||
return boot_result
|
||
|
||
# 9. 更新运行状态
|
||
self.running_scripts[script_service_id] = {
|
||
"script_path": script_path,
|
||
"started_at": context.start_time.isoformat(),
|
||
"status": "running",
|
||
"context": context
|
||
}
|
||
|
||
# 10. 更新数据库状态
|
||
await self._update_script_status(script_service_id, "running")
|
||
|
||
# 11. 获取注册统计
|
||
registrations = self.registry.get_script_registrations(script_service_id)
|
||
|
||
logger.info(f"脚本服务启动成功: {script_service_id}")
|
||
return {
|
||
"success": True,
|
||
"script_id": script_service_id,
|
||
"message": f"脚本服务启动成功,已注册 {registrations['apis']} 个接口,{registrations['functions']} 个函数,{registrations['events']} 个事件监听器",
|
||
"registrations": registrations
|
||
}
|
||
else:
|
||
await self._cleanup_failed_script(script_service_id)
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"启动脚本服务失败: {e}", exc_info=True)
|
||
if 'script_service_id' in locals():
|
||
await self._cleanup_failed_script(script_service_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)
|
||
|
||
# 清理设备处理器
|
||
device_service = get_device_service()
|
||
device_service.registry.clear_script_device_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.get('devices', 0)} 个设备处理器",
|
||
"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 .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:
|
||
# 从执行上下文获取file_id
|
||
context = self.script_contexts.get(script_id)
|
||
file_id = context.file_id if context else None
|
||
async with get_async_session() as session:
|
||
log_entry = VWEDScriptExecutionLog(
|
||
script_id=script_id,
|
||
file_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 |