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