VWED_server/services/online_script/script_engine_service.py

447 lines
19 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 .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