完成软件更新功能
This commit is contained in:
parent
ff8aa0269d
commit
948d23074a
@ -49,7 +49,7 @@ Name: "{autodesktop}\牛马软件柜"; Filename: "{app}\niumasoftware.exe"; Task
|
||||
Filename: "{app}\niumasoftware.exe"; Description: "运行 牛马软件柜"; Flags: nowait postinstall skipifsilent
|
||||
|
||||
[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]
|
||||
procedure CreateUpdateTask();
|
||||
@ -60,7 +60,7 @@ begin
|
||||
// 计划任务:以 SYSTEM 运行,按需触发(/SC ONDEMAND)
|
||||
// 注意:TR 不带参数,update_helper.exe 会读取 ProgramData 请求文件
|
||||
Cmd :=
|
||||
'/Create /F /TN "CleanDesktopOrganizer\Update" ' +
|
||||
'/Create /F /TN "\CleanDesktopOrganizer\Update" ' +
|
||||
'/RU "SYSTEM" /RL HIGHEST /SC ONDEMAND ' +
|
||||
'/TR "' + ExpandConstant('{app}\update_helper.exe') + '"';
|
||||
|
||||
|
||||
@ -48,7 +48,7 @@ Name: "{autodesktop}\牛马软件柜"; Filename: "{app}\niumasoftware.exe"; Task
|
||||
Filename: "{app}\niumasoftware.exe"; Description: "运行 牛马软件柜"; Flags: nowait postinstall skipifsilent
|
||||
|
||||
[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]
|
||||
procedure CreateUpdateTask();
|
||||
@ -59,7 +59,7 @@ begin
|
||||
// 计划任务:以 SYSTEM 运行,按需触发(/SC ONDEMAND)
|
||||
// 注意:TR 不带参数,update_helper.exe 会读取 ProgramData 请求文件
|
||||
Cmd :=
|
||||
'/Create /F /TN "CleanDesktopOrganizer\Update" ' +
|
||||
'/Create /F /TN "\CleanDesktopOrganizer\Update" ' +
|
||||
'/RU "SYSTEM" /RL HIGHEST /SC ONDEMAND ' +
|
||||
'/TR "' + ExpandConstant('{app}\update_helper.exe') + '"';
|
||||
|
||||
|
||||
125
ui/updater.py
125
ui/updater.py
@ -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,10 +354,36 @@ class Updater(QObject):
|
||||
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):
|
||||
|
||||
@ -18,6 +18,7 @@ import time
|
||||
|
||||
APP_NAME = "CleanDesktopOrganizer"
|
||||
REQUEST_FILE = "update_request.json"
|
||||
LOG_FILE = "update.log"
|
||||
|
||||
|
||||
def _programdata_dir() -> str:
|
||||
@ -29,6 +30,16 @@ def _request_path() -> str:
|
||||
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:
|
||||
p = _request_path()
|
||||
with open(p, "r", encoding="utf-8") as f:
|
||||
@ -79,6 +90,7 @@ def main() -> int:
|
||||
except FileNotFoundError:
|
||||
return 0
|
||||
except Exception:
|
||||
_log("ERROR: cannot read request file")
|
||||
return 2
|
||||
|
||||
src = str(req.get("src") or "")
|
||||
@ -87,8 +99,11 @@ def main() -> int:
|
||||
restart = bool(req.get("restart", True))
|
||||
|
||||
if not src or not dst:
|
||||
_log(f"ERROR: bad request src={src!r} dst={dst!r} pid={pid}")
|
||||
return 3
|
||||
|
||||
_log(f"START: pid={pid} src={src} dst={dst}")
|
||||
|
||||
# 等待主程序退出,最多 60 秒
|
||||
waited = 0.0
|
||||
while _pid_exists(pid) and waited < 60.0:
|
||||
@ -96,6 +111,7 @@ def main() -> int:
|
||||
waited += 0.5
|
||||
|
||||
ok = _copy_with_retries(src, dst, retries=15)
|
||||
_log(f"COPY: ok={ok}")
|
||||
try:
|
||||
os.remove(src)
|
||||
except Exception:
|
||||
@ -110,10 +126,13 @@ def main() -> int:
|
||||
if ok and restart:
|
||||
try:
|
||||
subprocess.Popen([dst], creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW)
|
||||
_log("RESTART: started")
|
||||
except Exception:
|
||||
_log("RESTART: failed")
|
||||
pass
|
||||
return 0
|
||||
|
||||
_log("DONE: failed")
|
||||
return 4
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user