From 0efaf2946b312b45acbe8d83330bc250a2a6f304 Mon Sep 17 00:00:00 2001 From: "feifei.xu" <307327147@qq.com> Date: Tue, 21 Jul 2026 16:26:08 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E5=8D=B8=E8=BD=BD=E5=90=8E=E9=87=8D=E5=90=AF=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 +- build.py | 7 +- core/plugin/plugin_registry.py | 288 +- core/plugin/plugins.py | 28 +- core/plugin/plugins_card.py | 14 +- main.py | 44 +- nuitka-crash-report.xml | 4572 ++++++++++++++++---------------- runtime_hook.py | 118 + workspace/mil.json | 8 + 9 files changed, 2704 insertions(+), 2380 deletions(-) create mode 100644 runtime_hook.py create mode 100644 workspace/mil.json diff --git a/.gitignore b/.gitignore index 4c4247c..d5da6f3 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,7 @@ htmlcov/ config.json *.qm *.ts -*.trae/ \ No newline at end of file +*.xml +*.trae/ +/plugins +/logs \ No newline at end of file diff --git a/build.py b/build.py index bdea196..75f82bf 100644 --- a/build.py +++ b/build.py @@ -18,7 +18,7 @@ BUILD_PATH = OUTPUT_PATH / f'{platform.system().lower()}-{platform.machine().low def build_main(): nuitka_cmd = [ - 'python', + sys.executable, '-m', 'nuitka', '--standalone', @@ -26,6 +26,11 @@ def build_main(): '--show-progress', '--plugin-enable=pyside6', '--include-module=qt_material', + # 把插件 SDK 的 runtime_hook 编进 exe,启动时把 plugins/*/ 加进 sys.path。 + # Nuitka 无 --runtime-hook 选项,用 --include-module + main.py 顶部 import 替代。 + # 宿主不需要预知插件依赖哪些 stdlib——纯 Python 依赖编进 mil.pyd, + # C 扩展由插件自带,runtime_hook 把 plugins/*/ 加进 sys.path 后能加载。 + '--include-module=runtime_hook', f"--include-data-dir={RESOURCES_PATH}=resources", ] diff --git a/core/plugin/plugin_registry.py b/core/plugin/plugin_registry.py index 4ef44f4..46930da 100644 --- a/core/plugin/plugin_registry.py +++ b/core/plugin/plugin_registry.py @@ -1,16 +1,22 @@ """插件注册中心。 架构说明: -- PluginWorker 运行在独立 QThread 中,仅做纯 I/O(扫描 .pyd、importlib 读元数据、 - 文件复制),不创建 QObject、不操作 QWidget。 +- PluginWorker 运行在独立 QThread 中,仅做纯 I/O(扫描 zip/.pyd、下载解压、 + 读 manifest 元数据),不创建 QObject、不操作 QWidget。 - PluginRegistry 留在主线程,负责 create_plugin()(返回 QWidget,必须在 GUI 线程 创建)与信号转发。 - 跨线程通信全部走 Qt 信号槽(自动 Queued),不再使用 threading.Thread。 + +分发模型: +- 远程:每插件一个 --.zip(export_runtime + pack_zip 产物) +- 本地:解压到 plugins// 子目录,卸载删整个子目录 """ import os import sys import time +import json import shutil +import zipfile import logging import importlib import importlib.util @@ -29,19 +35,6 @@ UNINSTALL_PENDING_FILE = LOCAL_PLUGINS_PATH / ".uninstall_pending" logger = logging.getLogger(__name__) -def _remove_with_retry(target: Path, retries: int = 5, interval: float = 0.2) -> bool: - """删除文件,失败时短暂重试,应对自重启场景下原进程未完全释放句柄的情况。""" - for _ in range(retries): - try: - target.unlink() - return True - except PermissionError: - time.sleep(interval) - except FileNotFoundError: - return True - return False - - def _load_module_from_path(module_name: str, file_path: Path): """按文件路径加载模块,绕过 sys.modules 缓存。 @@ -59,6 +52,15 @@ def _load_module_from_path(module_name: str, file_path: Path): return module +def _pyd_module_name(file_path: Path) -> str: + """从 .pyd 文件名提取 Python 模块名(PyInit_ 后缀)。 + + Nuitka 产物形如 mil.cp311-win_amd64.pyd,C 扩展入口函数为 + PyInit_mil,因此模块名必须取第一个点之前的部分,不能用完整 stem。 + """ + return file_path.name.split('.', 1)[0] + + class Event(Enum): Update = auto() Install = auto() @@ -79,76 +81,85 @@ class PluginWorker(QObject): @Slot(str) def do_discover_local(self, plugins_dir: str) -> None: - print(plugins_dir) + """扫描 plugins/<插件名>/ 子目录下的主插件 .pyd。""" plugins_dir = Path(plugins_dir) if not self._ensure_dir(plugins_dir): return - self.local_discovered.emit(self._scan_plugins(plugins_dir)) + self.local_discovered.emit(self._scan_local_plugins(plugins_dir)) @Slot(str) def do_discover_remote(self, plugins_dir: str) -> None: - print(plugins_dir) + """扫描远程 *.zip,读 zip 内 manifest.json 取插件元数据。""" if not os.path.exists(plugins_dir): logger.error(f"服务器链接错误!路径不存在: {plugins_dir}") return - logger.info(f"开始扫描远程插件: {plugins_dir}") + logger.info(f"开始扫描远程插件 zip: {plugins_dir}") self.remote_discovered.emit( - self._scan_plugins(Path(plugins_dir), with_remote_meta=True) + self._scan_remote_zips(Path(plugins_dir)) ) @Slot(str, str) - def do_install(self, local_path: str, remote_path: str) -> None: + def do_install(self, local_dir: str, remote_zip: str) -> None: + """下载 zip 流式写本地临时文件,再解压到 plugins/<插件名>/。 + + 流式下载保留 install_progress 信号,便于 UI 显示进度条。 + """ try: - total_size = os.path.getsize(remote_path) - copied_size = 0 - with open(remote_path, 'rb') as fsrc, open(local_path, 'wb') as fdst: + total_size = os.path.getsize(remote_zip) + target = Path(local_dir) + target.mkdir(parents=True, exist_ok=True) + tmp_zip = target / ".download.tmp" + + copied = 0 + with open(remote_zip, 'rb') as fsrc, open(tmp_zip, 'wb') as fdst: while True: buf = fsrc.read(1024 * 1024) if not buf: break fdst.write(buf) - copied_size += len(buf) - self.install_progress.emit(copied_size, total_size) - time.sleep(0.1) - self.install_finished.emit(True, f"{local_path}安装成功!!!") + copied += len(buf) + self.install_progress.emit(copied, total_size) + + with zipfile.ZipFile(tmp_zip, 'r') as zf: + zf.extractall(target) + tmp_zip.unlink() + self.install_finished.emit(True, f"安装成功: {local_dir}") except Exception as e: - logger.error(f"{remote_path}安装失败{e.args}") + logger.error(f"{remote_zip}安装失败{e.args}") self.install_finished.emit(False, str(e)) @Slot(str, str) - def do_uninstall(self, name: str, local_path: str) -> None: - """运行时无法删除已加载的 .pyd(Windows 文件锁),仅记入待删清单。 + def do_uninstall(self, name: str, local_dir: str) -> None: + """运行时无法删除已加载 .pyd,记入待删清单(目录路径),重启时删整个目录。 - 真正的文件删除由 PluginRegistry._process_pending_uninstalls 在下次启动、 + 真正的目录删除由 PluginRegistry._process_pending_uninstalls 在下次启动、 import 之前完成。运行时清理(字典/UI/widget)由主线程在 uninstall_finished 回调中处理。 """ try: - tool_name = Path(local_path).stem UNINSTALL_PENDING_FILE.parent.mkdir(parents=True, exist_ok=True) with open(UNINSTALL_PENDING_FILE, 'a', encoding='utf-8') as f: - f.write(f"{tool_name}\n") + f.write(f"{local_dir}\n") self.uninstall_finished.emit(name, True, f"{name}已加入卸载清单,重启后生效!!!") except Exception as e: - logger.error(f"{local_path}卸载失败{e.args}") + logger.error(f"{local_dir}卸载失败{e.args}") self.uninstall_finished.emit(name, False, str(e)) @Slot(str, str, str) - def do_update(self, name: str, local_path: str, remote_path: str) -> None: - """更新=删旧 .pyd + 从服务器复制新 .pyd。 + def do_update(self, name: str, local_dir: str, remote_zip: str) -> None: + """更新=删旧目录+从远程 zip 解压新版本。运行时记清单,重启时执行。 - 运行时 .pyd 已加载无法删除,记入待处理清单(含 remote_path), - 重启时由 _process_pending_uninstalls 删除旧文件并复制新文件。 + 运行时 .pyd 已加载无法删除,记入待处理清单(含 remote_zip), + 重启时由 _process_pending_uninstalls 删除旧目录并解压新 zip。 运行时 UI 清理复用 uninstall_finished 信号链路,触发与卸载一致的自重启。 """ try: - tool_name = Path(local_path).stem UNINSTALL_PENDING_FILE.parent.mkdir(parents=True, exist_ok=True) with open(UNINSTALL_PENDING_FILE, 'a', encoding='utf-8') as f: - f.write(f"{tool_name}|{remote_path}\n") + f.write(f"{local_dir}|{remote_zip}\n") self.uninstall_finished.emit(name, True, f"{name}更新已加入清单,重启后生效!!!") except Exception as e: - logger.error(f"{local_path}更新失败{e.args}") + logger.error(f"{local_dir}更新失败{e.args}") self.uninstall_finished.emit(name, False, str(e)) @staticmethod @@ -159,33 +170,56 @@ class PluginWorker(QObject): return True @staticmethod - def _scan_plugins(plugins_dir: Path, with_remote_meta: bool = False) -> list: - """扫描目录下的 .pyd 插件,按文件路径加载读取元数据。 + def _scan_local_plugins(plugins_dir: Path) -> list: + """扫描 plugins/<插件名>/ 子目录,读主插件 .pyd 元数据。 - .pyd 是 C 扩展,入口函数 PyInit_<文件名> 与文件名绑定,无法用别名 - 加载。因此本地与远程都用原名加载,但远程扫描前清掉 sys.modules - 缓存(避免读到本地旧版本),读完后再次清缓存(避免污染本地逻辑)。 - 本地扫描用原名加载并保留缓存,供 _on_local_discovered 二次 import。 + 跳过伴生包(openpyxl/et_xmlfile)与 C 扩展(下划线开头)。 + 每个插件子目录下只应有一个主插件 .pyd。 """ results: list = [] - for tool in plugins_dir.glob("*.pyd"): - tool_name = tool.stem + for sub in plugins_dir.iterdir(): + if not sub.is_dir(): + continue + for tool in sub.glob("*.pyd"): + # .pyd 文件名形如 mil.cp311-win_amd64.pyd, + # 模块名取第一个点之前的部分(PyInit_ 后缀) + tool_name = _pyd_module_name(tool) + # 伴生包与 C 扩展不是主插件,跳过 + if tool_name in ("openpyxl", "et_xmlfile") or tool_name.startswith("_"): + continue + try: + module = _load_module_from_path(tool_name, tool) + results.append({ + "tool_name": tool_name, + "name": module.read_plugin_name(), + "version": module.read_plugin_version(), + "local_dir": str(sub), + }) + except Exception as e: + logger.error(f"插件{tool_name}加载失败:{e}") + return results + + @staticmethod + def _scan_remote_zips(plugins_dir: Path) -> list: + """扫描远程 *.zip,读 zip 内 manifest.json 取插件元数据。 + + zip 文件名约定:--.zip + manifest.json 由 export_runtime.py 生成,含 plugin_name/version/description。 + """ + results: list = [] + for zip_file in plugins_dir.glob("*.zip"): try: - if with_remote_meta: - sys.modules.pop(tool_name, None) - module = _load_module_from_path(tool_name, tool) - item = { - "tool_name": tool_name, - "name": module.read_plugin_name(), - "version": module.read_plugin_version(), - } - if with_remote_meta: - item["description"] = module.read_plugin_description() - item["remote_path"] = str(plugins_dir) - sys.modules.pop(tool_name, None) - results.append(item) + with zipfile.ZipFile(zip_file, "r") as zf: + manifest = json.loads(zf.read("manifest.json")) + results.append({ + "tool_name": manifest["tool_name"], + "name": manifest["plugin_name"], + "version": manifest["plugin_version"], + "description": manifest["plugin_description"], + "remote_path": str(zip_file), + }) except Exception as e: - logger.error(f"插件{tool_name}加载失败:{e}") + logger.error(f"远程 zip {zip_file.name} 读取失败:{e}") return results @@ -200,6 +234,8 @@ class PluginRegistry(QObject): plugins_loader_signal = Signal(dict) update_plugins_card_signal = Signal(dict) uninstall_completed_signal = Signal(str) + install_progress = Signal(str, int, int) # name, copied_bytes, total_bytes + install_finished = Signal(bool, str) _discover_local_requested = Signal(str) _discover_remote_requested = Signal(str) @@ -207,14 +243,17 @@ class PluginRegistry(QObject): _uninstall_requested = Signal(str, str) _update_requested = Signal(str, str, str) + _PENDING_CLEANUP_DELAY = 1500 + def __init__(self, parent: Optional[QObject] = None) -> None: super().__init__(parent) self.plugins: dict = {} + self._installing_name: str = "" + self._last_logged_pct: int = 0 # 节流:记录上次上报的百分比 - # 启动时先清理上次遗留的待删清单(此时相关 .pyd 尚未 import, - # 且 worker 线程未启动,无并发文件访问) - self._process_pending_uninstalls() - + # Worker 在线程启动前创建(信号连接必须先于线程启动), + # 但实际启动延迟至清理完成后,避免新进程在 shutil.rmtree 之前 + # 就把插件 .pyd 加载进内存导致自锁。 self._worker = PluginWorker() self._thread = QThread() self._worker.moveToThread(self._thread) @@ -229,10 +268,22 @@ class PluginRegistry(QObject): self._worker.remote_discovered.connect(self._on_remote_discovered) self._worker.install_finished.connect(self._on_install_finished) self._worker.uninstall_finished.connect(self._on_uninstall_finished) + self._worker.install_progress.connect(self._on_install_progress) self._thread.finished.connect(self._worker.deleteLater) + # 延迟启动:先等旧进程彻底退出并释放 .pyd 句柄, + # 再执行待删清单清理,最后才扫描并加载插件。 + # 顺序必须是 清理 → 启动线程 → 发现插件,否则新进程自锁。 + QTimer.singleShot(self._PENDING_CLEANUP_DELAY, self._post_init) + + def _post_init(self) -> None: + """延迟初始化:清理 → 启动线程 → 发现插件。 + + 必须在旧进程退出后执行,且清理必须在任何插件 import 之前完成, + 否则新进程自己持有 .pyd 文件锁导致 shutil.rmtree 失败。 + """ + self._process_pending_uninstalls() self._thread.start() - # 延迟到事件循环启动后触发,保证所有接收方先 connect 后 emit QTimer.singleShot(0, self._start_discovery) def shutdown(self) -> None: @@ -246,15 +297,15 @@ class PluginRegistry(QObject): @staticmethod def _process_pending_uninstalls() -> None: - """启动时执行待处理清单:删除 .pyd 文件,更新项另从服务器复制新文件。 + """启动时执行待处理清单:删除插件目录,更新项另从远程 zip 解压。 清单行格式: - - ``tool_name`` 纯卸载,仅删除本地 .pyd - - ``tool_name|remote_path`` 更新,删除后从 remote_path 复制新文件 + - ```` 纯卸载,仅删除本地目录 + - ``|`` 更新,删除后从 remote_zip 解压新版本 必须在任何 importlib.import_module 之前调用,此时 .pyd 未被加载, Windows 文件锁不会触发 WinError 5。但自重启场景下,原进程可能尚未 - 完全释放文件句柄,因此删除失败时短暂重试。失败项保留原始清单行待下次处理。 + 完全释放文件句柄,因此删除失败时保留清单行待下次处理。 """ if not UNINSTALL_PENDING_FILE.exists(): return @@ -265,24 +316,38 @@ class PluginRegistry(QObject): if not line: continue parts = line.split('|', 1) - tool_name = parts[0] - target = LOCAL_PLUGINS_PATH / f"{tool_name}.pyd" - # 纯卸载且文件已不存在视为已完成;更新项即使文件不存在也要继续复制 + target = Path(parts[0]) + # 纯卸载且目录已不存在视为已完成;更新项即使目录不存在也要继续解压 if not target.exists() and len(parts) == 1: continue - if not _remove_with_retry(target): - failed.append(line) - logger.error(f"删除插件文件失败(重试后仍被占用): {target}") - continue - logger.info(f"已删除插件文件: {target}") - if len(parts) == 2: - remote_path = Path(parts[1]) + # 删除整个插件目录(带重试:Windows 文件锁可能在短时间后释放) + deleted = False + last_error = None + for attempt in range(3): try: - shutil.copyfile(remote_path, target) - logger.info(f"已更新插件文件: {target}") + shutil.rmtree(target) + logger.info(f"已删除插件目录: {target}") + deleted = True + break + except Exception as e: + last_error = e + if attempt < 2: + time.sleep(0.5) + if not deleted: + failed.append(line) + logger.error(f"删除插件目录失败(可能仍被占用): {target} {last_error}") + continue + # 更新项:从远程 zip 解压新版本 + if len(parts) == 2: + remote_zip = Path(parts[1]) + try: + target.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(remote_zip, 'r') as zf: + zf.extractall(target) + logger.info(f"已更新插件: {target}") except Exception as e: failed.append(line) - logger.error(f"复制更新文件失败: {e}") + logger.error(f"解压更新失败: {e}") if failed: UNINSTALL_PENDING_FILE.write_text("\n".join(failed) + "\n", encoding='utf-8') else: @@ -306,7 +371,10 @@ class PluginRegistry(QObject): self.plugins[name] = {} self.plugins[name]["obj"] = obj self.plugins[name]["local version"] = item["version"] + self.plugins[name]["local_dir"] = item["local_dir"] self.plugins_loader_signal.emit(self.plugins) + # 本地扫描完成后也刷新卡片(安装后 local version 更新,卡片需重绘状态) + self.update_plugins_card_signal.emit(self.plugins) @Slot(list) def _on_remote_discovered(self, results: list) -> None: @@ -326,11 +394,26 @@ class PluginRegistry(QObject): def _on_install_finished(self, success: bool, message: str) -> None: if success: logger.info(message) - self._start_discovery() - # self._discover_local_requested.emit(str(LOCAL_PLUGINS_PATH)) - + for sub in LOCAL_PLUGINS_PATH.iterdir(): + if sub.is_dir() and str(sub) not in sys.path: + sys.path.insert(0, str(sub)) + self._discover_local_requested.emit(str(LOCAL_PLUGINS_PATH)) else: logger.error(message) + self._installing_name = "" + self.install_finished.emit(success, message) + + @Slot(int, int) + def _on_install_progress(self, copied: int, total: int) -> None: + """将安装进度写入日志(显示在 FrameLog),按百分比节流避免刷屏。""" + if not self._installing_name or total == 0: + return + pct = int(copied / total * 100) + if pct - self._last_logged_pct >= 10 or pct == 100: + mb_copied = copied / 1024 / 1024 + mb_total = total / 1024 / 1024 + logger.info(f"[{self._installing_name}] 安装进度: {pct}% ({mb_copied:.1f}/{mb_total:.1f} MB)") + self._last_logged_pct = pct @Slot(str, bool, str) def _on_uninstall_finished(self, name: str, success: bool, message: str) -> None: @@ -345,17 +428,22 @@ class PluginRegistry(QObject): self.uninstall_completed_signal.emit(name) def start_plugins_event(self, event: Event, name: str) -> None: + """本地子目录路径作为 install/uninstall/update 的目标。 + + 本地布局:plugins//(zip 解压产物) + 远程来源:plugins[name]['remote path'] 指向 .zip 文件 + """ + tool_name = self.plugins[name]['tool_name'] + local_dir = str(LOCAL_PLUGINS_PATH / tool_name) if event == Event.Install: - tool_name = self.plugins[name]['tool_name'] - local_path = str(LOCAL_PLUGINS_PATH) + '\\' + f"{tool_name}.pyd" - remote_path = str(REMOTE_PLUGINS_PATH) + '\\' + f"{tool_name}.pyd" - self._install_requested.emit(local_path, remote_path) + self._installing_name = name + self._last_logged_pct = 0 + logger.info(f"[{name}] 开始安装...") + remote_path = self.plugins[name]['remote path'] + self._install_requested.emit(local_dir, remote_path) elif event == Event.Uninstall: - tool_name = self.plugins[name]['tool_name'] - local_path = str(LOCAL_PLUGINS_PATH) + '\\' + f"{tool_name}.pyd" - self._uninstall_requested.emit(name, local_path) + self._uninstall_requested.emit(name, local_dir) elif event == Event.Update: - tool_name = self.plugins[name]['tool_name'] - local_path = str(LOCAL_PLUGINS_PATH) + '\\' + f"{tool_name}.pyd" - remote_path = str(REMOTE_PLUGINS_PATH) + '\\' + f"{tool_name}.pyd" - self._update_requested.emit(name, local_path, remote_path) + self._installing_name = name + remote_path = self.plugins[name]['remote path'] + self._update_requested.emit(name, local_dir, remote_path) diff --git a/core/plugin/plugins.py b/core/plugin/plugins.py index 87341d1..bdbc5c4 100644 --- a/core/plugin/plugins.py +++ b/core/plugin/plugins.py @@ -25,7 +25,7 @@ from .plugins_ui import Ui_FormPlugins from PySide6.QtGui import QIcon from PySide6.QtWidgets import QWidget, QListWidgetItem -from PySide6.QtCore import QSize,Signal +from PySide6.QtCore import QSize, Signal from .plugins_card import PluginsCard from .plugin_registry import PluginRegistry @@ -137,22 +137,24 @@ class Plugins(QWidget, Ui_FormPlugins): # self.update_tool_card.emit(tools) def on_update_plugins_card(self, plugins:dict): - # print(plugins) for name in plugins.keys(): - item = QListWidgetItem(self.listWidget) - plugin_card = PluginsCard(name, plugins[name]) - plugin_card.send_event_signal.connect(self.plugin_registry.start_plugins_event) - self.cards[name] = plugin_card + if name in self.cards: + # 卡片已存在,只刷新状态(避免重复创建叠加) + self.cards[name].info = plugins[name] + self.cards[name].refresh() + else: + item = QListWidgetItem(self.listWidget) + plugin_card = PluginsCard(name, plugins[name]) + plugin_card.send_event_signal.connect(self.plugin_registry.start_plugins_event) + self.cards[name] = plugin_card - item.setSizeHint(QSize(plugin_card.sizeHint().width(), 200)) - self.listWidget.addItem(item) - self.listWidget.setCurrentItem(item) - self.listWidget.setItemWidget(item, plugin_card) - pass + item.setSizeHint(QSize(plugin_card.sizeHint().width(), 200)) + self.listWidget.addItem(item) + self.listWidget.setCurrentItem(item) + self.listWidget.setItemWidget(item, plugin_card) def on_plugin_uninstalled(self, name: str) -> None: """卸载完成后刷新对应卡片状态。""" card = self.cards.get(name) if card is not None: - card.refresh() - \ No newline at end of file + card.refresh() \ No newline at end of file diff --git a/core/plugin/plugins_card.py b/core/plugin/plugins_card.py index 458dd53..9cc72e2 100644 --- a/core/plugin/plugins_card.py +++ b/core/plugin/plugins_card.py @@ -59,7 +59,7 @@ class PluginsCard(QFrame, Ui_FramePluginsCard): def refresh(self) -> None: """根据 info 刷新卡片显示与按钮状态。""" self.textBrowser.setMarkdown(self.to_card()) - installed = self.info["local version"] is not None + installed = self.info.get("local version") is not None self.pushButtonInstall.setEnabled(not installed) self.pushButtonUninstall.setEnabled(installed) if installed: @@ -70,13 +70,17 @@ class PluginsCard(QFrame, Ui_FramePluginsCard): self.pushButtonUpdate.setEnabled(False) def to_card(self): + remote_ver = self.info.get('remote version') + local_ver = self.info.get('local version') + desc = self.info.get('remote description') or self.info.get('local description') or '暂无' + card = f"# {self.name}\n\n" - card += f"**最新:** V{self.info['remote version']}\n\n" - if self.info['local version'] is None: + card += f"**最新:** V{remote_ver}\n\n" if remote_ver else f"**最新:** 暂无版本信息\n\n" + if local_ver is None: card += f"**当前:** 未安装!\n\n" else: - card += f"**当前:** V{self.info['local version']}\n\n" - card += f"**描述:** {self.info['remote description']}" + card += f"**当前:** V{local_ver}\n\n" + card += f"**描述:** {desc}" return card def on_install_event(self): diff --git a/main.py b/main.py index 9d32c49..1d80269 100644 --- a/main.py +++ b/main.py @@ -13,6 +13,8 @@ Model Team Tools - 主程序入口 import os import sys import logging +import subprocess +import runtime_hook # noqa: F401 启动时把 plugins/*/ 加进 sys.path,必须在 core 之前 import core import traceback import importlib @@ -20,7 +22,7 @@ import importlib from pathlib import Path from typing import Optional -from PySide6.QtCore import QProcess, QTimer +from PySide6.QtCore import QTimer from PySide6.QtGui import QIcon,QAction from PySide6.QtWidgets import QApplication, QMainWindow, QMenuBar, QStatusBar from qt_material import apply_stylesheet @@ -159,25 +161,39 @@ class MainWindow(QMainWindow, Ui_MainWindow): def _restart_app(self) -> None: """启动新进程并退出当前进程。 - sys.executable + sys.argv 同时兼容开发模式(python.exe main.py) - 与打包模式(main.exe)。 + 开发模式:python.exe main.py + 打包模式:main.exe(Nuitka standalone) + + 用 subprocess.Popen + DETACHED_PROCESS 直接调 Win32 CreateProcess。 + Nuitka standalone 下 sys.executable 指向不存在的原始 python.exe, + 此时改用 sys.argv[0](即 main.exe 自身)。 """ work_dir = os.path.dirname(os.path.abspath(sys.argv[0])) - - # 【重要】添加防递归标记,防止新进程启动后再次触发重启逻辑 + args = sys.argv[:] if '--restarting' not in args: args.append('--restarting') - - # 启动新进程 - pid = QProcess.startDetached(sys.executable, args, work_dir) - - if pid: - print(f"新进程启动成功 (PID: {pid}),准备退出当前进程...") - # ✅ 关键修复:延迟 300ms 退出,确保新进程彻底“断奶” - QTimer.singleShot(300, QApplication.quit) + + # Nuitka standalone 下 sys.executable 指向编译机器的 python.exe(分发环境不存在), + # 直接用 os.path.isfile 检测,不存在时回退到 sys.argv[0](main.exe 自身)。 + if os.path.isfile(sys.executable): + cmd = [sys.executable] + args else: - print("重启失败,请检查路径或权限") + cmd = [os.path.abspath(sys.argv[0])] + args[1:] + + try: + proc = subprocess.Popen( + cmd, + cwd=work_dir, + creationflags=subprocess.DETACHED_PROCESS, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + logger.info(f"新进程已启动 (PID: {proc.pid}),当前进程即将退出") + QTimer.singleShot(100, QApplication.quit) + except Exception as e: + logger.error(f"启动新进程失败: {e}") if __name__ == '__main__': diff --git a/nuitka-crash-report.xml b/nuitka-crash-report.xml index 6d273c3..ed766f4 100644 --- a/nuitka-crash-report.xml +++ b/nuitka-crash-report.xml @@ -1,10 +1,9 @@ - - + + - - - + + @@ -20,36 +19,33 @@ - - - + + - - + + - - - + + - - + + - - - + + @@ -59,34 +55,33 @@ - - + + - - + + - - + + - - + + - - + + - - - + + @@ -97,27 +92,25 @@ - - - + + - - - + + + - - + + - - - + + @@ -126,13 +119,22 @@ + - - - - - - + + + + + + + + + + + + + + @@ -262,6 +264,8 @@ + + @@ -417,10 +421,9 @@ - - - - + + + @@ -429,54 +432,53 @@ + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + @@ -485,78 +487,78 @@ - - + + - - + + - - + + - - - + + + - - - + + + - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + @@ -568,20 +570,20 @@ - - + + - - - + + + - - + + @@ -595,8 +597,8 @@ - - + + @@ -618,13 +620,13 @@ - - + + - - + + @@ -633,18 +635,18 @@ - - + + - - + + - - - + + + @@ -659,34 +661,34 @@ - - - + + + - - - + + + - - - + + + - - + + @@ -700,9 +702,9 @@ - - - + + + @@ -715,8 +717,8 @@ - - + + @@ -740,8 +742,8 @@ - - + + @@ -787,8 +789,8 @@ - - + + @@ -797,8 +799,8 @@ - - + + @@ -811,8 +813,8 @@ - - + + @@ -824,15 +826,15 @@ - - + + - - + + @@ -843,8 +845,8 @@ - - + + @@ -860,13 +862,13 @@ - - + + - - + + @@ -880,8 +882,8 @@ - - + + @@ -903,8 +905,8 @@ - - + + @@ -917,8 +919,8 @@ - - + + @@ -927,8 +929,8 @@ - - + + @@ -936,8 +938,8 @@ - - + + @@ -966,13 +968,13 @@ - - + + - - + + @@ -984,8 +986,8 @@ - - + + @@ -1005,8 +1007,8 @@ - - + + @@ -1044,8 +1046,8 @@ - - + + @@ -1063,8 +1065,8 @@ - - + + @@ -1079,8 +1081,8 @@ - - + + @@ -1102,8 +1104,8 @@ - - + + @@ -1118,8 +1120,8 @@ - - + + @@ -1130,8 +1132,8 @@ - - + + @@ -1161,8 +1163,8 @@ - - + + @@ -1172,8 +1174,8 @@ - - + + @@ -1187,20 +1189,20 @@ - - + + - - + + - - + + @@ -1232,8 +1234,8 @@ - - + + @@ -1245,26 +1247,26 @@ - - - + + + - + - - + + - - + + @@ -1272,9 +1274,9 @@ - - - + + + @@ -1283,8 +1285,8 @@ - - + + @@ -1302,8 +1304,8 @@ - - + + @@ -1321,16 +1323,16 @@ - - + + - - + + @@ -1340,8 +1342,8 @@ - - + + @@ -1349,9 +1351,9 @@ - - - + + + @@ -1359,16 +1361,16 @@ - - + + - - - + + + @@ -1390,9 +1392,9 @@ - - - + + + @@ -1400,20 +1402,20 @@ - - + + - - + + - - + + @@ -1422,8 +1424,8 @@ - - + + @@ -1436,8 +1438,8 @@ - - + + @@ -1456,8 +1458,8 @@ - - + + @@ -1470,8 +1472,8 @@ - - + + @@ -1490,9 +1492,9 @@ - - - + + + @@ -1504,16 +1506,16 @@ - - - + + + - - - + + + @@ -1521,17 +1523,176 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + @@ -1540,8 +1701,8 @@ - - + + @@ -1575,16 +1736,16 @@ - - + + - - - + + + @@ -1600,10 +1761,10 @@ - + - - + + @@ -1615,8 +1776,8 @@ - - + + @@ -1629,8 +1790,8 @@ - - + + @@ -1640,9 +1801,9 @@ - - - + + + @@ -1653,9 +1814,9 @@ - - - + + + @@ -1665,8 +1826,8 @@ - - + + @@ -1679,8 +1840,8 @@ - - + + @@ -1696,16 +1857,16 @@ - - + + - - + + @@ -1716,16 +1877,16 @@ - - + + - - + + @@ -1736,8 +1897,8 @@ - - + + @@ -1748,21 +1909,21 @@ - - + + - - + + - - + + @@ -1775,8 +1936,8 @@ - - + + @@ -1788,8 +1949,8 @@ - - + + @@ -1801,8 +1962,8 @@ - - + + @@ -1814,16 +1975,16 @@ - - + + - - + + @@ -1844,8 +2005,8 @@ - - + + @@ -1853,8 +2014,8 @@ - - + + @@ -1866,8 +2027,8 @@ - - + + @@ -1875,8 +2036,8 @@ - - + + @@ -1892,9 +2053,9 @@ - - - + + + @@ -1905,29 +2066,29 @@ - - - + + + - - + + - - + + - - + + @@ -1935,8 +2096,8 @@ - - + + @@ -1944,282 +2105,282 @@ - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + @@ -2227,8 +2388,8 @@ - - + + @@ -2236,8 +2397,8 @@ - - + + @@ -2245,8 +2406,8 @@ - - + + @@ -2254,8 +2415,8 @@ - - + + @@ -2263,8 +2424,8 @@ - - + + @@ -2272,8 +2433,8 @@ - - + + @@ -2281,8 +2442,8 @@ - - + + @@ -2290,8 +2451,8 @@ - - + + @@ -2299,8 +2460,8 @@ - - + + @@ -2308,23 +2469,23 @@ - - + + - - + + - - + + @@ -2332,8 +2493,8 @@ - - + + @@ -2342,8 +2503,8 @@ - - + + @@ -2351,8 +2512,8 @@ - - + + @@ -2360,8 +2521,8 @@ - - + + @@ -2369,8 +2530,8 @@ - - + + @@ -2378,8 +2539,8 @@ - - + + @@ -2387,8 +2548,8 @@ - - + + @@ -2396,8 +2557,8 @@ - - + + @@ -2405,113 +2566,113 @@ - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + @@ -2519,150 +2680,150 @@ - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - + + + - - + + - - + + - - + + - - + + - - + + @@ -2670,22 +2831,22 @@ - - + + - - + + - - + + @@ -2693,8 +2854,8 @@ - - + + @@ -2702,8 +2863,8 @@ - - + + @@ -2711,94 +2872,94 @@ - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + @@ -2806,17 +2967,17 @@ - - + + - + - - + + @@ -2829,8 +2990,8 @@ - - + + @@ -2849,8 +3010,8 @@ - - + + @@ -2863,9 +3024,9 @@ - - - + + + @@ -2879,8 +3040,8 @@ - - + + @@ -2891,9 +3052,9 @@ - - - + + + @@ -2904,9 +3065,9 @@ - - - + + + @@ -2926,25 +3087,25 @@ - - - + + + - - + + - - + + @@ -2956,9 +3117,9 @@ - - - + + + @@ -2974,8 +3135,8 @@ - - + + @@ -2992,15 +3153,15 @@ - - + + - - - + + + @@ -3020,8 +3181,8 @@ - - + + @@ -3040,10 +3201,10 @@ - + - - + + @@ -3052,8 +3213,8 @@ - - + + @@ -3061,23 +3222,23 @@ - - - + + + - - - + + + - - + + @@ -3086,16 +3247,16 @@ - - + + - - + + @@ -3112,8 +3273,8 @@ - - + + @@ -3136,8 +3297,8 @@ - - + + @@ -3156,8 +3317,8 @@ - - + + @@ -3168,8 +3329,8 @@ - - + + @@ -3190,9 +3351,9 @@ - - - + + + @@ -3203,9 +3364,9 @@ - - - + + + @@ -3213,9 +3374,34 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3229,9 +3415,9 @@ - - - + + + @@ -3246,9 +3432,9 @@ - - - + + + @@ -3278,15 +3464,14 @@ - - - - + + + @@ -3294,53 +3479,53 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + @@ -3348,9 +3533,9 @@ - - - + + + @@ -3358,9 +3543,9 @@ - - - + + + @@ -3374,17 +3559,17 @@ - - - + + + - - - + + + @@ -3397,9 +3582,9 @@ - - - + + + @@ -3409,9 +3594,9 @@ - - - + + + @@ -3423,8 +3608,8 @@ - - + + @@ -3434,15 +3619,15 @@ - - + + - - - + + + @@ -3463,9 +3648,9 @@ - - - + + + @@ -3496,9 +3681,9 @@ - - - + + + @@ -3508,17 +3693,16 @@ - - + + - - - + + @@ -3555,17 +3739,15 @@ - - - + + - - - + + @@ -3576,9 +3758,8 @@ - - - + + @@ -3598,9 +3779,8 @@ - - - + + @@ -3633,15 +3813,13 @@ - - - + + - - - + + @@ -3653,9 +3831,8 @@ - - - + + @@ -3668,9 +3845,8 @@ - - - + + @@ -3739,9 +3915,8 @@ - - - + + @@ -3749,9 +3924,8 @@ - - - + + @@ -3789,9 +3963,8 @@ - - - + + @@ -3801,9 +3974,8 @@ - - - + + @@ -3817,9 +3989,8 @@ - - - + + @@ -3859,9 +4030,8 @@ - - - + + @@ -3878,9 +4048,8 @@ - - - + + @@ -3889,9 +4058,8 @@ - - - + + @@ -3904,9 +4072,8 @@ - - - + + @@ -3941,9 +4108,8 @@ - - - + + @@ -3955,9 +4121,8 @@ - - - + + @@ -3985,17 +4150,16 @@ - - - + + - - - + + + @@ -4003,10 +4167,10 @@ - + - - + + @@ -4014,10 +4178,10 @@ - + - - + + @@ -4025,23 +4189,23 @@ - + - - + + - - - + + + - - - + + + @@ -4052,9 +4216,9 @@ - - - + + + @@ -4075,8 +4239,8 @@ - - + + @@ -4102,8 +4266,8 @@ - - + + @@ -4114,8 +4278,8 @@ - - + + @@ -4123,9 +4287,8 @@ - - - + + @@ -4135,14 +4298,13 @@ - - + + - - - + + @@ -4158,19 +4320,18 @@ - - - + + - - + + - - - + + + @@ -4181,8 +4342,8 @@ - - + + @@ -4197,8 +4358,8 @@ - - + + @@ -4207,30 +4368,24 @@ - - - + + - - - - - - - + + - - + + @@ -4262,8 +4417,8 @@ - - + + @@ -4300,8 +4455,8 @@ - - + + @@ -4316,15 +4471,15 @@ - - + + - - - + + + @@ -4349,8 +4504,8 @@ - - + + @@ -4367,8 +4522,8 @@ - - + + @@ -4397,8 +4552,8 @@ - - + + @@ -4419,8 +4574,8 @@ - - + + @@ -4436,8 +4591,8 @@ - - + + @@ -4457,8 +4612,8 @@ - - + + @@ -4480,8 +4635,8 @@ - - + + @@ -4502,8 +4657,8 @@ - - + + @@ -4522,8 +4677,8 @@ - - + + @@ -4537,8 +4692,8 @@ - - + + @@ -4553,8 +4708,8 @@ - - + + @@ -4566,8 +4721,8 @@ - - + + @@ -4590,8 +4745,8 @@ - - + + @@ -4612,8 +4767,9 @@ - - + + + @@ -4634,17 +4790,11 @@ - - - - - - - - + + @@ -4652,9 +4802,9 @@ - - - + + + @@ -4671,33 +4821,33 @@ - - - + + + - - - + + + - + - - + + - - - + + + @@ -4705,9 +4855,9 @@ - - - + + + @@ -4726,9 +4876,9 @@ - - - + + + @@ -4751,9 +4901,9 @@ - - - + + + @@ -4777,8 +4927,8 @@ - - + + @@ -4792,8 +4942,8 @@ - - + + @@ -4802,9 +4952,9 @@ - - - + + + @@ -4826,10 +4976,10 @@ - + - - + + @@ -4855,8 +5005,8 @@ - - + + @@ -4867,9 +5017,9 @@ - - - + + + @@ -4883,9 +5033,9 @@ - - - + + + @@ -4897,8 +5047,8 @@ - - + + @@ -4912,8 +5062,8 @@ - - + + @@ -4927,8 +5077,8 @@ - - + + @@ -4936,14 +5086,13 @@ - - + + - - - - + + + @@ -4987,19 +5136,17 @@ - - - - + + + - - - - + + + @@ -5014,8 +5161,8 @@ - - + + @@ -5027,9 +5174,9 @@ - - - + + + @@ -5037,8 +5184,8 @@ - - + + @@ -5057,9 +5204,9 @@ - - - + + + @@ -5073,14 +5220,14 @@ - - - + + + - - - + + + @@ -5090,16 +5237,16 @@ - - - + + + - - - + + + @@ -5112,9 +5259,9 @@ - - - + + + @@ -5122,8 +5269,8 @@ - - + + @@ -5135,8 +5282,8 @@ - - + + @@ -5148,9 +5295,20 @@ + + + + + + + + + + + - - + + @@ -5162,8 +5320,8 @@ - - + + @@ -5172,13 +5330,13 @@ - - + + - - + + @@ -5190,10 +5348,8 @@ - - - - + + @@ -5216,13 +5372,13 @@ - - + + - - + + @@ -5235,9 +5391,9 @@ - - - + + + @@ -5300,9 +5456,9 @@ - - - + + + @@ -5310,8 +5466,8 @@ - - + + @@ -5325,8 +5481,8 @@ - - + + @@ -5341,8 +5497,8 @@ - - + + @@ -5355,61 +5511,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -5417,8 +5521,8 @@ - - + + @@ -5426,8 +5530,8 @@ - - + + @@ -5435,8 +5539,8 @@ - - + + @@ -5464,16 +5568,16 @@ - - - + + + - - + + @@ -5493,9 +5597,9 @@ - - - + + + @@ -5504,15 +5608,15 @@ - - + + - - - + + + @@ -5520,8 +5624,8 @@ - - + + @@ -5553,8 +5657,8 @@ - - + + @@ -5563,8 +5667,8 @@ - - + + @@ -5585,9 +5689,9 @@ - - - + + + @@ -5616,8 +5720,8 @@ - - + + @@ -5641,15 +5745,15 @@ - - + + - - - + + + @@ -5680,8 +5784,8 @@ - - + + @@ -5694,14 +5798,14 @@ - - - + + + - - - + + + @@ -5717,16 +5821,16 @@ - - + + - - + + @@ -5738,8 +5842,8 @@ - - + + @@ -5750,15 +5854,15 @@ - - + + - - + + @@ -5781,9 +5885,9 @@ - - - + + + @@ -5794,9 +5898,9 @@ - - - + + + @@ -5808,9 +5912,9 @@ - - - + + + @@ -5818,8 +5922,8 @@ - - + + @@ -5838,21 +5942,21 @@ - - - + + + - - - + + + - - + + @@ -5860,8 +5964,8 @@ - - + + @@ -5874,8 +5978,8 @@ - - + + @@ -5921,15 +6025,15 @@ - - + + - - + + @@ -5937,9 +6041,9 @@ - - - + + + @@ -5953,9 +6057,9 @@ - - - + + + @@ -5969,8 +6073,8 @@ - - + + @@ -5991,8 +6095,8 @@ - - + + @@ -6001,36 +6105,36 @@ - - + + - - + + - - + + - - + + - - + + @@ -6047,15 +6151,15 @@ - - + + - - + + @@ -6079,8 +6183,8 @@ - - + + @@ -6091,8 +6195,8 @@ - - + + @@ -6105,23 +6209,23 @@ - - + + - - + + - - + + @@ -6136,15 +6240,15 @@ - - + + - - + + @@ -6164,13 +6268,13 @@ - - + + - - + + @@ -6186,8 +6290,8 @@ - - + + @@ -6197,15 +6301,15 @@ - - + + - - + + @@ -6221,9 +6325,9 @@ - - - + + + @@ -6267,958 +6371,976 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -7304,134 +7426,92 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - @@ -7440,6 +7520,7 @@ + @@ -7454,7 +7535,6 @@ - diff --git a/runtime_hook.py b/runtime_hook.py new file mode 100644 index 0000000..7612889 --- /dev/null +++ b/runtime_hook.py @@ -0,0 +1,118 @@ +"""宿主启动时的 runtime hook。 + +宿主打包时通过 --runtime-hook 指定本文件,宿主 exe 启动时会在 main 之前执行本脚本。 +作用:把 exe 旁边的 plugins/ 目录加进 sys.path, +让插件自带的 .pyd + stdlib 子包(含 C 扩展)可以被 Python import 机制找到。 + +宿主打包命令示例(PyInstaller): + pyinstaller --onefile --runtime-hook tools/runtime_hook.py host_main.py + +宿主打包命令示例(Nuitka): + nuitka --standalone --onefile --include-module=runtime_hook host_main.py + +宿主部署目录布局: + host.exe + plugins/ ← 由 mil_sdk/tools/export_runtime.py 生成 + └── mil/ ← 每插件一个子目录(zip 解压产物) + ├── mil.cp311-*.pyd + ├── openpyxl.cp311-*.pyd + ├── et_xmlfile.cp311-*.pyd + ├── xml/ ← 插件自带的 stdlib 子包(含 _elementtree.pyd) + │ └── etree/ + └── manifest.json + +兼容旧扁平布局:plugins/ 直接含 manifest.json(过渡期)。 + +注意:本文件不依赖任何第三方包,只用 stdlib(os / sys / json / pathlib)。 + 因为它在宿主 sys.path 配置好之前执行,不能 import 任何外部包。 +""" +from __future__ import annotations + +import os +import sys +from pathlib import Path + + +def _resolve_plugins_dir() -> Path | None: + """定位 plugins/ 目录。 + + 优先级: + 1. 环境变量 MIL_PLUGIN_DIR + 2. sys.executable 旁边的 plugins/(onedir 模式) + 3. sys._MEIPASS 旁边的 plugins/(onefile 模式解压目录) + """ + # 1) 环境变量 + env = os.environ.get("MIL_PLUGIN_DIR") + if env: + p = Path(env) + if p.is_dir(): + return p.resolve() + + # 2) onedir:exe 旁边的 plugins/ + exe_dir = Path(sys.executable).resolve().parent + plugins = exe_dir / "plugins" + if plugins.is_dir(): + return plugins.resolve() + + # 3) onefile:PyInstaller 解压目录旁边的 plugins/ + meipass = getattr(sys, "_MEIPASS", None) + if meipass: + plugins = Path(meipass).parent / "plugins" + if plugins.is_dir(): + return plugins.resolve() + + return None + + +def _check_python_version(manifest_path: Path) -> None: + """校验插件的 Python 版本与当前解释器兼容(minor 版本必须一致)。""" + import json + + try: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + except Exception: + return # manifest 损坏不阻止启动,让 import 错误自己暴露 + + plugin_py = manifest.get("python_version", "") + host_py = f"{sys.version_info.major}.{sys.version_info.minor}" + if not plugin_py.startswith(host_py): + sys.stderr.write( + f"[mil] WARNING: 插件 Python 版本({plugin_py}) 与宿主({host_py}) 可能不兼容\n" + ) + + +def _insert_path(path: Path) -> None: + """把 path 加到 sys.path[0],已存在则跳过。""" + p = str(path) + if p not in sys.path: + sys.path.insert(0, p) + + +def setup() -> None: + """把 plugins/ 或 plugins/*/ 子目录加进 sys.path 最前面。 + + 兼容两种布局: + - 旧扁平:plugins/ 直接含 manifest.json(过渡期) + - 新子目录:plugins/<插件名>/ 各自含 manifest.json + """ + plugins_dir = _resolve_plugins_dir() + if plugins_dir is None: + return + + # 旧扁平布局:plugins/ 直接有 manifest.json + if (plugins_dir / "manifest.json").exists(): + _check_python_version(plugins_dir / "manifest.json") + _insert_path(plugins_dir) + return + + # 新子目录布局:逐个扫描 plugins/*/ + for sub in plugins_dir.iterdir(): + if not sub.is_dir(): + continue + if (sub / "manifest.json").exists(): + _check_python_version(sub / "manifest.json") + _insert_path(sub) + + +# 模块加载时立即执行(PyInstaller runtime hook 的约定) +setup() diff --git a/workspace/mil.json b/workspace/mil.json new file mode 100644 index 0000000..1da7587 --- /dev/null +++ b/workspace/mil.json @@ -0,0 +1,8 @@ +{ + "DataPath": "", + "FilePath": "", + "AddTimeEn": true, + "GeratePath": true, + "CurrProject": "", + "ItemConfigs": {} +} \ No newline at end of file