完成软件更新功能

This commit is contained in:
2026-04-08 00:36:09 +08:00
parent ff8aa0269d
commit 948d23074a
4 changed files with 145 additions and 9 deletions
+122 -5
View File
@@ -8,6 +8,8 @@ import sys
import subprocess
import tempfile
import json
import ctypes
import base64
from PyQt6.QtCore import QThread, pyqtSignal, QObject
from PyQt6.QtWidgets import QProgressDialog, QMessageBox, QApplication
import urllib.request
@@ -15,7 +17,8 @@ import urllib.request
UPDATE_CHECK_URL = "https://api.yunzer.cn/api/softwareupgrade/check?code=niumasortware"
APP_NAME = "CleanDesktopOrganizer"
UPDATE_TASK_NAME = r"CleanDesktopOrganizer\Update"
# schtasks 的任务名在不同环境下可能需要/不需要前导 "\",这里两种都尝试
UPDATE_TASK_NAMES = [r"\CleanDesktopOrganizer\Update", r"CleanDesktopOrganizer\Update"]
def _current_version() -> str:
@@ -107,10 +110,10 @@ def _request_file_path() -> str:
return os.path.join(d, "update_request.json")
def _run_update_task() -> bool:
def _task_exists(name: str) -> bool:
try:
r = subprocess.run(
["schtasks", "/Run", "/TN", UPDATE_TASK_NAME],
["schtasks", "/Query", "/TN", name],
capture_output=True,
text=True,
encoding="utf-8",
@@ -122,6 +125,94 @@ def _run_update_task() -> bool:
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 等文件不变。
@@ -263,8 +354,34 @@ class Updater(QObject):
QApplication.quit()
return
# 兜底:如果计划任务不存在/失败,尝试旧的自替换方式(仅当安装目录可写时有效)
_replace_and_restart(path)
# 兜底策略 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)