修复依赖缺失问题
This commit is contained in:
parent
535b83cd56
commit
cd4ffbfa90
128
main.py
128
main.py
@ -1,7 +1,11 @@
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
from PyQt6.QtCore import Qt
|
||||
import ctypes
|
||||
import ctypes.wintypes
|
||||
import subprocess
|
||||
import importlib.util
|
||||
from PyQt6.QtCore import Qt, QAbstractNativeEventFilter
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from PyQt6.QtGui import QIcon
|
||||
from db.database import init_db
|
||||
@ -12,6 +16,111 @@ from db import database
|
||||
|
||||
from app_info import __VERSION__, app_title
|
||||
|
||||
|
||||
def _install_requirements_on_startup() -> None:
|
||||
"""
|
||||
启动时仅在“检测到缺失依赖”时安装 requirements。
|
||||
失败时不阻塞主程序启动。
|
||||
"""
|
||||
if getattr(sys, "frozen", False):
|
||||
# 打包态通常无法通过当前 exe 直接调用 pip,直接跳过
|
||||
return
|
||||
|
||||
req_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "requirements.txt")
|
||||
if not os.path.exists(req_path):
|
||||
return
|
||||
|
||||
module_name_map = {
|
||||
"pyqt6": "PyQt6",
|
||||
"pillow": "PIL",
|
||||
}
|
||||
|
||||
def _req_name(req_line: str) -> str:
|
||||
s = req_line.split(";", 1)[0].strip()
|
||||
s = s.split("[", 1)[0]
|
||||
for sep in ("==", ">=", "<=", "~=", "!=", ">", "<"):
|
||||
if sep in s:
|
||||
s = s.split(sep, 1)[0].strip()
|
||||
break
|
||||
return s
|
||||
|
||||
missing_reqs: list[str] = []
|
||||
try:
|
||||
with open(req_path, "r", encoding="utf-8") as f:
|
||||
for raw in f:
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
pkg_name = _req_name(line)
|
||||
if not pkg_name:
|
||||
continue
|
||||
mod_name = module_name_map.get(pkg_name.lower(), pkg_name)
|
||||
if importlib.util.find_spec(mod_name) is None:
|
||||
missing_reqs.append(line)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if not missing_reqs:
|
||||
return
|
||||
|
||||
cmd = [sys.executable, "-m", "pip", "install", *missing_reqs]
|
||||
kwargs = {
|
||||
"stdout": subprocess.DEVNULL,
|
||||
"stderr": subprocess.DEVNULL,
|
||||
"check": False,
|
||||
}
|
||||
if sys.platform == "win32":
|
||||
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
||||
try:
|
||||
subprocess.run(cmd, **kwargs)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class GlobalHotkey(QAbstractNativeEventFilter):
|
||||
"""用 Win32 RegisterHotKey 注册真正的全局快捷键 Alt+`"""
|
||||
_HOTKEY_ID = 0x4E4D # 任意唯一 ID
|
||||
_MOD_ALT = 0x0001
|
||||
_VK_BACKTICK = 0xC0 # VK_OEM_3 = ` 键
|
||||
|
||||
def __init__(self, panel: "PanelWindow", ball: "FloatBall"):
|
||||
super().__init__()
|
||||
self._panel = panel
|
||||
self._ball = ball
|
||||
self._hwnd = None
|
||||
|
||||
def install(self):
|
||||
if sys.platform != "win32":
|
||||
return
|
||||
# 用一个隐藏窗口的 HWND 来接收 WM_HOTKEY
|
||||
self._hwnd = int(self._panel.winId())
|
||||
ctypes.windll.user32.RegisterHotKey(
|
||||
self._hwnd, self._HOTKEY_ID, self._MOD_ALT, self._VK_BACKTICK
|
||||
)
|
||||
QApplication.instance().installNativeEventFilter(self)
|
||||
|
||||
def uninstall(self):
|
||||
if self._hwnd:
|
||||
ctypes.windll.user32.UnregisterHotKey(self._hwnd, self._HOTKEY_ID)
|
||||
QApplication.instance().removeNativeEventFilter(self)
|
||||
|
||||
def nativeEventFilter(self, event_type, message):
|
||||
if event_type == b"windows_generic_MSG":
|
||||
import ctypes
|
||||
msg = ctypes.cast(int(message), ctypes.POINTER(ctypes.wintypes.MSG)).contents
|
||||
WM_HOTKEY = 0x0312
|
||||
if msg.message == WM_HOTKEY and msg.wParam == self._HOTKEY_ID:
|
||||
self._toggle()
|
||||
return False, 0
|
||||
|
||||
def _toggle(self):
|
||||
if self._panel.isVisible():
|
||||
self._panel.minimize_to_ball()
|
||||
else:
|
||||
self._panel.show_near(self._ball.pos(), BALL_SIZE)
|
||||
self._panel.raise_()
|
||||
self._panel.activateWindow()
|
||||
|
||||
# ===================== 打包兼容核心函数 =====================
|
||||
def get_resource_path(relative_path):
|
||||
"""
|
||||
@ -77,7 +186,9 @@ def _wake_existing_or_exit() -> bool:
|
||||
def main():
|
||||
if not _wake_existing_or_exit():
|
||||
return
|
||||
|
||||
|
||||
_install_requirements_on_startup()
|
||||
|
||||
QApplication.setAttribute(Qt.ApplicationAttribute.AA_Use96Dpi, True)
|
||||
try:
|
||||
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
||||
@ -111,16 +222,9 @@ def main():
|
||||
ball.clicked.connect(lambda: panel.show_near(ball.pos(), BALL_SIZE))
|
||||
ball.right_clicked.connect(lambda pos: panel.tray.contextMenu().exec(pos))
|
||||
|
||||
# 全局快捷键 Alt+` 唤醒/隐藏主界面
|
||||
from PyQt6.QtGui import QShortcut, QKeySequence
|
||||
_shortcut = QShortcut(QKeySequence("Alt+`"), panel)
|
||||
_shortcut.setContext(Qt.ShortcutContext.ApplicationShortcut)
|
||||
def _toggle_panel():
|
||||
if panel.isVisible():
|
||||
panel.minimize_to_ball()
|
||||
else:
|
||||
panel.show_near(ball.pos(), BALL_SIZE)
|
||||
_shortcut.activated.connect(_toggle_panel)
|
||||
# 全局快捷键 Alt+` 唤醒/隐藏主界面(Win32 RegisterHotKey,真正全局)
|
||||
_hotkey = GlobalHotkey(panel, ball)
|
||||
_hotkey.install()
|
||||
|
||||
has_saved = bool(database.get_setting("panel_x", ""))
|
||||
if has_saved:
|
||||
|
||||
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
"""标准对话框(QMessageBox / QInputDialog / QFileDialog)与当前主题一致。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtCore import Qt, QTimer
|
||||
from PyQt6.QtGui import QTextOption
|
||||
from PyQt6.QtWidgets import (
|
||||
QFileDialog,
|
||||
@ -37,7 +37,6 @@ def stylesheet() -> str:
|
||||
}}
|
||||
QMessageBox QLabel#qt_msgbox_label {{
|
||||
min-width: 200px;
|
||||
max-width: 560px;
|
||||
}}
|
||||
QMessageBox QLabel#qt_msgboxex_icon_label {{
|
||||
min-width: 28px;
|
||||
@ -140,31 +139,74 @@ def _apply(w: QWidget | None) -> None:
|
||||
|
||||
|
||||
def _prepare_qmessagebox(msg: QMessageBox) -> None:
|
||||
"""让提示框按内容换行并自适应宽高(避免固定宽度截断文字)。"""
|
||||
lbl = msg.findChild(QLabel, "qt_msgbox_label")
|
||||
if lbl is not None:
|
||||
lbl.setWordWrap(True)
|
||||
lbl.setTextFormat(Qt.TextFormat.PlainText)
|
||||
# 无空格长中文也能在边界处断行,而不是整段挤在一行被裁切
|
||||
try:
|
||||
lbl.setWordWrapMode(QTextOption.WrapMode.WrapAnywhere)
|
||||
except AttributeError:
|
||||
pass
|
||||
lbl.setMinimumWidth(200)
|
||||
lbl.setMaximumWidth(560)
|
||||
lbl.setSizePolicy(
|
||||
QSizePolicy.Policy.Preferred,
|
||||
QSizePolicy.Policy.MinimumExpanding,
|
||||
)
|
||||
lay = msg.layout()
|
||||
if lay is not None:
|
||||
lay.activate()
|
||||
"""让提示框按内容换行并自适应宽高。"""
|
||||
def _tune_labels() -> None:
|
||||
# QMessageBox 里通常有两段文字:
|
||||
# - qt_msgbox_label(主文本)
|
||||
# - qt_msgbox_informativelabel(辅助文本/更长的提示)
|
||||
# 部分平台会在显示/布局后才创建这些 label,因此这里支持延迟执行一次。
|
||||
for obj_name in ("qt_msgbox_label", "qt_msgbox_informativelabel"):
|
||||
w = msg.findChild(QWidget, obj_name)
|
||||
if w is None:
|
||||
continue
|
||||
|
||||
if hasattr(w, "setWordWrap"):
|
||||
try:
|
||||
w.setWordWrap(True) # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(w, "setTextFormat"):
|
||||
try:
|
||||
w.setTextFormat(Qt.TextFormat.PlainText) # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(w, "setWordWrapMode"):
|
||||
try:
|
||||
w.setWordWrapMode(QTextOption.WrapMode.WrapAnywhere) # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(w, "setTextElideMode"):
|
||||
try:
|
||||
w.setTextElideMode(Qt.TextElideMode.ElideNone) # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(w, "setMinimumWidth"):
|
||||
try:
|
||||
w.setMinimumWidth(0) # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(w, "setMaximumWidth"):
|
||||
try:
|
||||
w.setMaximumWidth(16777215) # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(w, "setSizePolicy"):
|
||||
try:
|
||||
w.setSizePolicy(
|
||||
QSizePolicy.Policy.Expanding,
|
||||
QSizePolicy.Policy.MinimumExpanding,
|
||||
) # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
pass
|
||||
msg.setSizePolicy(
|
||||
QSizePolicy.Policy.Preferred,
|
||||
QSizePolicy.Policy.Preferred,
|
||||
)
|
||||
msg.setMinimumSize(0, 0)
|
||||
msg.setMaximumSize(16777215, 16777215)
|
||||
|
||||
lay = msg.layout()
|
||||
if lay is not None:
|
||||
# QMessageBox 内部通常是 QGridLayout:第 1 列是文本列
|
||||
# 某些主题/平台会让文本列偏窄从而触发省略,这里显式拉伸该列并设置最小宽度。
|
||||
try:
|
||||
lay.setColumnStretch(1, 1)
|
||||
except Exception:
|
||||
pass
|
||||
lay.activate()
|
||||
_tune_labels()
|
||||
msg.adjustSize()
|
||||
QTimer.singleShot(0, lambda: (_tune_labels(), msg.adjustSize()))
|
||||
|
||||
|
||||
def question(
|
||||
|
||||
225
ui/updater.py
225
ui/updater.py
@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
软件自动更新模块
|
||||
- 检查版本:GET https://api.yunzer.cn/api/softwareupgrade/check?code=niumasortware
|
||||
@ -13,12 +14,10 @@ import base64
|
||||
from PyQt6.QtCore import QThread, pyqtSignal, QObject
|
||||
from PyQt6.QtWidgets import QProgressDialog, QMessageBox, QApplication
|
||||
import urllib.request
|
||||
import ui.dialog_style as dialog_style
|
||||
|
||||
|
||||
UPDATE_CHECK_URL = "https://api.yunzer.cn/api/softwareupgrade/check?code=niumasortware"
|
||||
APP_NAME = "CleanDesktopOrganizer"
|
||||
# schtasks 的任务名在不同环境下可能需要/不需要前导 "\",这里两种都尝试
|
||||
UPDATE_TASK_NAMES = [r"\CleanDesktopOrganizer\Update", r"CleanDesktopOrganizer\Update"]
|
||||
|
||||
|
||||
def _current_version() -> str:
|
||||
@ -45,7 +44,6 @@ class _CheckWorker(QThread):
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
import json
|
||||
with urllib.request.urlopen(UPDATE_CHECK_URL, timeout=10) as resp:
|
||||
body = json.loads(resp.read().decode())
|
||||
if body.get("code") == 200:
|
||||
@ -72,7 +70,6 @@ class _DownloadWorker(QThread):
|
||||
if total_size > 0:
|
||||
pct = min(100, int(count * block_size * 100 / total_size))
|
||||
self.progress.emit(pct)
|
||||
|
||||
urllib.request.urlretrieve(self._url, self._dest, _reporthook)
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(self._dest)
|
||||
@ -80,165 +77,26 @@ class _DownloadWorker(QThread):
|
||||
self.error.emit(str(e))
|
||||
|
||||
|
||||
def _programdata_dir() -> str:
|
||||
base = os.environ.get("PROGRAMDATA") or r"C:\ProgramData"
|
||||
return os.path.join(base, APP_NAME)
|
||||
|
||||
|
||||
def _ensure_dir(p: str):
|
||||
try:
|
||||
os.makedirs(p, exist_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _update_work_dir() -> str:
|
||||
"""
|
||||
全局安装场景下,Program Files 不可写;更新文件与请求文件统一放 ProgramData。
|
||||
该目录应由安装器创建并赋予 Users Modify 权限。
|
||||
"""
|
||||
d = os.path.join(_programdata_dir(), "updates")
|
||||
_ensure_dir(d)
|
||||
if os.path.isdir(d):
|
||||
return d
|
||||
return tempfile.gettempdir()
|
||||
|
||||
|
||||
def _request_file_path() -> str:
|
||||
d = _programdata_dir()
|
||||
_ensure_dir(d)
|
||||
return os.path.join(d, "update_request.json")
|
||||
|
||||
|
||||
def _task_exists(name: str) -> bool:
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["schtasks", "/Query", "/TN", name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
return r.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _run_update_task() -> bool:
|
||||
for name in UPDATE_TASK_NAMES:
|
||||
if not _task_exists(name):
|
||||
continue
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["schtasks", "/Run", "/TN", name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
if r.returncode == 0:
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
|
||||
def _run_update_helper_elevated() -> bool:
|
||||
"""
|
||||
当计划任务不存在时的兜底:以管理员权限运行 update_helper.exe。
|
||||
这会触发 UAC(无法完全静默),但能保证更新能完成。
|
||||
"""
|
||||
if sys.platform != "win32":
|
||||
return False
|
||||
try:
|
||||
helper = os.path.join(os.path.dirname(sys.executable), "update_helper.exe")
|
||||
if not os.path.isfile(helper):
|
||||
return False
|
||||
# ShellExecuteW 返回值 > 32 表示成功启动
|
||||
r = ctypes.windll.shell32.ShellExecuteW(None, "runas", helper, None, None, 0)
|
||||
return int(r) > 32
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _run_elevated_copy_and_restart(src: str, dst: str, pid: int) -> bool:
|
||||
"""
|
||||
不依赖 update_helper.exe 的兜底方案:
|
||||
直接用管理员权限启动 PowerShell,等待原进程退出后覆盖 dst 并重启。
|
||||
"""
|
||||
if sys.platform != "win32":
|
||||
return False
|
||||
try:
|
||||
ps = rf"""
|
||||
$pid_target = {int(pid)}
|
||||
$src = '{str(src).replace("'", "''")}'
|
||||
$dst = '{str(dst).replace("'", "''")}'
|
||||
|
||||
$waited = 0
|
||||
while ((Get-Process -Id $pid_target -ErrorAction SilentlyContinue) -and $waited -lt 60) {{
|
||||
Start-Sleep -Milliseconds 500
|
||||
$waited += 0.5
|
||||
}}
|
||||
|
||||
$ok = $false
|
||||
for ($i = 0; $i -lt 20; $i++) {{
|
||||
try {{
|
||||
Copy-Item -Path $src -Destination $dst -Force
|
||||
$ok = $true
|
||||
break
|
||||
}} catch {{
|
||||
Start-Sleep -Milliseconds 800
|
||||
}}
|
||||
}}
|
||||
|
||||
if ($ok) {{
|
||||
Remove-Item $src -Force -ErrorAction SilentlyContinue
|
||||
Start-Process $dst
|
||||
}}
|
||||
"""
|
||||
# PowerShell -EncodedCommand 需要 UTF-16LE + base64
|
||||
enc = base64.b64encode(ps.encode("utf-16le")).decode("ascii")
|
||||
r = ctypes.windll.shell32.ShellExecuteW(
|
||||
None,
|
||||
"runas",
|
||||
"powershell",
|
||||
f"-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand {enc}",
|
||||
None,
|
||||
0,
|
||||
)
|
||||
return int(r) > 32
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _replace_and_restart(new_exe: str):
|
||||
"""
|
||||
onedir 模式:只替换 exe 本身,dll 等文件不变。
|
||||
用 PowerShell 后台脚本等待原进程退出后替换并重启,完全无窗口。
|
||||
"""
|
||||
if not getattr(sys, "frozen", False):
|
||||
subprocess.Popen([new_exe])
|
||||
QApplication.quit()
|
||||
return
|
||||
|
||||
current_exe = sys.executable
|
||||
pid = os.getpid()
|
||||
pid = os.getpid()
|
||||
|
||||
ps_script = f"""
|
||||
$pid_target = {pid}
|
||||
$src = '{new_exe.replace(chr(92), chr(92)*2)}'
|
||||
$dst = '{current_exe.replace(chr(92), chr(92)*2)}'
|
||||
|
||||
# 等待原进程退出,最多 30 秒
|
||||
$waited = 0
|
||||
while ((Get-Process -Id $pid_target -ErrorAction SilentlyContinue) -and $waited -lt 30) {{
|
||||
Start-Sleep -Milliseconds 500
|
||||
$waited += 0.5
|
||||
}}
|
||||
|
||||
# 重试覆盖,最多 10 次
|
||||
$ok = $false
|
||||
for ($i = 0; $i -lt 10; $i++) {{
|
||||
try {{
|
||||
@ -255,7 +113,6 @@ if ($ok) {{
|
||||
Start-Process $dst
|
||||
}}
|
||||
"""
|
||||
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
delete=False, suffix=".ps1", mode="w", encoding="utf-8"
|
||||
)
|
||||
@ -263,11 +120,9 @@ if ($ok) {{
|
||||
tmp.close()
|
||||
|
||||
subprocess.Popen(
|
||||
[
|
||||
"powershell", "-WindowStyle", "Hidden",
|
||||
"-NonInteractive", "-ExecutionPolicy", "Bypass",
|
||||
"-File", tmp.name,
|
||||
],
|
||||
["powershell", "-WindowStyle", "Hidden",
|
||||
"-NonInteractive", "-ExecutionPolicy", "Bypass",
|
||||
"-File", tmp.name],
|
||||
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
QApplication.quit()
|
||||
@ -292,7 +147,7 @@ class Updater(QObject):
|
||||
|
||||
if not _is_newer(latest, current):
|
||||
if not self._silent:
|
||||
QMessageBox.information(self._parent_widget, "检查更新", f"当前已是最新版本 v{current}")
|
||||
dialog_style.information(self._parent_widget, "检查更新", f"当前已是最新版本 v{current}")
|
||||
return
|
||||
|
||||
notes = data.get("releaseNotes", "") or ""
|
||||
@ -301,24 +156,21 @@ class Updater(QObject):
|
||||
msg += f"\n\n更新内容:\n{notes}"
|
||||
msg += "\n\n是否立即下载更新?"
|
||||
|
||||
ret = QMessageBox.question(
|
||||
self._parent_widget, "发现新版本", msg,
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.Yes,
|
||||
)
|
||||
ret = dialog_style.question(self._parent_widget, "发现新版本", msg)
|
||||
if ret != QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
|
||||
self._url = url
|
||||
self._download(url)
|
||||
|
||||
def _on_check_error(self, err: str):
|
||||
if not self._silent:
|
||||
QMessageBox.warning(self._parent_widget, "检查更新失败", f"无法连接更新服务器:\n{err}")
|
||||
dialog_style.warning(self._parent_widget, "检查更新失败", f"无法连接更新服务器:\n{err}")
|
||||
|
||||
def _download(self, url: str):
|
||||
# 全局安装:不要下载到安装目录(通常在 Program Files,不可写)
|
||||
dest_dir = _update_work_dir() if getattr(sys, "frozen", False) else tempfile.gettempdir()
|
||||
if getattr(sys, "frozen", False):
|
||||
dest_dir = os.path.dirname(sys.executable)
|
||||
else:
|
||||
dest_dir = tempfile.gettempdir()
|
||||
dest = os.path.join(dest_dir, "_update_new.exe")
|
||||
|
||||
self._progress = QProgressDialog("正在下载更新...", "取消", 0, 100, self._parent_widget)
|
||||
@ -335,57 +187,8 @@ class Updater(QObject):
|
||||
|
||||
def _on_download_done(self, path: str):
|
||||
self._progress.close()
|
||||
# frozen 且全局安装:交给计划任务(最高权限)覆盖安装目录 exe
|
||||
if getattr(sys, "frozen", False):
|
||||
try:
|
||||
req = {
|
||||
"src": path,
|
||||
"dst": sys.executable,
|
||||
"pid": os.getpid(),
|
||||
"restart": True,
|
||||
}
|
||||
with open(_request_file_path(), "w", encoding="utf-8") as f:
|
||||
json.dump(req, f, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self._parent_widget, "更新失败", f"无法准备更新任务:\n{e}")
|
||||
return
|
||||
|
||||
if _run_update_task():
|
||||
QApplication.quit()
|
||||
return
|
||||
|
||||
# 兜底策略 1:计划任务不存在时,提示并可通过 UAC 直接运行 update_helper.exe 完成一次更新
|
||||
if _run_update_helper_elevated():
|
||||
QApplication.quit()
|
||||
return
|
||||
|
||||
# 兜底策略 1.5:如果安装目录里没有 update_helper.exe,则直接用管理员 PowerShell 完成覆盖
|
||||
if _run_elevated_copy_and_restart(path, sys.executable, os.getpid()):
|
||||
QApplication.quit()
|
||||
return
|
||||
|
||||
# 兜底策略 2:如果安装目录可写(非常少见),尝试旧的自替换方式
|
||||
try:
|
||||
can_write = os.access(sys.executable, os.W_OK)
|
||||
except Exception:
|
||||
can_write = False
|
||||
if can_write:
|
||||
_replace_and_restart(path)
|
||||
return
|
||||
|
||||
QMessageBox.critical(
|
||||
self._parent_widget,
|
||||
"更新失败",
|
||||
"已下载更新,但未找到/无法运行更新计划任务(需要管理员权限)。\n\n"
|
||||
"请尝试:\n"
|
||||
"1) 以管理员身份重新安装一次安装包(用于创建更新计划任务)\n"
|
||||
"2) 或在“任务计划程序”检查是否存在任务:CleanDesktopOrganizer\\Update\n"
|
||||
"3) 也可尝试用管理员权限运行 update_helper.exe 完成一次更新",
|
||||
)
|
||||
return
|
||||
|
||||
_replace_and_restart(path)
|
||||
|
||||
def _on_download_error(self, err: str):
|
||||
self._progress.close()
|
||||
QMessageBox.critical(self._parent_widget, "下载失败", f"下载更新失败:\n{err}")
|
||||
dialog_style.warning(self._parent_widget, "下载失败", f"下载更新失败:\n{err}")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user