From e0789d4a28a10aa336ef9fce20bc6e6dcf6243b0 Mon Sep 17 00:00:00 2001 From: yuncishu <3053860096@qq.com> Date: Sat, 31 Oct 2026 23:05:59 +0800 Subject: [PATCH] fix: make update installer atomic and Unicode-safe (1.2.4) --- src/updater.py | 177 +++++++++++++++++++++++++++++++++++++++++++------ src/version.py | 2 +- 2 files changed, 159 insertions(+), 20 deletions(-) diff --git a/src/updater.py b/src/updater.py index 1eefb2d..8332745 100644 --- a/src/updater.py +++ b/src/updater.py @@ -109,8 +109,132 @@ def _offset_dir(version): return path -def _quote(path): - return '"' + str(path).replace('"', '""') + '"' +def _ps_quote(value): + return "'" + str(value).replace("'", "''") + "'" + + +def powershell_exe(): + root = os.environ.get("SystemRoot") or os.environ.get("WINDIR") or r"C:\Windows" + path = os.path.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") + return path if os.path.exists(path) else "powershell.exe" + + +def build_install_script(current, downloaded, pid, digest=""): + """生成 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") + script = r""" +$ErrorActionPreference = 'Continue' +$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 {{ + Add-Content -LiteralPath $log -Encoding UTF8 -Value ("[" + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + "] " + $message) + }} catch {{}} +}} +Write-UpdateLog "update start; pid={pid}" +Start-Sleep -Seconds 2 +try {{ Stop-Process -Id {pid} -Force -ErrorAction SilentlyContinue }} catch {{}} +Start-Sleep -Milliseconds 800 + +$dstDir = Split-Path -Parent $dst +$staged = $dst + ".update" +$backup = $dst + ".bak" +Remove-Item -LiteralPath $staged, $backup -Force -ErrorAction SilentlyContinue +$verified = $false +for ($i = 0; $i -lt 30; $i++) {{ + try {{ + Copy-Item -LiteralPath $src -Destination $staged -Force -ErrorAction Stop + if ($expected) {{ + $actual = (Get-FileHash -LiteralPath $staged -Algorithm SHA256).Hash + if ($actual -ne $expected) {{ throw "SHA256 mismatch" }} + }} + $verified = $true + Write-UpdateLog "staged copy verified" + break + }} 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) + }} + Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue + exit +}} + +$swapped = $false +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 {{ + 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) + }} + }} + Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue + exit +}} + +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)) {{ + Move-Item -LiteralPath $backup -Destination $dst -Force -ErrorAction SilentlyContinue + }} + if (Test-Path -LiteralPath $src) {{ + Start-Process -FilePath $src -WorkingDirectory (Split-Path -Parent $src) + }} +}} +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), + ) + return script, log_path class UpdateDialog(QDialog): @@ -367,24 +491,39 @@ class UpdateDialog(QDialog): self.status.setText("安装包已打开") return current = os.path.abspath(sys.executable) - script_path = os.path.join(tempfile.gettempdir(), "netease_player_update.cmd") - script = "\r\n".join([ - "@echo off", - "chcp 65001 >nul", - "timeout /t 2 /nobreak >nul", - "taskkill /PID %d /F >nul 2>&1" % os.getpid(), - 'copy /Y %s %s >nul 2>&1' % (_quote(path), _quote(current)), - 'start "" %s' % _quote(current), - 'del "%~f0" >nul 2>&1', - "", - ]) - with open(script_path, "w", encoding="utf-8") as handle: + 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) - subprocess.Popen( - ["cmd.exe", "/c", script_path], - creationflags=0x08000000 | 0x00000008, - close_fds=True, - ) + try: + subprocess.Popen( + [ + powershell_exe(), "-NoProfile", "-NonInteractive", + "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", + "-File", script_path, + ], + creationflags=0x08000000 | 0x00000008, + close_fds=True, + ) + 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.status.setText("正在重启并安装,新版本会自动打开…") + self._notify("installing", "正在安装更新") QApplication.quit() def _notify(self, state, text): diff --git a/src/version.py b/src/version.py index 4e5591e..6e6a394 100644 --- a/src/version.py +++ b/src/version.py @@ -1,7 +1,7 @@ """应用版本与更新地址。""" APP_NAME = "网易云播放器" -APP_VERSION = "1.2.3" +APP_VERSION = "1.2.4" BUILD_DATE = "2026-10-31" UPDATE_ENDPOINT = "https://music.yuncishu.xyz/api/app/update" DOWNLOAD_PAGE = "https://music.yuncishu.xyz/download/"