2025-09-12 16:15:13 +08:00
|
|
|
|
#!/usr/bin/env python
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
脚本引擎服务
|
|
|
|
|
核心脚本执行引擎,支持多脚本并发运行和生命周期管理
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import time
|
|
|
|
|
import uuid
|
2025-09-25 10:52:52 +08:00
|
|
|
|
import asyncio
|
2025-09-12 16:15:13 +08:00
|
|
|
|
import traceback
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from typing import Dict, Any, Optional, List
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
2025-09-25 10:52:52 +08:00
|
|
|
|
from data.session import get_async_session
|
2025-09-20 16:50:45 +08:00
|
|
|
|
from data.models.script_file import VWEDScriptFile
|
2025-09-12 16:15:13 +08:00
|
|
|
|
from data.models.script_registry import VWEDScriptRegistry
|
|
|
|
|
from data.models.script_execution_log import VWEDScriptExecutionLog
|
2025-09-20 16:50:45 +08:00
|
|
|
|
from .script_registry_service import get_global_registry
|
|
|
|
|
from .device_handler_service import get_device_service
|
2025-09-12 16:15:13 +08:00
|
|
|
|
from utils.logger import get_logger
|
2025-09-25 10:52:52 +08:00
|
|
|
|
from config.settings import settings
|
2025-09-12 16:15:13 +08:00
|
|
|
|
|
|
|
|
|
logger = get_logger("services.script_engine")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ScriptExecutionContext:
|
|
|
|
|
"""脚本执行上下文"""
|
|
|
|
|
|
2025-09-20 16:50:45 +08:00
|
|
|
|
def __init__(self, script_id: str, file_path: str, registry_service, file_id: int = None):
|
2025-09-12 16:15:13 +08:00
|
|
|
|
self.script_id = script_id
|
|
|
|
|
self.file_path = file_path
|
2025-09-20 16:50:45 +08:00
|
|
|
|
self.file_id = file_id
|
2025-09-12 16:15:13 +08:00
|
|
|
|
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] = {}
|
2025-09-25 10:52:52 +08:00
|
|
|
|
self.base_path = Path(settings.SCRIPT_SAVE_PATH)
|
|
|
|
|
self._initialized = False
|
2025-09-12 16:15:13 +08:00
|
|
|
|
|
2025-09-25 10:52:52 +08:00
|
|
|
|
async def _ensure_initialized(self):
|
|
|
|
|
"""确保引擎已初始化(延迟初始化)"""
|
|
|
|
|
if not self._initialized:
|
|
|
|
|
await self._initialize_cleanup_active_scripts()
|
|
|
|
|
self._initialized = True
|
|
|
|
|
|
2025-09-20 16:50:45 +08:00
|
|
|
|
async def start_script_service(self, script_path: str = None, script_id: int = None, start_params: Dict = None) -> Dict[str, Any]:
|
2025-09-12 16:15:13 +08:00
|
|
|
|
"""启动脚本服务 - 支持多脚本并发"""
|
2025-09-25 10:52:52 +08:00
|
|
|
|
import asyncio
|
|
|
|
|
|
|
|
|
|
# 确保引擎已初始化
|
|
|
|
|
await self._ensure_initialized()
|
|
|
|
|
|
2025-09-12 16:15:13 +08:00
|
|
|
|
try:
|
2025-09-20 16:50:45 +08:00
|
|
|
|
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
|
2025-09-25 10:52:52 +08:00
|
|
|
|
# 使用正确的文件路径拼接逻辑
|
|
|
|
|
full_script_path = Path(os.path.join(self.base_path, script_file.project.project_path, script_file.file_path))
|
2025-09-20 16:50:45 +08:00
|
|
|
|
elif script_path is not None:
|
|
|
|
|
# 传统方式,通过script_path查找
|
|
|
|
|
full_script_path = Path(script_path)
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
return {"success": False, "error": "必须提供script_id或script_path参数"}
|
|
|
|
|
# 2. 验证脚本文件存在
|
2025-09-12 16:15:13 +08:00
|
|
|
|
if not os.path.exists(full_script_path):
|
|
|
|
|
return {"success": False, "error": f"脚本文件不存在: {script_path}"}
|
|
|
|
|
|
2025-09-25 10:52:52 +08:00
|
|
|
|
# 3. 预检查脚本是否有boot函数
|
|
|
|
|
boot_check_result = await self._check_boot_function(full_script_path)
|
|
|
|
|
if not boot_check_result["success"]:
|
|
|
|
|
return boot_check_result
|
|
|
|
|
|
|
|
|
|
# 4. 确定脚本服务ID
|
|
|
|
|
if script_id is not None:
|
|
|
|
|
# 使用数据库中的script_id作为全局唯一标识
|
|
|
|
|
script_service_id = str(script_id)
|
|
|
|
|
else:
|
|
|
|
|
# 对于通过script_path启动的脚本,保持原有生成逻辑
|
|
|
|
|
script_service_id = f"{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}"
|
|
|
|
|
|
|
|
|
|
# 5. 检查脚本是否已在运行
|
|
|
|
|
if script_service_id in self.running_scripts:
|
|
|
|
|
# 拒绝启动已运行的脚本
|
|
|
|
|
running_info = self.running_scripts[script_service_id]
|
2025-09-12 16:15:13 +08:00
|
|
|
|
return {
|
|
|
|
|
"success": False,
|
2025-09-25 10:52:52 +08:00
|
|
|
|
"error": f"脚本 {script_service_id} 已在运行中",
|
|
|
|
|
"running_info": {
|
|
|
|
|
"script_id": script_service_id,
|
|
|
|
|
"script_path": running_info["script_path"],
|
|
|
|
|
"started_at": running_info["started_at"],
|
|
|
|
|
"status": running_info["status"]
|
|
|
|
|
},
|
|
|
|
|
"message": "请先停止正在运行的实例"
|
2025-09-12 16:15:13 +08:00
|
|
|
|
}
|
2025-09-25 10:52:52 +08:00
|
|
|
|
|
|
|
|
|
# 6. 清理可能存在的数据库旧记录和注册项(清理残留的非活跃记录)
|
|
|
|
|
await self._cleanup_script_resources(script_service_id)
|
2025-09-12 16:15:13 +08:00
|
|
|
|
|
2025-09-25 10:52:52 +08:00
|
|
|
|
# 7. 异步启动脚本(不阻塞当前请求)
|
|
|
|
|
asyncio.create_task(self._async_start_script(
|
|
|
|
|
script_service_id, script_path, full_script_path, script_file, start_params
|
|
|
|
|
))
|
2025-09-12 16:15:13 +08:00
|
|
|
|
|
2025-09-25 10:52:52 +08:00
|
|
|
|
# 立即返回启动状态,不等待完成
|
|
|
|
|
return {
|
|
|
|
|
"success": True,
|
|
|
|
|
"script_id": script_service_id,
|
|
|
|
|
"message": f"脚本服务启动中: {script_path}",
|
|
|
|
|
"status": "starting"
|
|
|
|
|
}
|
2025-09-12 16:15:13 +08:00
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"启动脚本服务失败: {e}", exc_info=True)
|
|
|
|
|
return {"success": False, "error": f"脚本服务启动失败: {str(e)}"}
|
|
|
|
|
|
|
|
|
|
async def stop_script_service(self, script_id: str) -> Dict[str, Any]:
|
|
|
|
|
"""停止特定脚本服务 - 关键功能"""
|
2025-09-25 10:52:52 +08:00
|
|
|
|
# 确保引擎已初始化
|
|
|
|
|
await self._ensure_initialized()
|
|
|
|
|
|
2025-09-12 16:15:13 +08:00
|
|
|
|
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)
|
|
|
|
|
|
2025-09-20 16:50:45 +08:00
|
|
|
|
# 清理设备处理器
|
|
|
|
|
device_service = get_device_service()
|
|
|
|
|
device_service.registry.clear_script_device_registrations(script_id)
|
|
|
|
|
|
2025-09-12 16:15:13 +08:00
|
|
|
|
# 从运行列表移除
|
|
|
|
|
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,
|
2025-09-20 16:50:45 +08:00
|
|
|
|
"message": f"脚本服务已停止 ({script_info['script_path']}),已清理 {registrations['apis']} 个接口、{registrations['functions']} 个函数、{registrations['events']} 个事件监听器、{registrations.get('devices', 0)} 个设备处理器",
|
2025-09-12 16:15:13 +08:00
|
|
|
|
"registrations": registrations
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"停止脚本服务失败: {e}", exc_info=True)
|
|
|
|
|
return {"success": False, "error": f"停止脚本服务失败: {str(e)}"}
|
|
|
|
|
|
2025-09-25 10:52:52 +08:00
|
|
|
|
async def get_running_scripts(self) -> List[Dict[str, Any]]:
|
2025-09-12 16:15:13 +08:00
|
|
|
|
"""获取所有运行中的脚本"""
|
2025-09-25 10:52:52 +08:00
|
|
|
|
# 确保引擎已初始化
|
|
|
|
|
await self._ensure_initialized()
|
|
|
|
|
|
2025-09-12 16:15:13 +08:00
|
|
|
|
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
|
|
|
|
|
|
2025-09-25 10:52:52 +08:00
|
|
|
|
async def get_script_history(self, script_id: str) -> List[Dict[str, Any]]:
|
|
|
|
|
"""获取脚本的所有历史记录"""
|
|
|
|
|
try:
|
|
|
|
|
async with get_async_session() as session:
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
|
|
|
|
|
result = await session.execute(
|
|
|
|
|
select(VWEDScriptRegistry)
|
|
|
|
|
.where(VWEDScriptRegistry.script_id == script_id)
|
|
|
|
|
.order_by(VWEDScriptRegistry.created_at.desc())
|
|
|
|
|
)
|
|
|
|
|
records = result.scalars().all()
|
|
|
|
|
|
|
|
|
|
return [record.to_dict() for record in records]
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"获取脚本历史记录失败: {e}")
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
async def get_active_script_record(self, script_id: str) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""获取脚本的活跃记录"""
|
|
|
|
|
try:
|
|
|
|
|
async with get_async_session() as session:
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
|
|
|
|
|
result = await session.execute(
|
|
|
|
|
select(VWEDScriptRegistry)
|
|
|
|
|
.where(VWEDScriptRegistry.script_id == script_id)
|
|
|
|
|
.where(VWEDScriptRegistry.is_active == True)
|
|
|
|
|
)
|
|
|
|
|
record = result.scalar_one_or_none()
|
|
|
|
|
|
|
|
|
|
return record.to_dict() if record else None
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"获取活跃脚本记录失败: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
2025-09-12 16:15:13 +08:00
|
|
|
|
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
|
|
|
|
|
|
2025-09-25 10:52:52 +08:00
|
|
|
|
def find_running_script_by_id(self, script_id: str) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""根据脚本ID查找运行中的脚本"""
|
|
|
|
|
if script_id in self.running_scripts:
|
|
|
|
|
return {"script_id": script_id, **self.running_scripts[script_id]}
|
|
|
|
|
return None
|
|
|
|
|
|
2025-09-12 16:15:13 +08:00
|
|
|
|
async def execute_script_function(self, script_id: str, function_name: str,
|
|
|
|
|
function_args: Any) -> Dict[str, Any]:
|
|
|
|
|
"""执行脚本中的指定函数"""
|
2025-09-25 10:52:52 +08:00
|
|
|
|
from .script_output_capture import capture_script_output, push_script_error
|
|
|
|
|
|
2025-09-12 16:15:13 +08:00
|
|
|
|
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}"}
|
|
|
|
|
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
2025-09-25 10:52:52 +08:00
|
|
|
|
# 使用输出捕获上下文管理器
|
|
|
|
|
with capture_script_output(script_id):
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
# 推送错误信息到WebSocket
|
|
|
|
|
await push_script_error(script_id, f"函数 {function_name} 执行失败: {error_message}")
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"success": False,
|
|
|
|
|
"error": error_message,
|
|
|
|
|
"traceback": error_traceback,
|
|
|
|
|
"execution_time_ms": execution_time_ms
|
|
|
|
|
}
|
2025-09-12 16:15:13 +08:00
|
|
|
|
|
|
|
|
|
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]:
|
|
|
|
|
"""加载并执行脚本"""
|
2025-09-25 10:52:52 +08:00
|
|
|
|
from .script_output_capture import capture_script_output, push_script_error
|
|
|
|
|
|
2025-09-12 16:15:13 +08:00
|
|
|
|
try:
|
|
|
|
|
# 读取脚本内容
|
|
|
|
|
with open(script_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
script_code = f.read()
|
2025-09-25 10:52:52 +08:00
|
|
|
|
|
2025-09-12 16:15:13 +08:00
|
|
|
|
# 准备脚本全局环境
|
2025-09-20 16:50:45 +08:00
|
|
|
|
from .script_vwed_objects import create_vwed_object
|
2025-09-12 16:15:13 +08:00
|
|
|
|
|
|
|
|
|
script_globals = {
|
|
|
|
|
'__name__': '__main__',
|
|
|
|
|
'__file__': script_path,
|
|
|
|
|
'VWED': create_vwed_object(script_id), # 注入VWED统一对象
|
|
|
|
|
'context': context
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 10:52:52 +08:00
|
|
|
|
# 使用输出捕获上下文管理器
|
|
|
|
|
with capture_script_output(script_id):
|
|
|
|
|
# 执行脚本
|
|
|
|
|
exec(script_code, script_globals)
|
|
|
|
|
|
|
|
|
|
# 保存脚本全局环境以供后续使用
|
|
|
|
|
context.globals = script_globals
|
|
|
|
|
|
|
|
|
|
return {"success": True, "message": "脚本加载成功"}
|
2025-09-12 16:15:13 +08:00
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2025-09-25 10:52:52 +08:00
|
|
|
|
# 将错误信息也推送到WebSocket
|
|
|
|
|
await push_script_error(script_id, f"脚本加载失败: {error_message}")
|
|
|
|
|
|
2025-09-12 16:15:13 +08:00
|
|
|
|
return {"success": False, "error": error_message, "traceback": error_traceback}
|
|
|
|
|
|
|
|
|
|
async def _execute_boot_function(self, script_id: str,
|
|
|
|
|
context: ScriptExecutionContext) -> Dict[str, Any]:
|
|
|
|
|
"""执行boot函数注册服务"""
|
2025-09-25 10:52:52 +08:00
|
|
|
|
import asyncio
|
|
|
|
|
import inspect
|
|
|
|
|
from .script_output_capture import capture_script_output, push_script_error
|
|
|
|
|
|
2025-09-12 16:15:13 +08:00
|
|
|
|
try:
|
|
|
|
|
if 'boot' not in context.globals:
|
|
|
|
|
return {"success": False, "error": "脚本缺少boot()入口函数"}
|
|
|
|
|
|
|
|
|
|
boot_function = context.globals['boot']
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
2025-09-25 10:52:52 +08:00
|
|
|
|
# 使用输出捕获上下文管理器
|
|
|
|
|
with capture_script_output(script_id):
|
|
|
|
|
# 执行boot函数
|
|
|
|
|
if callable(boot_function):
|
|
|
|
|
# 检查boot函数是否是异步函数
|
|
|
|
|
if inspect.iscoroutinefunction(boot_function):
|
|
|
|
|
# 如果是异步函数,直接await
|
|
|
|
|
await boot_function()
|
|
|
|
|
else:
|
|
|
|
|
# 如果是同步函数,在线程池中执行,避免阻塞事件循环
|
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
|
await loop.run_in_executor(None, boot_function)
|
|
|
|
|
else:
|
|
|
|
|
return {"success": False, "error": "boot不是一个可调用函数"}
|
2025-09-12 16:15:13 +08:00
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2025-09-25 10:52:52 +08:00
|
|
|
|
# 推送错误信息到WebSocket
|
|
|
|
|
await push_script_error(script_id, f"boot函数执行失败: {error_message}")
|
|
|
|
|
|
2025-09-12 16:15:13 +08:00
|
|
|
|
return {"success": False, "error": error_message, "traceback": error_traceback}
|
|
|
|
|
|
2025-09-25 10:52:52 +08:00
|
|
|
|
async def _initialize_cleanup_active_scripts(self):
|
|
|
|
|
"""初始化时清理所有活跃状态的脚本记录"""
|
|
|
|
|
try:
|
|
|
|
|
logger.info("开始初始化清理所有活跃状态的脚本记录")
|
|
|
|
|
async with get_async_session() as session:
|
|
|
|
|
from sqlalchemy import update
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# 标记所有活跃的函数注册记录为非活跃
|
|
|
|
|
from data.models.script_function_registration import VWEDScriptFunctionRegistration
|
|
|
|
|
await session.execute(
|
|
|
|
|
update(VWEDScriptFunctionRegistration)
|
|
|
|
|
.where(VWEDScriptFunctionRegistration.is_active == True)
|
|
|
|
|
.values(is_active=False)
|
|
|
|
|
)
|
|
|
|
|
except ImportError:
|
|
|
|
|
logger.warning("VWEDScriptFunctionRegistration 模型未找到")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# 标记所有活跃的API注册记录为非活跃
|
|
|
|
|
from data.models.script_api_registration import VWEDScriptAPIRegistration
|
|
|
|
|
await session.execute(
|
|
|
|
|
update(VWEDScriptAPIRegistration)
|
|
|
|
|
.where(VWEDScriptAPIRegistration.is_active == True)
|
|
|
|
|
.values(is_active=False)
|
|
|
|
|
)
|
|
|
|
|
except ImportError:
|
|
|
|
|
logger.warning("VWEDScriptAPIRegistration 模型未找到")
|
|
|
|
|
|
|
|
|
|
# 标记所有活跃的脚本注册中心记录为非活跃状态
|
|
|
|
|
result = await session.execute(
|
|
|
|
|
update(VWEDScriptRegistry)
|
|
|
|
|
.where(VWEDScriptRegistry.is_active == True)
|
|
|
|
|
.values(
|
|
|
|
|
is_active=False,
|
|
|
|
|
status='stopped',
|
|
|
|
|
stopped_at=datetime.now()
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await session.commit()
|
|
|
|
|
logger.info(f"初始化清理完成,共标记 {result.rowcount} 个活跃脚本记录为非活跃状态")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"初始化清理活跃脚本记录失败: {e}", exc_info=True)
|
|
|
|
|
|
|
|
|
|
async def _cleanup_script_resources(self, script_id: str):
|
|
|
|
|
"""清理脚本相关资源并标记历史记录为非活跃状态"""
|
|
|
|
|
try:
|
|
|
|
|
# 1. 清理内存中的注册项
|
|
|
|
|
self.registry.clear_script_registrations(script_id)
|
|
|
|
|
|
|
|
|
|
# 2. 清理设备处理器
|
|
|
|
|
device_service = get_device_service()
|
|
|
|
|
device_service.registry.clear_script_device_registrations(script_id)
|
|
|
|
|
|
|
|
|
|
# 3. 清理运行记录和上下文
|
|
|
|
|
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]
|
|
|
|
|
|
|
|
|
|
# 4. 标记历史记录为非活跃状态(保留历史记录,不删除)
|
|
|
|
|
async with get_async_session() as session:
|
|
|
|
|
from sqlalchemy import update
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from data.models.script_function_registration import VWEDScriptFunctionRegistration
|
|
|
|
|
# 标记函数注册记录为非活跃
|
|
|
|
|
await session.execute(
|
|
|
|
|
update(VWEDScriptFunctionRegistration)
|
|
|
|
|
.where(VWEDScriptFunctionRegistration.script_id == script_id)
|
|
|
|
|
.values(is_active=False)
|
|
|
|
|
)
|
|
|
|
|
except ImportError:
|
|
|
|
|
logger.warning("VWEDScriptFunctionRegistration 模型未找到")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from data.models.script_api_registration import VWEDScriptAPIRegistration
|
|
|
|
|
# 标记API注册记录为非活跃
|
|
|
|
|
await session.execute(
|
|
|
|
|
update(VWEDScriptAPIRegistration)
|
|
|
|
|
.where(VWEDScriptAPIRegistration.script_id == script_id)
|
|
|
|
|
.values(is_active=False)
|
|
|
|
|
)
|
|
|
|
|
except ImportError:
|
|
|
|
|
logger.warning("VWEDScriptAPIRegistration 模型未找到")
|
|
|
|
|
|
|
|
|
|
# 标记脚本注册中心记录为非活跃状态
|
|
|
|
|
await session.execute(
|
|
|
|
|
update(VWEDScriptRegistry)
|
|
|
|
|
.where(VWEDScriptRegistry.script_id == script_id)
|
|
|
|
|
.where(VWEDScriptRegistry.is_active == True)
|
|
|
|
|
.values(
|
|
|
|
|
is_active=False,
|
|
|
|
|
status='stopped',
|
|
|
|
|
stopped_at=datetime.now()
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await session.commit()
|
|
|
|
|
|
|
|
|
|
logger.info(f"已将脚本 {script_id} 的历史记录标记为非活跃状态")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"清理脚本资源失败: {script_id}, 错误: {e}", exc_info=True)
|
|
|
|
|
|
2025-09-12 16:15:13 +08:00
|
|
|
|
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:
|
2025-09-25 10:52:52 +08:00
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
|
|
|
|
|
# 查找活跃的脚本记录
|
|
|
|
|
result = await session.execute(
|
|
|
|
|
select(VWEDScriptRegistry)
|
|
|
|
|
.where(VWEDScriptRegistry.script_id == script_id)
|
|
|
|
|
.where(VWEDScriptRegistry.is_active == True)
|
|
|
|
|
)
|
|
|
|
|
registry_record = result.scalar_one_or_none()
|
|
|
|
|
|
2025-09-12 16:15:13 +08:00
|
|
|
|
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()
|
2025-09-25 10:52:52 +08:00
|
|
|
|
registry_record.is_active = False # 停止时标记为非活跃
|
2025-09-12 16:15:13 +08:00
|
|
|
|
|
|
|
|
|
# 更新注册统计
|
|
|
|
|
registrations = self.registry.get_script_registrations(script_id)
|
2025-09-25 10:52:52 +08:00
|
|
|
|
# 传递所有支持的参数
|
|
|
|
|
supported_params = {
|
|
|
|
|
'apis': registrations.get('apis', 0),
|
|
|
|
|
'functions': registrations.get('functions', 0),
|
|
|
|
|
'events': registrations.get('events', 0),
|
|
|
|
|
'timers': registrations.get('timers', 0),
|
|
|
|
|
'devices': registrations.get('devices', 0)
|
|
|
|
|
}
|
|
|
|
|
registry_record.update_registration_counts(**supported_params)
|
2025-09-12 16:15:13 +08:00
|
|
|
|
|
|
|
|
|
await session.commit()
|
2025-09-25 10:52:52 +08:00
|
|
|
|
else:
|
|
|
|
|
logger.warning(f"未找到活跃的脚本记录: {script_id}")
|
2025-09-12 16:15:13 +08:00
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"更新脚本状态失败: {e}")
|
2025-09-20 16:50:45 +08:00
|
|
|
|
|
2025-09-25 10:52:52 +08:00
|
|
|
|
async def _check_boot_function(self, script_path: str) -> Dict[str, Any]:
|
|
|
|
|
"""检查脚本是否包含boot函数且没有其他入口点"""
|
|
|
|
|
try:
|
|
|
|
|
with open(script_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
script_content = f.read()
|
|
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
# 1. 检查是否有boot函数定义(支持同步和异步函数)
|
|
|
|
|
boot_pattern = r'^\s*(async\s+)?def\s+boot\s*\('
|
|
|
|
|
has_boot = bool(re.search(boot_pattern, script_content, re.MULTILINE))
|
|
|
|
|
|
|
|
|
|
if not has_boot:
|
|
|
|
|
return {
|
|
|
|
|
"success": False,
|
|
|
|
|
"error": "脚本缺少boot()入口函数。请在脚本中定义boot()函数作为服务启动入口。"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 2. 检查是否存在其他入口点(如 if __name__ == "__main__")
|
|
|
|
|
main_check_patterns = [
|
|
|
|
|
r'if\s+__name__\s*==\s*["\']__main__["\']\s*:', # if __name__ == "__main__":
|
|
|
|
|
r'if\s+__name__\s*==\s*["\']__main__["\']\s*\:', # 变体
|
|
|
|
|
r'if\s+__name__\s*==\s*"__main__"\s*:', # 双引号
|
|
|
|
|
r'if\s+__name__\s*==\s*\'__main__\'\s*:', # 单引号
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for pattern in main_check_patterns:
|
|
|
|
|
if re.search(pattern, script_content, re.IGNORECASE):
|
|
|
|
|
return {
|
|
|
|
|
"success": False,
|
|
|
|
|
"error": "脚本中不能包含 'if __name__ == \"__main__\":' 等其他入口点。请仅使用boot()函数作为唯一入口点。"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 3. 检查潜在的阻塞调用
|
|
|
|
|
blocking_check_result = self._check_for_blocking_calls(script_content)
|
|
|
|
|
if not blocking_check_result["success"]:
|
|
|
|
|
return blocking_check_result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {"success": True}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return {"success": False, "error": f"检查boot函数失败: {str(e)}"}
|
|
|
|
|
|
|
|
|
|
def _check_for_blocking_calls(self, script_content: str) -> Dict[str, Any]:
|
|
|
|
|
"""检查脚本中潜在的阻塞调用"""
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
# 定义潜在的阻塞函数调用模式
|
|
|
|
|
blocking_patterns = [
|
|
|
|
|
(r'time\.sleep\s*\(', 'time.sleep()', '建议在异步函数中使用 await asyncio.sleep() 代替'),
|
|
|
|
|
(r'input\s*\(', 'input()', '不建议在脚本中使用阻塞式输入'),
|
|
|
|
|
(r'\.join\s*\(\s*\)', '.join()', '线程join调用可能导致阻塞,建议使用异步方式'),
|
|
|
|
|
(r'requests\.(get|post|put|delete|patch)', 'requests同步调用', '建议使用 aiohttp 或其他异步HTTP库'),
|
|
|
|
|
(r'\.wait\s*\(\s*\)', '.wait()', 'wait调用可能导致阻塞'),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
warnings = []
|
|
|
|
|
for pattern, description, suggestion in blocking_patterns:
|
|
|
|
|
if re.search(pattern, script_content, re.IGNORECASE):
|
|
|
|
|
warnings.append(f"检测到潜在阻塞调用: {description} - {suggestion}")
|
|
|
|
|
|
|
|
|
|
# 检查是否在异步函数中使用了time.sleep
|
|
|
|
|
async_boot_with_time_sleep = re.search(
|
|
|
|
|
r'async\s+def\s+boot\s*\([^)]*\):[^}]*time\.sleep\s*\(',
|
|
|
|
|
script_content,
|
|
|
|
|
re.DOTALL | re.MULTILINE
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if async_boot_with_time_sleep:
|
|
|
|
|
return {
|
|
|
|
|
"success": False,
|
|
|
|
|
"error": "在异步boot()函数中检测到 time.sleep() 调用,这会阻塞事件循环。请使用 'await asyncio.sleep()' 代替。"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 如果有警告但不是致命错误,记录警告但允许继续
|
|
|
|
|
if warnings:
|
|
|
|
|
return {
|
|
|
|
|
"success": False,
|
|
|
|
|
"error": "在boot()函数中检测到 time.sleep() 调用,这会阻塞事件循环。请使用 'await asyncio.sleep()' 代替。"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {"success": True, "warnings": warnings}
|
|
|
|
|
|
|
|
|
|
async def _async_start_script(self, script_service_id: str, script_path: str,
|
|
|
|
|
full_script_path: Path, script_file, start_params: Dict = None):
|
|
|
|
|
"""异步启动脚本的实际逻辑"""
|
|
|
|
|
try:
|
|
|
|
|
# 创建执行上下文
|
|
|
|
|
context = ScriptExecutionContext(script_service_id, script_path, self.registry,
|
|
|
|
|
script_file.id if script_file else None)
|
|
|
|
|
self.script_contexts[script_service_id] = context
|
|
|
|
|
|
|
|
|
|
# 设置全局注册中心上下文
|
|
|
|
|
self.registry.set_current_script(script_service_id)
|
|
|
|
|
self.registry.add_active_script(script_service_id, script_path)
|
|
|
|
|
|
|
|
|
|
# 创建数据库记录(新记录默认为活跃状态)
|
|
|
|
|
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',
|
|
|
|
|
is_active=True, # 新记录为活跃状态
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
# 加载并执行脚本
|
|
|
|
|
result = await self._load_and_execute_script(script_service_id, full_script_path, context)
|
|
|
|
|
|
|
|
|
|
if result["success"]:
|
|
|
|
|
# 调用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)
|
|
|
|
|
logger.error(f"脚本 {script_service_id} boot函数执行失败: {boot_result['error']}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 更新运行状态
|
|
|
|
|
self.running_scripts[script_service_id] = {
|
|
|
|
|
"script_path": script_path,
|
|
|
|
|
"started_at": context.start_time.isoformat(),
|
|
|
|
|
"status": "running",
|
|
|
|
|
"context": context
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 更新数据库状态
|
|
|
|
|
await self._update_script_status(script_service_id, "running")
|
|
|
|
|
|
|
|
|
|
# 获取注册统计
|
|
|
|
|
registrations = self.registry.get_script_registrations(script_service_id)
|
|
|
|
|
|
|
|
|
|
logger.info(f"脚本服务启动成功: {script_service_id}, 已注册 {registrations['apis']} 个接口,{registrations['functions']} 个函数")
|
|
|
|
|
else:
|
|
|
|
|
await self._cleanup_failed_script(script_service_id)
|
|
|
|
|
logger.error(f"脚本 {script_service_id} 加载失败: {result['error']}")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"异步启动脚本失败: {e}", exc_info=True)
|
|
|
|
|
await self._cleanup_failed_script(script_service_id)
|
|
|
|
|
|
2025-09-12 16:15:13 +08:00
|
|
|
|
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:
|
2025-09-20 16:50:45 +08:00
|
|
|
|
# 从执行上下文获取file_id
|
|
|
|
|
context = self.script_contexts.get(script_id)
|
|
|
|
|
file_id = context.file_id if context else None
|
2025-09-12 16:15:13 +08:00
|
|
|
|
async with get_async_session() as session:
|
|
|
|
|
log_entry = VWEDScriptExecutionLog(
|
|
|
|
|
script_id=script_id,
|
2025-09-20 16:50:45 +08:00
|
|
|
|
file_id=file_id,
|
2025-09-12 16:15:13 +08:00
|
|
|
|
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
|