完成软件更新功能

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

View File

@ -49,7 +49,7 @@ Name: "{autodesktop}\牛马软件柜"; Filename: "{app}\niumasoftware.exe"; Task
Filename: "{app}\niumasoftware.exe"; Description: "运行 牛马软件柜"; Flags: nowait postinstall skipifsilent Filename: "{app}\niumasoftware.exe"; Description: "运行 牛马软件柜"; Flags: nowait postinstall skipifsilent
[UninstallRun] [UninstallRun]
Filename: "{sys}\schtasks.exe"; Parameters: "/Delete /TN ""CleanDesktopOrganizer\Update"" /F"; Flags: runhidden Filename: "{sys}\schtasks.exe"; Parameters: "/Delete /TN ""\CleanDesktopOrganizer\Update"" /F"; Flags: runhidden
[Code] [Code]
procedure CreateUpdateTask(); procedure CreateUpdateTask();
@ -60,7 +60,7 @@ begin
// 计划任务:以 SYSTEM 运行,按需触发(/SC ONDEMAND // 计划任务:以 SYSTEM 运行,按需触发(/SC ONDEMAND
// 注意TR 不带参数update_helper.exe 会读取 ProgramData 请求文件 // 注意TR 不带参数update_helper.exe 会读取 ProgramData 请求文件
Cmd := Cmd :=
'/Create /F /TN "CleanDesktopOrganizer\Update" ' + '/Create /F /TN "\CleanDesktopOrganizer\Update" ' +
'/RU "SYSTEM" /RL HIGHEST /SC ONDEMAND ' + '/RU "SYSTEM" /RL HIGHEST /SC ONDEMAND ' +
'/TR "' + ExpandConstant('{app}\update_helper.exe') + '"'; '/TR "' + ExpandConstant('{app}\update_helper.exe') + '"';

View File

@ -48,7 +48,7 @@ Name: "{autodesktop}\牛马软件柜"; Filename: "{app}\niumasoftware.exe"; Task
Filename: "{app}\niumasoftware.exe"; Description: "运行 牛马软件柜"; Flags: nowait postinstall skipifsilent Filename: "{app}\niumasoftware.exe"; Description: "运行 牛马软件柜"; Flags: nowait postinstall skipifsilent
[UninstallRun] [UninstallRun]
Filename: "{sys}\schtasks.exe"; Parameters: "/Delete /TN ""CleanDesktopOrganizer\Update"" /F"; Flags: runhidden Filename: "{sys}\schtasks.exe"; Parameters: "/Delete /TN ""\CleanDesktopOrganizer\Update"" /F"; Flags: runhidden
[Code] [Code]
procedure CreateUpdateTask(); procedure CreateUpdateTask();
@ -59,7 +59,7 @@ begin
// 计划任务:以 SYSTEM 运行,按需触发(/SC ONDEMAND // 计划任务:以 SYSTEM 运行,按需触发(/SC ONDEMAND
// 注意TR 不带参数update_helper.exe 会读取 ProgramData 请求文件 // 注意TR 不带参数update_helper.exe 会读取 ProgramData 请求文件
Cmd := Cmd :=
'/Create /F /TN "CleanDesktopOrganizer\Update" ' + '/Create /F /TN "\CleanDesktopOrganizer\Update" ' +
'/RU "SYSTEM" /RL HIGHEST /SC ONDEMAND ' + '/RU "SYSTEM" /RL HIGHEST /SC ONDEMAND ' +
'/TR "' + ExpandConstant('{app}\update_helper.exe') + '"'; '/TR "' + ExpandConstant('{app}\update_helper.exe') + '"';

View File

