完成软件更新功能
This commit is contained in:
+122
-5
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user