From 52aee3b5d58fd677c80eeb43a757e79be77b38f7 Mon Sep 17 00:00:00 2001 From: yuncishu <3053860096@qq.com> Date: Sat, 31 Oct 2026 23:52:53 +0800 Subject: [PATCH] fix: update installer never ran, app closed without updating (1.2.6) The installer was spawned with DETACHED_PROCESS, which makes the host process exit as soon as it starts, so the script body never executed: no log, no file swap, no restart. Only CREATE_NO_WINDOW is needed to keep the window hidden. - stop both the bootloader and the Python child of a onefile build - verify the script process is still alive before quitting the app - write logs to updates/logs and always relaunch on failure --- src/updater.py | 274 ++++++++++++++++++++++++++++++------------------- src/version.py | 2 +- 2 files changed, 170 insertions(+), 106 deletions(-) diff --git a/src/updater.py b/src/updater.py index 8332745..8778faa 100644 --- a/src/updater.py +++ b/src/updater.py @@ -5,6 +5,7 @@ import re import subprocess import sys import tempfile +import time import urllib.parse import requests @@ -102,9 +103,14 @@ def _safe_suffix(url): return suffix if suffix in (".exe", ".msi", ".zip") else ".exe" -def _offset_dir(version): +CREATE_NO_WINDOW = 0x08000000 + + +def _offset_dir(sub=""): base = os.environ.get("LOCALAPPDATA") or tempfile.gettempdir() path = os.path.join(base, "NeteasePlayer", "updates") + if sub: + path = os.path.join(path, sub) os.makedirs(path, exist_ok=True) return path @@ -119,121 +125,146 @@ def powershell_exe(): return path if os.path.exists(path) else "powershell.exe" -def build_install_script(current, downloaded, pid, digest=""): - """生成 Unicode 安全的更新脚本:校验副本后再交换文件,失败则从下载路径启动。""" +def build_install_script(current, downloaded, pid, digest="", parent_pid=None): + """生成 Unicode 安全的更新脚本:校验副本后再交换文件,任何失败路径都会重新拉起程序。""" current = os.path.abspath(current) downloaded = os.path.abspath(downloaded) log_dir = _offset_dir("logs") log_path = os.path.join(log_dir, "update.log") + pids = [int(pid)] + if parent_pid and int(parent_pid) > 100 and int(parent_pid) not in pids: + # PyInstaller 单文件有两个进程:bootloader 才是持有 exe 文件的那个 + pids.append(int(parent_pid)) script = r""" $ErrorActionPreference = 'Continue' -$src = {src} -$dst = {dst} -$expected = {expected} -$logDir = {log_dir} -$log = {log} +$src = __SRC__ +$dst = __DST__ +$expected = __EXPECTED__ +$logDir = __LOG_DIR__ +$log = __LOG__ New-Item -ItemType Directory -Force -Path $logDir | Out-Null -function Write-UpdateLog($message) {{ - try {{ +function Write-UpdateLog($message) { + try { Add-Content -LiteralPath $log -Encoding UTF8 -Value ("[" + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + "] " + $message) - }} catch {{}} -}} -Write-UpdateLog "update start; pid={pid}" + } catch {} +} +function Start-App($path) { + if (-not $path) { return $false } + if (-not (Test-Path -LiteralPath $path)) { + Write-UpdateLog ("missing target: " + $path) + return $false + } + try { + Start-Process -FilePath $path -WorkingDirectory (Split-Path -Parent $path) -ErrorAction Stop + Write-UpdateLog ("started: " + $path) + return $true + } catch { + Write-UpdateLog ("start failed: " + $path + " :: " + $_.Exception.Message) + return $false + } +} + +Write-UpdateLog "=== update begin; pids=__PIDS_TEXT__ ===" +Write-UpdateLog ("src=" + $src) +Write-UpdateLog ("dst=" + $dst) Start-Sleep -Seconds 2 -try {{ Stop-Process -Id {pid} -Force -ErrorAction SilentlyContinue }} catch {{}} -Start-Sleep -Milliseconds 800 +foreach ($p in @(__PIDS__)) { + try { Stop-Process -Id $p -Force -ErrorAction SilentlyContinue } catch {} +} +Start-Sleep -Milliseconds 1200 -$dstDir = Split-Path -Parent $dst $staged = $dst + ".update" $backup = $dst + ".bak" -Remove-Item -LiteralPath $staged, $backup -Force -ErrorAction SilentlyContinue +Remove-Item -LiteralPath $staged -Force -ErrorAction SilentlyContinue + $verified = $false -for ($i = 0; $i -lt 30; $i++) {{ - try {{ +for ($i = 0; $i -lt 30; $i++) { + try { Copy-Item -LiteralPath $src -Destination $staged -Force -ErrorAction Stop - if ($expected) {{ + if ($expected) { $actual = (Get-FileHash -LiteralPath $staged -Algorithm SHA256).Hash - if ($actual -ne $expected) {{ throw "SHA256 mismatch" }} - }} + if ($actual -ne $expected) { throw "SHA256 mismatch" } + } $verified = $true Write-UpdateLog "staged copy verified" break - }} catch {{ + } catch { Write-UpdateLog ("stage attempt " + $i + " failed: " + $_.Exception.Message) Start-Sleep -Milliseconds 500 - }} -}} -if (-not $verified) {{ - Write-UpdateLog "stage failed; launching downloaded executable" - if (Test-Path -LiteralPath $src) {{ - Start-Process -FilePath $src -WorkingDirectory (Split-Path -Parent $src) - }} + } +} + +if (-not $verified) { + Write-UpdateLog "stage failed; launching downloaded copy" + if (-not (Start-App $src)) { Start-App $dst } + Remove-Item -LiteralPath $staged -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue exit -}} +} $swapped = $false -for ($i = 0; $i -lt 30; $i++) {{ - try {{ - if (Test-Path -LiteralPath $dst) {{ +for ($i = 0; $i -lt 30; $i++) { + try { + if (Test-Path -LiteralPath $dst) { Move-Item -LiteralPath $dst -Destination $backup -Force -ErrorAction Stop - }} + } $swapped = $true break - }} catch {{ + } catch { Write-UpdateLog ("rename old attempt " + $i + " failed: " + $_.Exception.Message) Start-Sleep -Milliseconds 500 - }} -}} -if (-not $swapped) {{ - Write-UpdateLog "cannot rename running exe; launching staged copy in place" - try {{ - Move-Item -LiteralPath $staged -Destination $dst -Force -ErrorAction Stop - Start-Process -FilePath $dst -WorkingDirectory $dstDir - }} catch {{ - Write-UpdateLog ("launch staged fallback failed: " + $_.Exception.Message) - if (Test-Path -LiteralPath $src) {{ - Start-Process -FilePath $src -WorkingDirectory (Split-Path -Parent $src) - }} - }} + } +} + +if (-not $swapped) { + Write-UpdateLog "cannot rename running exe; starting downloaded copy" + if (-not (Start-App $src)) { Start-App $dst } + Remove-Item -LiteralPath $staged -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue exit -}} +} -try {{ +$installed = $false +try { Move-Item -LiteralPath $staged -Destination $dst -Force -ErrorAction Stop - Start-Process -FilePath $dst -WorkingDirectory $dstDir -ErrorAction Stop - Write-UpdateLog "new version started" - for ($i = 0; $i -lt 15; $i++) {{ - try {{ - if (Test-Path -LiteralPath $backup) {{ - [System.IO.File]::Delete($backup) - }} - Write-UpdateLog "old backup cleaned" - break - }} catch {{ - Write-UpdateLog ("cleanup attempt " + $i + " failed: " + $_.Exception.Message) - Start-Sleep -Seconds 1 - }} - }} -}} catch {{ - Write-UpdateLog ("swap/start failed: " + $_.Exception.Message) - if ((Test-Path -LiteralPath $backup) -and -not (Test-Path -LiteralPath $dst)) {{ + $installed = $true + Write-UpdateLog "new file in place" +} catch { + Write-UpdateLog ("swap failed: " + $_.Exception.Message) + if ((Test-Path -LiteralPath $backup) -and -not (Test-Path -LiteralPath $dst)) { Move-Item -LiteralPath $backup -Destination $dst -Force -ErrorAction SilentlyContinue - }} - if (Test-Path -LiteralPath $src) {{ - Start-Process -FilePath $src -WorkingDirectory (Split-Path -Parent $src) - }} -}} + Write-UpdateLog "rolled back from backup" + } +} + +if ($installed) { + if (-not (Start-App $dst)) { + Write-UpdateLog "new version did not start; trying downloaded copy" + Start-App $src + } +} else { + if (-not (Start-App $dst)) { Start-App $src } +} + +for ($i = 0; $i -lt 20; $i++) { + if (-not (Test-Path -LiteralPath $backup)) { break } + try { + [System.IO.File]::Delete($backup) + Write-UpdateLog "old backup cleaned" + break + } catch { + Start-Sleep -Milliseconds 500 + } +} +Write-UpdateLog "=== update end ===" Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue -""".format( - src=_ps_quote(downloaded), - dst=_ps_quote(current), - expected=_ps_quote(digest or ""), - log_dir=_ps_quote(log_dir), - log=_ps_quote(log_path), - pid=int(pid), - ) +""".replace("__SRC__", _ps_quote(downloaded)) \ + .replace("__DST__", _ps_quote(current)) \ + .replace("__EXPECTED__", _ps_quote(digest or "")) \ + .replace("__LOG_DIR__", _ps_quote(log_dir)) \ + .replace("__LOG__", _ps_quote(log_path)) \ + .replace("__PIDS_TEXT__", ",".join(str(p) for p in pids)) \ + .replace("__PIDS__", ", ".join(str(p) for p in pids)) return script, log_path @@ -476,8 +507,8 @@ class UpdateDialog(QDialog): if action == "retry": self.primary.setProperty("action", "") self.start_download() - elif action == "install": - self.install() + elif action == "page": + self._open_page() else: self.install() @@ -491,41 +522,74 @@ class UpdateDialog(QDialog): self.status.setText("安装包已打开") return current = os.path.abspath(sys.executable) - script, log_path = build_install_script( - current, path, os.getpid(), self.info.get("sha256") or "") - script_path = os.path.join(tempfile.gettempdir(), "netease_player_update.ps1") - with open(script_path, "w", encoding="utf-8-sig") as handle: - handle.write(script) + parent_pid = None try: - subprocess.Popen( + candidate = os.getppid() + if candidate and candidate > 100: + parent_pid = candidate + except Exception: # noqa: BLE001 + parent_pid = None + script, log_path = build_install_script( + current, path, os.getpid(), self.info.get("sha256") or "", parent_pid) + # 放在更新目录而不是 Temp,避免脚本尚未启动就被系统清理 + script_path = os.path.join(_offset_dir("logs"), "install.ps1") + try: + with open(script_path, "w", encoding="utf-8-sig") as handle: + handle.write(script) + except OSError as exc: + self._manual_install(path, "无法写入更新脚本:%s" % exc) + return + try: + proc = subprocess.Popen( [ powershell_exe(), "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-File", script_path, ], - creationflags=0x08000000 | 0x00000008, + creationflags=CREATE_NO_WINDOW, close_fds=True, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, ) except Exception as exc: # noqa: BLE001 - # 更新脚本无法启动时,至少直接运行已下载的新版本 - try: - subprocess.Popen( - [path], - cwd=os.path.dirname(path), - creationflags=0x00000008, - close_fds=True, - ) - except Exception: - self.status.setText("无法启动更新程序:%s" % exc) - self.primary.setText("重试更新") - self.primary.setEnabled(True) - self.primary.setProperty("action", "install") - return - QApplication.quit() + self._manual_install(path, "无法启动更新程序:%s" % exc) + return + # PowerShell 被安全策略或参数问题打断时会立刻退出,必须确认它真的活着 + deadline = time.time() + 1.5 + while time.time() < deadline: + if proc.poll() is not None: + break + time.sleep(0.1) + if proc.poll() is not None: + self._manual_install(path, "更新脚本未能运行") + return self.status.setText("正在重启并安装,新版本会自动打开…") self._notify("installing", "正在安装更新") QApplication.quit() + def _manual_install(self, path, reason): + """更新脚本无法代劳时,至少把新版本直接跑起来,不让用户只剩一个关掉的窗口。""" + try: + subprocess.Popen( + [path], + cwd=os.path.dirname(path), + creationflags=CREATE_NO_WINDOW, + close_fds=True, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except Exception as exc: # noqa: BLE001 + self.status.setText("%s;请前往下载页手动安装" % reason) + self.primary.setText("前往下载页") + self.primary.setEnabled(True) + self.primary.setProperty("action", "page") + return + self.status.setText("%s,已直接启动新版本" % reason) + self._notify("installing", "已启动新版本") + QApplication.quit() + def _notify(self, state, text): if self.on_state: self.on_state(state, text) diff --git a/src/version.py b/src/version.py index 6f29006..422795e 100644 --- a/src/version.py +++ b/src/version.py @@ -1,7 +1,7 @@ """应用版本与更新地址。""" APP_NAME = "网易云播放器" -APP_VERSION = "1.2.5" +APP_VERSION = "1.2.6" BUILD_DATE = "2026-10-31" UPDATE_ENDPOINT = "https://music.yuncishu.xyz/api/app/update" DOWNLOAD_PAGE = "https://music.yuncishu.xyz/download/"