818 lines
30 KiB
Python
818 lines
30 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
在线脚本管理服务
|
||
提供脚本的创建、查询、更新、删除和执行功能
|
||
"""
|
||
|
||
import os
|
||
import uuid
|
||
import datetime
|
||
import asyncio
|
||
import tempfile
|
||
from concurrent.futures import ThreadPoolExecutor
|
||
from typing import Dict, List, Optional, Any, Tuple, Union
|
||
import importlib.util
|
||
from sqlalchemy import func, and_, or_, desc, asc
|
||
from sqlalchemy.orm import Session
|
||
|
||
from data.models.script import VWEDScript, VWEDScriptVersion, VWEDScriptLog
|
||
from config.settings import settings
|
||
from utils.logger import get_logger
|
||
|
||
# 设置日志记录器
|
||
logger = get_logger("app.script_service")
|
||
|
||
# 创建脚本执行线程池
|
||
executor = ThreadPoolExecutor(max_workers=settings.SCRIPT_MAX_WORKERS)
|
||
|
||
# 正在运行的脚本任务
|
||
running_scripts: Dict[str, asyncio.Task] = {}
|
||
|
||
|
||
class ScriptService:
|
||
"""在线脚本管理服务类"""
|
||
|
||
@staticmethod
|
||
def get_script_list(
|
||
db: Session,
|
||
page: int = 1,
|
||
page_size: int = 20,
|
||
name: Optional[str] = None,
|
||
status: Optional[int] = None,
|
||
folder_path: Optional[str] = None,
|
||
tags: Optional[str] = None,
|
||
is_public: Optional[int] = None,
|
||
created_by: Optional[str] = None,
|
||
start_time: Optional[str] = None,
|
||
end_time: Optional[str] = None,
|
||
sort_field: str = "updated_on",
|
||
sort_order: str = "desc"
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
获取脚本列表
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
page: 页码
|
||
page_size: 每页记录数
|
||
name: 脚本名称,支持模糊查询
|
||
status: 脚本状态(1:启用, 0:禁用)
|
||
folder_path: 脚本所在目录路径
|
||
tags: 标签,支持模糊查询
|
||
is_public: 是否公开(1:是, 0:否)
|
||
created_by: 创建者
|
||
start_time: 创建时间范围的开始时间,格式:yyyy-MM-dd HH:mm:ss
|
||
end_time: 创建时间范围的结束时间,格式:yyyy-MM-dd HH:mm:ss
|
||
sort_field: 排序字段
|
||
sort_order: 排序方式(asc/desc)
|
||
|
||
Returns:
|
||
Dict[str, Any]: 包含脚本列表和分页信息的字典
|
||
"""
|
||
try:
|
||
# 构建查询
|
||
query = db.query(VWEDScript)
|
||
|
||
# 添加过滤条件
|
||
if name:
|
||
query = query.filter(VWEDScript.name.like(f"%{name}%"))
|
||
if status is not None:
|
||
query = query.filter(VWEDScript.status == status)
|
||
if folder_path:
|
||
query = query.filter(VWEDScript.folder_path == folder_path)
|
||
if tags:
|
||
query = query.filter(VWEDScript.tags.like(f"%{tags}%"))
|
||
if is_public is not None:
|
||
query = query.filter(VWEDScript.is_public == is_public)
|
||
if created_by:
|
||
query = query.filter(VWEDScript.created_by == created_by)
|
||
if start_time and end_time:
|
||
query = query.filter(
|
||
VWEDScript.created_on.between(start_time, end_time)
|
||
)
|
||
|
||
# 计算总数
|
||
total = query.count()
|
||
|
||
# 添加排序
|
||
if sort_order.lower() == "asc":
|
||
query = query.order_by(asc(getattr(VWEDScript, sort_field)))
|
||
else:
|
||
query = query.order_by(desc(getattr(VWEDScript, sort_field)))
|
||
|
||
# 分页
|
||
scripts = query.offset((page - 1) * page_size).limit(page_size).all()
|
||
|
||
# 转换为字典列表
|
||
script_list = []
|
||
for script in scripts:
|
||
script_dict = {
|
||
"id": script.id,
|
||
"name": script.name,
|
||
"folderPath": script.folder_path,
|
||
"fileName": script.file_name,
|
||
"description": script.description,
|
||
"version": script.version,
|
||
"status": script.status,
|
||
"isPublic": bool(script.is_public),
|
||
"tags": script.tags,
|
||
"createdBy": script.created_by,
|
||
"createdOn": script.created_on.strftime("%Y-%m-%d %H:%M:%S") if script.created_on else None,
|
||
"updatedBy": script.updated_by,
|
||
"updatedOn": script.updated_on.strftime("%Y-%m-%d %H:%M:%S") if script.updated_on else None
|
||
}
|
||
script_list.append(script_dict)
|
||
|
||
return {
|
||
"total": total,
|
||
"list": script_list
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取脚本列表失败: {str(e)}")
|
||
raise
|
||
|
||
@staticmethod
|
||
def get_script_detail(db: Session, script_id: str) -> Dict[str, Any]:
|
||
"""
|
||
获取脚本详情
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
script_id: 脚本ID
|
||
|
||
Returns:
|
||
Dict[str, Any]: 脚本详情字典
|
||
"""
|
||
try:
|
||
script = db.query(VWEDScript).filter(VWEDScript.id == script_id).first()
|
||
if not script:
|
||
raise ValueError(f"脚本不存在: {script_id}")
|
||
|
||
script_dict = {
|
||
"id": script.id,
|
||
"name": script.name,
|
||
"folderPath": script.folder_path,
|
||
"fileName": script.file_name,
|
||
"description": script.description,
|
||
"code": script.code,
|
||
"version": script.version,
|
||
"status": script.status,
|
||
"isPublic": bool(script.is_public),
|
||
"tags": script.tags,
|
||
"createdBy": script.created_by,
|
||
"createdOn": script.created_on.strftime("%Y-%m-%d %H:%M:%S") if script.created_on else None,
|
||
"updatedBy": script.updated_by,
|
||
"updatedOn": script.updated_on.strftime("%Y-%m-%d %H:%M:%S") if script.updated_on else None,
|
||
"testParams": script.test_params
|
||
}
|
||
|
||
return script_dict
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取脚本详情失败: {str(e)}")
|
||
raise
|
||
|
||
@staticmethod
|
||
def create_script(
|
||
db: Session,
|
||
folder_path: str,
|
||
file_name: str,
|
||
code: str,
|
||
name: Optional[str] = None,
|
||
description: Optional[str] = None,
|
||
status: int = 1,
|
||
is_public: int = 1,
|
||
tags: Optional[str] = None,
|
||
test_params: Optional[str] = None,
|
||
created_by: Optional[str] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
创建脚本
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
folder_path: 脚本所在目录路径
|
||
file_name: 脚本文件名
|
||
code: 脚本代码内容
|
||
name: 脚本名称
|
||
description: 脚本功能描述
|
||
status: 状态(1:启用, 0:禁用)
|
||
is_public: 是否公开(1:是, 0:否)
|
||
tags: 标签,用于分类查询
|
||
test_params: 测试参数(JSON格式)
|
||
created_by: 创建者
|
||
|
||
Returns:
|
||
Dict[str, Any]: 创建的脚本信息
|
||
"""
|
||
try:
|
||
# 检查文件名是否以.py结尾
|
||
if not file_name.endswith(".py"):
|
||
raise ValueError("脚本文件名必须以.py结尾")
|
||
|
||
# 如果name为空,使用文件名(不含扩展名)作为name
|
||
if not name:
|
||
name = os.path.splitext(file_name)[0]
|
||
|
||
# 处理脚本保存路径
|
||
# 所有用户脚本都保存在scripts/user_save目录下
|
||
if not folder_path or folder_path == "/":
|
||
# 如果用户没有指定路径,使用默认根目录
|
||
folder_path = "/"
|
||
else:
|
||
# 确保路径以/开头
|
||
if not folder_path.startswith("/"):
|
||
folder_path = f"/{folder_path}"
|
||
# 确保路径以/结尾
|
||
if not folder_path.endswith("/"):
|
||
folder_path = f"{folder_path}/"
|
||
|
||
# 检查数据库中是否已存在相同路径和文件名的脚本
|
||
existing_script = db.query(VWEDScript).filter(
|
||
VWEDScript.folder_path == folder_path,
|
||
VWEDScript.file_name == file_name
|
||
).first()
|
||
|
||
if existing_script:
|
||
raise ValueError(f"脚本已存在: {folder_path}{file_name}")
|
||
|
||
# 检查物理文件是否已存在
|
||
base_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts", "user_save")
|
||
relative_path = folder_path.lstrip("/")
|
||
full_path = os.path.join(base_path, relative_path)
|
||
script_file_path = os.path.join(full_path, file_name)
|
||
|
||
if os.path.exists(script_file_path):
|
||
raise ValueError(f"脚本文件已存在: {folder_path}{file_name}")
|
||
|
||
# 生成唯一ID
|
||
script_id = str(uuid.uuid4())
|
||
|
||
# 创建脚本记录
|
||
script = VWEDScript(
|
||
id=script_id,
|
||
name=name,
|
||
folder_path=folder_path,
|
||
file_name=file_name,
|
||
description=description,
|
||
code=code,
|
||
version=1,
|
||
status=status,
|
||
is_public=is_public,
|
||
tags=tags,
|
||
created_by=created_by,
|
||
created_on=datetime.datetime.now(),
|
||
updated_by=created_by,
|
||
updated_on=datetime.datetime.now(),
|
||
test_params=test_params
|
||
)
|
||
|
||
db.add(script)
|
||
|
||
# 创建版本记录
|
||
version = VWEDScriptVersion(
|
||
id=str(uuid.uuid4()),
|
||
script_id=script_id,
|
||
version=1,
|
||
code=code,
|
||
change_log="初始版本",
|
||
created_by=created_by,
|
||
created_on=datetime.datetime.now()
|
||
)
|
||
|
||
db.add(version)
|
||
|
||
# 物理保存脚本文件
|
||
try:
|
||
# 确保目录存在
|
||
os.makedirs(full_path, exist_ok=True)
|
||
|
||
# 保存脚本文件
|
||
with open(script_file_path, "w", encoding="utf-8") as f:
|
||
f.write(code)
|
||
|
||
logger.info(f"脚本文件已保存到: {script_file_path}")
|
||
except Exception as e:
|
||
logger.error(f"保存脚本文件失败: {str(e)}")
|
||
# 继续执行,不影响数据库操作
|
||
|
||
db.commit()
|
||
|
||
return {"id": script_id}
|
||
|
||
except Exception as e:
|
||
db.rollback()
|
||
logger.error(f"创建脚本失败: {str(e)}")
|
||
raise
|
||
|
||
@staticmethod
|
||
def update_script(
|
||
db: Session,
|
||
script_id: str,
|
||
folder_path: Optional[str] = None,
|
||
file_name: Optional[str] = None,
|
||
code: Optional[str] = None,
|
||
name: Optional[str] = None,
|
||
description: Optional[str] = None,
|
||
status: Optional[int] = None,
|
||
is_public: Optional[int] = None,
|
||
tags: Optional[str] = None,
|
||
test_params: Optional[str] = None,
|
||
change_log: Optional[str] = None,
|
||
updated_by: Optional[str] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
更新脚本
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
script_id: 脚本ID
|
||
folder_path: 脚本所在目录路径
|
||
file_name: 脚本文件名
|
||
code: 脚本代码内容
|
||
name: 脚本名称
|
||
description: 脚本功能描述
|
||
status: 状态(1:启用, 0:禁用)
|
||
is_public: 是否公开(1:是, 0:否)
|
||
tags: 标签,用于分类查询
|
||
test_params: 测试参数(JSON格式)
|
||
change_log: 版本变更说明
|
||
updated_by: 更新者
|
||
|
||
Returns:
|
||
Dict[str, Any]: 更新后的脚本版本信息
|
||
"""
|
||
try:
|
||
# 查询脚本
|
||
script = db.query(VWEDScript).filter(VWEDScript.id == script_id).first()
|
||
if not script:
|
||
raise ValueError(f"脚本不存在: {script_id}")
|
||
|
||
# 检查文件名是否以.py结尾
|
||
if file_name and not file_name.endswith(".py"):
|
||
raise ValueError("脚本文件名必须以.py结尾")
|
||
|
||
# 记录更新前的信息,用于文件操作
|
||
old_folder_path = script.folder_path
|
||
old_file_name = script.file_name
|
||
|
||
# 处理脚本保存路径
|
||
if folder_path is not None:
|
||
if not folder_path or folder_path == "/":
|
||
folder_path = "/"
|
||
else:
|
||
# 确保路径以/开头
|
||
if not folder_path.startswith("/"):
|
||
folder_path = f"/{folder_path}"
|
||
# 确保路径以/结尾
|
||
if not folder_path.endswith("/"):
|
||
folder_path = f"{folder_path}/"
|
||
|
||
# 只有代码内容变更才会创建新版本
|
||
create_new_version = False
|
||
|
||
# 更新脚本属性
|
||
if folder_path is not None:
|
||
script.folder_path = folder_path
|
||
if file_name is not None:
|
||
script.file_name = file_name
|
||
if code is not None and code != script.code:
|
||
script.code = code
|
||
create_new_version = True
|
||
if name is not None:
|
||
script.name = name
|
||
if description is not None:
|
||
script.description = description
|
||
if status is not None:
|
||
script.status = status
|
||
if is_public is not None:
|
||
script.is_public = is_public
|
||
if tags is not None:
|
||
script.tags = tags
|
||
if test_params is not None:
|
||
script.test_params = test_params
|
||
|
||
script.updated_by = updated_by
|
||
script.updated_on = datetime.datetime.now()
|
||
|
||
# 如果代码内容变更,创建新版本
|
||
if create_new_version:
|
||
# 增加版本号
|
||
script.version += 1
|
||
new_version = script.version
|
||
|
||
# 创建版本记录
|
||
version = VWEDScriptVersion(
|
||
id=str(uuid.uuid4()),
|
||
script_id=script_id,
|
||
version=new_version,
|
||
code=code,
|
||
change_log=change_log or f"更新到版本 {new_version}",
|
||
created_by=updated_by,
|
||
created_on=datetime.datetime.now()
|
||
)
|
||
|
||
db.add(version)
|
||
else:
|
||
new_version = script.version
|
||
|
||
# 物理更新脚本文件
|
||
try:
|
||
base_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts", "user_save")
|
||
|
||
# 处理文件移动或重命名
|
||
path_or_name_changed = (folder_path is not None and folder_path != old_folder_path) or \
|
||
(file_name is not None and file_name != old_file_name)
|
||
|
||
# 旧文件路径
|
||
old_relative_path = old_folder_path.lstrip("/")
|
||
old_full_path = os.path.join(base_path, old_relative_path)
|
||
old_script_file_path = os.path.join(old_full_path, old_file_name)
|
||
|
||
# 新文件路径
|
||
new_folder_path = folder_path if folder_path is not None else old_folder_path
|
||
new_file_name = file_name if file_name is not None else old_file_name
|
||
new_relative_path = new_folder_path.lstrip("/")
|
||
new_full_path = os.path.join(base_path, new_relative_path)
|
||
new_script_file_path = os.path.join(new_full_path, new_file_name)
|
||
|
||
# 确保目录存在
|
||
os.makedirs(new_full_path, exist_ok=True)
|
||
|
||
if path_or_name_changed:
|
||
# 如果旧文件存在,则移动或重命名
|
||
if os.path.exists(old_script_file_path):
|
||
# 如果新文件已存在,先删除
|
||
if os.path.exists(new_script_file_path):
|
||
os.remove(new_script_file_path)
|
||
# 移动文件
|
||
os.rename(old_script_file_path, new_script_file_path)
|
||
logger.info(f"脚本文件已从 {old_script_file_path} 移动到 {new_script_file_path}")
|
||
|
||
# 如果代码内容变更,更新文件内容
|
||
if code is not None:
|
||
with open(new_script_file_path, "w", encoding="utf-8") as f:
|
||
f.write(code)
|
||
logger.info(f"脚本文件内容已更新: {new_script_file_path}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"更新脚本文件失败: {str(e)}")
|
||
# 继续执行,不影响数据库操作
|
||
|
||
db.commit()
|
||
|
||
return {"version": new_version}
|
||
|
||
except Exception as e:
|
||
db.rollback()
|
||
logger.error(f"更新脚本失败: {str(e)}")
|
||
raise
|
||
|
||
@staticmethod
|
||
def delete_script(db: Session, script_id: str) -> None:
|
||
"""
|
||
删除脚本
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
script_id: 脚本ID
|
||
"""
|
||
try:
|
||
# 查询脚本
|
||
script = db.query(VWEDScript).filter(VWEDScript.id == script_id).first()
|
||
if not script:
|
||
raise ValueError(f"脚本不存在: {script_id}")
|
||
|
||
# 记录脚本文件路径信息,用于后续删除物理文件
|
||
folder_path = script.folder_path
|
||
file_name = script.file_name
|
||
|
||
# 删除版本记录
|
||
db.query(VWEDScriptVersion).filter(VWEDScriptVersion.script_id == script_id).delete()
|
||
|
||
# 删除执行日志
|
||
db.query(VWEDScriptLog).filter(VWEDScriptLog.script_id == script_id).delete()
|
||
|
||
# 删除脚本记录
|
||
db.delete(script)
|
||
|
||
# 物理删除脚本文件
|
||
try:
|
||
# 构建完整的物理文件路径
|
||
base_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts", "user_save")
|
||
relative_path = folder_path.lstrip("/")
|
||
full_path = os.path.join(base_path, relative_path)
|
||
script_file_path = os.path.join(full_path, file_name)
|
||
|
||
# 如果文件存在,则删除
|
||
if os.path.exists(script_file_path):
|
||
os.remove(script_file_path)
|
||
logger.info(f"脚本文件已删除: {script_file_path}")
|
||
|
||
# 检查目录是否为空,如果为空则删除目录
|
||
if os.path.exists(full_path) and not os.listdir(full_path):
|
||
os.rmdir(full_path)
|
||
logger.info(f"空目录已删除: {full_path}")
|
||
except Exception as e:
|
||
logger.error(f"删除脚本文件失败: {str(e)}")
|
||
# 继续执行,不影响数据库操作
|
||
|
||
db.commit()
|
||
|
||
except Exception as e:
|
||
db.rollback()
|
||
logger.error(f"删除脚本失败: {str(e)}")
|
||
raise
|
||
|
||
@staticmethod
|
||
async def run_script(
|
||
db: Session,
|
||
script_id: str,
|
||
params: Optional[Dict[str, Any]] = None,
|
||
task_record_id: Optional[str] = None,
|
||
block_record_id: Optional[str] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
运行脚本
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
script_id: 脚本ID
|
||
params: 脚本执行参数
|
||
task_record_id: 关联的任务记录ID
|
||
block_record_id: 关联的任务块记录ID
|
||
|
||
Returns:
|
||
Dict[str, Any]: 脚本执行结果
|
||
"""
|
||
try:
|
||
# 查询脚本
|
||
script = db.query(VWEDScript).filter(VWEDScript.id == script_id).first()
|
||
if not script:
|
||
raise ValueError(f"脚本不存在: {script_id}")
|
||
|
||
# 检查脚本状态
|
||
if script.status != 1:
|
||
raise ValueError(f"脚本已禁用,无法执行: {script_id}")
|
||
|
||
# 创建日志记录
|
||
log_id = str(uuid.uuid4())
|
||
log = VWEDScriptLog(
|
||
id=log_id,
|
||
script_id=script_id,
|
||
version=script.version,
|
||
task_record_id=task_record_id,
|
||
block_record_id=block_record_id,
|
||
input_params=params,
|
||
started_on=datetime.datetime.now()
|
||
)
|
||
|
||
db.add(log)
|
||
db.commit()
|
||
|
||
# 在临时目录创建Python脚本文件
|
||
with tempfile.NamedTemporaryFile(suffix='.py', delete=False) as temp_file:
|
||
temp_path = temp_file.name
|
||
temp_file.write(script.code.encode('utf-8'))
|
||
|
||
# 执行脚本并获取结果
|
||
start_time = datetime.datetime.now()
|
||
|
||
# 创建异步任务执行脚本
|
||
loop = asyncio.get_event_loop()
|
||
task = loop.create_task(
|
||
ScriptService._execute_script(temp_path, params, log_id, db)
|
||
)
|
||
running_scripts[log_id] = task
|
||
|
||
# 等待执行完成或超时
|
||
try:
|
||
result = await asyncio.wait_for(task, timeout=settings.SCRIPT_TIMEOUT)
|
||
except asyncio.TimeoutError:
|
||
# 脚本执行超时,更新日志记录
|
||
log = db.query(VWEDScriptLog).filter(VWEDScriptLog.id == log_id).first()
|
||
if log:
|
||
log.status = 0
|
||
log.error_message = "脚本执行超时"
|
||
log.ended_on = datetime.datetime.now()
|
||
log.execution_time = int((datetime.datetime.now() - start_time).total_seconds() * 1000)
|
||
db.commit()
|
||
|
||
# 清理临时文件
|
||
try:
|
||
os.unlink(temp_path)
|
||
except:
|
||
pass
|
||
|
||
# 从运行中的脚本字典中移除
|
||
if log_id in running_scripts:
|
||
del running_scripts[log_id]
|
||
|
||
raise ValueError("脚本执行超时")
|
||
|
||
# 从运行中的脚本字典中移除
|
||
if log_id in running_scripts:
|
||
del running_scripts[log_id]
|
||
|
||
# 清理临时文件
|
||
try:
|
||
os.unlink(temp_path)
|
||
except:
|
||
pass
|
||
|
||
return {
|
||
"logId": log_id,
|
||
"result": result,
|
||
"executionTime": int((datetime.datetime.now() - start_time).total_seconds() * 1000)
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"运行脚本失败: {str(e)}")
|
||
raise
|
||
|
||
@staticmethod
|
||
async def _execute_script(
|
||
script_path: str,
|
||
params: Optional[Dict[str, Any]],
|
||
log_id: str,
|
||
db: Session
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
执行Python脚本文件
|
||
|
||
Args:
|
||
script_path: 脚本文件路径
|
||
params: 脚本参数
|
||
log_id: 日志ID
|
||
db: 数据库会话
|
||
|
||
Returns:
|
||
Dict[str, Any]: 脚本执行结果
|
||
"""
|
||
try:
|
||
# 在线程池中执行脚本
|
||
loop = asyncio.get_event_loop()
|
||
result = await loop.run_in_executor(
|
||
executor,
|
||
ScriptService._run_script_in_process,
|
||
script_path,
|
||
params
|
||
)
|
||
|
||
# 更新日志记录
|
||
log = db.query(VWEDScriptLog).filter(VWEDScriptLog.id == log_id).first()
|
||
if log:
|
||
log.status = 1
|
||
log.output_result = result
|
||
log.ended_on = datetime.datetime.now()
|
||
log.execution_time = int((log.ended_on - log.started_on).total_seconds() * 1000)
|
||
db.commit()
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
error_message = str(e)
|
||
logger.error(f"脚本执行失败: {error_message}")
|
||
|
||
# 更新日志记录
|
||
log = db.query(VWEDScriptLog).filter(VWEDScriptLog.id == log_id).first()
|
||
if log:
|
||
log.status = 0
|
||
log.error_message = error_message
|
||
log.ended_on = datetime.datetime.now()
|
||
log.execution_time = int((log.ended_on - log.started_on).total_seconds() * 1000)
|
||
db.commit()
|
||
|
||
raise
|
||
|
||
@staticmethod
|
||
def _run_script_in_process(script_path: str, params: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||
"""
|
||
在独立进程中执行Python脚本
|
||
|
||
Args:
|
||
script_path: 脚本文件路径
|
||
params: 脚本参数
|
||
|
||
Returns:
|
||
Dict[str, Any]: 脚本执行结果
|
||
"""
|
||
try:
|
||
# 加载模块
|
||
spec = importlib.util.spec_from_file_location("script_module", script_path)
|
||
if not spec or not spec.loader:
|
||
raise ValueError("无法加载脚本模块")
|
||
|
||
module = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(module)
|
||
|
||
# 检查是否存在main函数
|
||
if hasattr(module, "main"):
|
||
# 调用main函数执行脚本
|
||
if params:
|
||
result = module.main(**params)
|
||
else:
|
||
result = module.main()
|
||
|
||
return {"status": "success", "output": result}
|
||
else:
|
||
# 没有main函数,直接执行模块
|
||
return {"status": "success", "message": "脚本执行完成,但未找到main函数"}
|
||
|
||
except Exception as e:
|
||
logger.error(f"脚本内部执行失败: {str(e)}")
|
||
raise ValueError(f"脚本内部执行失败: {str(e)}")
|
||
|
||
@staticmethod
|
||
async def stop_script(db: Session, log_id: str) -> None:
|
||
"""
|
||
停止正在执行的脚本
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
log_id: 脚本执行日志ID
|
||
"""
|
||
try:
|
||
# 检查日志是否存在
|
||
log = db.query(VWEDScriptLog).filter(VWEDScriptLog.id == log_id).first()
|
||
if not log:
|
||
raise ValueError(f"脚本执行日志不存在: {log_id}")
|
||
|
||
# 检查脚本是否正在运行
|
||
if log_id not in running_scripts:
|
||
raise ValueError(f"脚本未在运行中: {log_id}")
|
||
|
||
# 取消任务
|
||
task = running_scripts[log_id]
|
||
task.cancel()
|
||
|
||
# 从运行中的脚本字典中移除
|
||
del running_scripts[log_id]
|
||
|
||
# 更新日志记录
|
||
log.status = 0
|
||
log.error_message = "脚本执行被手动终止"
|
||
log.ended_on = datetime.datetime.now()
|
||
if log.started_on:
|
||
log.execution_time = int((log.ended_on - log.started_on).total_seconds() * 1000)
|
||
db.commit()
|
||
|
||
except Exception as e:
|
||
db.rollback()
|
||
logger.error(f"停止脚本执行失败: {str(e)}")
|
||
raise
|
||
|
||
@staticmethod
|
||
def get_script_log(db: Session, log_id: str) -> Dict[str, Any]:
|
||
"""
|
||
获取脚本执行日志详情
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
log_id: 脚本执行日志ID
|
||
|
||
Returns:
|
||
Dict[str, Any]: 脚本执行日志详情
|
||
"""
|
||
try:
|
||
# 查询日志
|
||
log = db.query(VWEDScriptLog).join(
|
||
VWEDScript, VWEDScript.id == VWEDScriptLog.script_id
|
||
).filter(
|
||
VWEDScriptLog.id == log_id
|
||
).first()
|
||
|
||
if not log:
|
||
raise ValueError(f"脚本执行日志不存在: {log_id}")
|
||
|
||
# 查询脚本版本代码
|
||
version = db.query(VWEDScriptVersion).filter(
|
||
VWEDScriptVersion.script_id == log.script_id,
|
||
VWEDScriptVersion.version == log.version
|
||
).first()
|
||
|
||
log_dict = {
|
||
"id": log.id,
|
||
"scriptId": log.script_id,
|
||
"scriptName": log.script.name if log.script else None,
|
||
"version": log.version,
|
||
"taskRecordId": log.task_record_id,
|
||
"blockRecordId": log.block_record_id,
|
||
"inputParams": log.input_params,
|
||
"outputResult": log.output_result,
|
||
"status": log.status,
|
||
"errorMessage": log.error_message,
|
||
"executionTime": log.execution_time,
|
||
"startedOn": log.started_on.strftime("%Y-%m-%d %H:%M:%S") if log.started_on else None,
|
||
"endedOn": log.ended_on.strftime("%Y-%m-%d %H:%M:%S") if log.ended_on else None,
|
||
"code": version.code if version else None
|
||
}
|
||
|
||
return log_dict
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取脚本执行日志详情失败: {str(e)}")
|
||
raise |