From df797bc90634bd25a93fb76329889a391da40843 Mon Sep 17 00:00:00 2001 From: dongchangxi <458593490@qq.com> Date: Fri, 5 Dec 2025 10:52:13 +0800 Subject: [PATCH] =?UTF-8?q?bug=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- script/factory_sliceing_v2/build_exe.spec | 7 +- script/factory_sliceing_v2/utils/funcs.py | 154 ++++++++++++++++++++-- 2 files changed, 151 insertions(+), 10 deletions(-) diff --git a/script/factory_sliceing_v2/build_exe.spec b/script/factory_sliceing_v2/build_exe.spec index 5c98a77..6a4d44b 100644 --- a/script/factory_sliceing_v2/build_exe.spec +++ b/script/factory_sliceing_v2/build_exe.spec @@ -6,7 +6,10 @@ a = Analysis( ['main.py'], pathex=[], binaries=[], - datas=[], + datas=[ + # 将 small_machine_transform.py 作为数据文件包含,以便 Blender 可以调用 + ('utils/small_machine_transform.py', 'utils'), + ], hiddenimports=[ 'redis', 'oss2', @@ -39,7 +42,7 @@ exe = EXE( [], name='factory_sliceing', debug=False, - bootloader_ignore_signals=False, + bootloader_ignore_signals=False, # 允许信号传播,使 Ctrl+C 可以中断 strip=False, upx=True, upx_exclude=[], diff --git a/script/factory_sliceing_v2/utils/funcs.py b/script/factory_sliceing_v2/utils/funcs.py index e276db6..bcae57f 100644 --- a/script/factory_sliceing_v2/utils/funcs.py +++ b/script/factory_sliceing_v2/utils/funcs.py @@ -6,6 +6,8 @@ import shutil import json import shlex import subprocess +import sys +import signal from concurrent.futures import ThreadPoolExecutor, as_completed from .oss_redis import ossClient from .logs import log @@ -21,6 +23,75 @@ elif ENV == 'prod': url = 'https://mp.api.suwa3d.com' +def get_resource_path(relative_path): + """ + 获取资源文件的绝对路径,兼容 PyInstaller 打包后的环境 + 参数: + relative_path: 相对于脚本目录的相对路径 + 返回: + 资源文件的绝对路径 + """ + try: + # PyInstaller 创建临时文件夹,并将路径存储在 _MEIPASS 中 + base_path = sys._MEIPASS + except AttributeError: + # 如果不是打包环境,使用脚本所在目录 + base_path = os.path.dirname(os.path.abspath(__file__)) + + return os.path.join(base_path, relative_path) + + +def get_transform_script_path(): + """ + 获取 small_machine_transform.py 脚本的路径 + 在打包成exe后,如果脚本不在可访问位置,则将其提取到临时目录 + 返回: + transform_script_path: 脚本的绝对路径 + """ + # 首先尝试从资源路径获取 + if getattr(sys, 'frozen', False): + # 打包后的环境 + # 尝试从资源路径获取 + resource_path = get_resource_path('utils/small_machine_transform.py') + + # 如果资源路径存在,直接使用 + if os.path.exists(resource_path): + return resource_path + + # 如果不存在,尝试提取到临时目录 + import tempfile + temp_dir = tempfile.gettempdir() + extract_path = os.path.join(temp_dir, 'factory_sliceing_utils', 'small_machine_transform.py') + + # 确保目录存在 + os.makedirs(os.path.dirname(extract_path), exist_ok=True) + + # 如果提取路径不存在,尝试从资源中复制 + if not os.path.exists(extract_path): + # 尝试从多个可能的资源路径查找 + possible_paths = [ + get_resource_path('small_machine_transform.py'), + get_resource_path('utils/small_machine_transform.py'), + ] + + for src_path in possible_paths: + if os.path.exists(src_path): + shutil.copy2(src_path, extract_path) + log(f"已将 small_machine_transform.py 提取到: {extract_path}") + return extract_path + + # 如果提取路径存在,直接使用 + if os.path.exists(extract_path): + return extract_path + + # 如果都找不到,返回资源路径(即使不存在,让调用者处理错误) + return resource_path + else: + # 开发环境,直接使用相对路径 + script_dir = os.path.dirname(os.path.abspath(__file__)) + return os.path.join(script_dir, 'small_machine_transform.py') + + # 根据打印ID 获取下载目录 def getDownloadDirByPrintId(printIds): # 调用接口获取下载的路径 @@ -317,9 +388,8 @@ def _process_single_item(v, arrDownloadPath, dirNewName, dirPath, isSmallMachine # 通过blender 调用执行 python 文件 blender_bin_path = findBpyModule() - # 获取 small_machine_transform.py 的绝对路径 - script_dir = os.path.dirname(os.path.abspath(__file__)) - transform_script_path = os.path.join(script_dir, 'small_machine_transform.py') + # 获取 small_machine_transform.py 的绝对路径(兼容打包环境) + transform_script_path = get_transform_script_path() # 将 homo_matrix 转换为 JSON 字符串 homo_matrix_json = json.dumps(homo_matrix) @@ -358,17 +428,85 @@ def _process_single_item(v, arrDownloadPath, dirNewName, dirPath, isSmallMachine try: log(f"开始执行 Blender 转换命令...") - # 使用 subprocess 执行命令,捕获输出和错误 + # 使用 subprocess.Popen 以便更好地控制进程和信号处理 # 在 Windows 上,Blender 输出可能是 UTF-8,需要指定编码并处理错误 - result = subprocess.run( + # 准备进程启动参数 + startupinfo = None + preexec_fn = None + if platform.system() == 'Windows': + # Windows 上,不设置 CREATE_NEW_PROCESS_GROUP,以便 Ctrl+C 可以传播 + import subprocess as sp + startupinfo = sp.STARTUPINFO() + startupinfo.dwFlags |= sp.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = sp.SW_HIDE + else: + # Unix 系统,设置 preexec_fn 以确保信号处理正确 + preexec_fn = os.setsid + + process = subprocess.Popen( cmd, - capture_output=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, text=True, encoding='utf-8', # 指定 UTF-8 编码 errors='replace', # 遇到无法解码的字符时替换为占位符,而不是抛出异常 - timeout=300, # 5分钟超时 - check=False # 不自动抛出异常,手动检查返回码 + startupinfo=startupinfo, + preexec_fn=preexec_fn ) + + try: + # 等待进程完成,捕获输出 + stdout, stderr = process.communicate(timeout=300) # 5分钟超时 + returncode = process.returncode + except subprocess.TimeoutExpired: + # 超时,终止进程 + log(f"Blender 命令执行超时,正在终止进程...") + if platform.system() == 'Windows': + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + else: + # Unix 系统,发送 SIGTERM 到进程组 + try: + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + process.wait(timeout=5) + except (subprocess.TimeoutExpired, ProcessLookupError, OSError): + try: + os.killpg(os.getpgid(process.pid), signal.SIGKILL) + except (ProcessLookupError, OSError): + pass + raise + except KeyboardInterrupt: + # 收到中断信号,终止进程 + log(f"收到中断信号,正在终止 Blender 进程...") + if platform.system() == 'Windows': + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + else: + # Unix 系统,发送 SIGTERM 到进程组 + try: + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + process.wait(timeout=5) + except (subprocess.TimeoutExpired, ProcessLookupError, OSError): + try: + os.killpg(os.getpgid(process.pid), signal.SIGKILL) + except (ProcessLookupError, OSError): + pass + raise # 重新抛出,让调用者处理 + + # 创建结果对象(模拟 subprocess.run 的返回值) + class Result: + def __init__(self, returncode, stdout, stderr): + self.returncode = returncode + self.stdout = stdout + self.stderr = stderr + + result = Result(returncode, stdout, stderr) log(f"Blender 命令执行完成, 返回码: {result.returncode}")