@ -8,6 +8,8 @@ import sys
import subprocess import subprocess
import tempfile import tempfile
import json import json
import ctypes
import base64
from PyQt6.QtCore import QThread, pyqtSignal, QObject from PyQt6.QtCore import QThread, pyqtSignal, QObject
from PyQt6.QtWidgets import QProgressDialog, QMessageBox, QApplication from PyQt6.QtWidgets import QProgressDialog, QMessageBox, QApplication
import urllib.request import urllib.request
@ -15,7 +17,8 @@ import urllib.request
UPDATE_CHECK_URL = "https://api.yunzer.cn/api/softwareupgrade/check?code=niumasortware" UPDATE_CHECK_URL = "https://api.yunzer.cn/api/softwareupgrade/check?code=niumasortware"
APP_NAME = "CleanDesktopOrganizer" APP_NAME = "CleanDesktopOrganizer"
UPDATE_TASK_NAME = r"CleanDesktopOrganizer\Update" # schtasks 的任务名在不同环境下可能需要/不需要前导 "\",这里两种都尝试
UPDATE_TASK_NAMES = [r"\CleanDesktopOrganizer\Update", r"CleanDesktopOrganizer\Update"]
def _current_version() -> str: def _current_version() -> str:
@ -107,10 +110,10 @@ def _request_file_path() -> str:
return os.path.join(d, "update_request.json") return os.path.join(d, "update_request.json")
def _run_update_task() -> bool: def _task_exists(name: str) -> bool:
try: try:
r = subprocess.run( r = subprocess.run(
["schtasks", "/Run", "/TN", UPDATE_TASK_NAME], ["schtasks", "/Query", "/TN", name],
capture_output=True, capture_output=True,
text=True, text=True,
encoding="utf-8", encoding="utf-8",
@ -122,6 +125,94 @@ def _run_update_task() -> bool:
return False 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): def _replace_and_restart(new_exe: str):
""" """
onedir 模式只替换 exe 本身dll 等文件不变 onedir 模式只替换 exe 本身dll 等文件不变
@ -263,10 +354,36 @@ class Updater(QObject):
QApplication.quit() QApplication.quit()
return 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) _replace_and_restart(path)
return return
QMessageBox.critical(
self._parent_widget,
"更新失败",
"已下载更新,但未找到/无法运行更新计划任务(需要管理员权限)。\n\n"
"请尝试:\n"
"1) 以管理员身份重新安装一次安装包(用于创建更新计划任务)\n"
"2) 或在“任务计划程序”检查是否存在任务CleanDesktopOrganizer\\Update\n"
"3) 也可尝试用管理员权限运行 update_helper.exe 完成一次更新",
)
return
_replace_and_restart(path) _replace_and_restart(path)
def _on_download_error(self, err: str): def _on_download_error(self, err: str):

View File

@ -18,6 +18,7 @@ import time
APP_NAME = "CleanDesktopOrganizer" APP_NAME = "CleanDesktopOrganizer"
REQUEST_FILE = "update_request.json" REQUEST_FILE = "update_request.json"
LOG_FILE = "update.log"
def _programdata_dir() -> str: def _programdata_dir() -> str:
@ -29,6 +30,16 @@ def _request_path() -> str:
return os.path.join(_programdata_dir(), REQUEST_FILE) return os.path.join(_programdata_dir(), REQUEST_FILE)
def _log(msg: str):
try:
os.makedirs(_programdata_dir(), exist_ok=True)
p = os.path.join(_programdata_dir(), LOG_FILE)
with open(p, "a", encoding="utf-8") as f:
f.write(msg.rstrip() + "\n")
except Exception:
pass
def _read_request() -> dict: def _read_request() -> dict:
p = _request_path() p = _request_path()
with open(p, "r", encoding="utf-8") as f: with open(p, "r", encoding="utf-8") as f:
@ -79,6 +90,7 @@ def main() -> int:
except FileNotFoundError: except FileNotFoundError:
return 0 return 0
except Exception: except Exception:
_log("ERROR: cannot read request file")
return 2 return 2
src = str(req.get("src") or "") src = str(req.get("src") or "")
@ -87,8 +99,11 @@ def main() -> int:
restart = bool(req.get("restart", True)) restart = bool(req.get("restart", True))
if not src or not dst: if not src or not dst:
_log(f"ERROR: bad request src={src!r} dst={dst!r} pid={pid}")
return 3 return 3
_log(f"START: pid={pid} src={src} dst={dst}")
# 等待主程序退出,最多 60 秒 # 等待主程序退出,最多 60 秒
waited = 0.0 waited = 0.0
while _pid_exists(pid) and waited < 60.0: while _pid_exists(pid) and waited < 60.0:
@ -96,6 +111,7 @@ def main() -> int:
waited += 0.5 waited += 0.5
ok = _copy_with_retries(src, dst, retries=15) ok = _copy_with_retries(src, dst, retries=15)
_log(f"COPY: ok={ok}")
try: try:
os.remove(src) os.remove(src)
except Exception: except Exception:
@ -110,10 +126,13 @@ def main() -> int:
if ok and restart: if ok and restart:
try: try:
subprocess.Popen([dst], creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW) subprocess.Popen([dst], creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW)
_log("RESTART: started")
except Exception: except Exception:
_log("RESTART: failed")
pass pass
return 0 return 0
_log("DONE: failed")
return 4 return 4