1 Commits
Author SHA1 Message Date
yuncishu 52aee3b5d5 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
2026-10-31 23:52:53 +08:00
2 changed files with 170 additions and 106 deletions

No files matched your search

+169 -105
View File
@@ -5,6 +5,7 @@ import re
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import time
import urllib.parse import urllib.parse
import requests import requests
@@ -102,9 +103,14 @@ def _safe_suffix(url):
return suffix if suffix in (".exe", ".msi", ".zip") else ".exe" 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() base = os.environ.get("LOCALAPPDATA") or tempfile.gettempdir()
path = os.path.join(base, "NeteasePlayer", "updates") path = os.path.join(base, "NeteasePlayer", "updates")
if sub:
path = os.path.join(path, sub)
os.makedirs(path, exist_ok=True) os.makedirs(path, exist_ok=True)
return path return path
@@ -119,121 +125,146 @@ def powershell_exe():
return path if os.path.exists(path) else "powershell.exe" return path if os.path.exists(path) else "powershell.exe"
def build_install_script(current, downloaded, pid, digest=""): def build_install_script(current, downloaded, pid, digest="", parent_pid=None):
"""生成 Unicode 安全的更新脚本:校验副本后再交换文件,失败则从下载路径启动。""" """生成 Unicode 安全的更新脚本:校验副本后再交换文件,任何失败路径都会重新拉起程序。"""
current = os.path.abspath(current) current = os.path.abspath(current)
downloaded = os.path.abspath(downloaded) downloaded = os.path.abspath(downloaded)
log_dir = _offset_dir("logs") log_dir = _offset_dir("logs")
log_path = os.path.join(log_dir, "update.log") 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""" script = r"""
$ErrorActionPreference = 'Continue' $ErrorActionPreference = 'Continue'
$src = {src} $src = __SRC__
$dst = {dst} $dst = __DST__
$expected = {expected} $expected = __EXPECTED__
$logDir = {log_dir} $logDir = __LOG_DIR__
$log = {log} $log = __LOG__
New-Item -ItemType Directory -Force -Path $logDir | Out-Null New-Item -ItemType Directory -Force -Path $logDir | Out-Null
function Write-UpdateLog($message) {{ function Write-UpdateLog($message) {
try {{ try {
Add-Content -LiteralPath $log -Encoding UTF8 -Value ("[" + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + "] " + $message) Add-Content -LiteralPath $log -Encoding UTF8 -Value ("[" + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + "] " + $message)
}} catch {{}} } catch {}
}} }
Write-UpdateLog "update start; pid={pid}" 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 Start-Sleep -Seconds 2
try {{ Stop-Process -Id {pid} -Force -ErrorAction SilentlyContinue }} catch {{}} foreach ($p in @(__PIDS__)) {
Start-Sleep -Milliseconds 800 try { Stop-Process -Id $p -Force -ErrorAction SilentlyContinue } catch {}
}
Start-Sleep -Milliseconds 1200
$dstDir = Split-Path -Parent $dst
$staged = $dst + ".update" $staged = $dst + ".update"
$backup = $dst + ".bak" $backup = $dst + ".bak"
Remove-Item -LiteralPath $staged, $backup -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $staged -Force -ErrorAction SilentlyContinue
$verified = $false $verified = $false
for ($i = 0; $i -lt 30; $i++) {{ for ($i = 0; $i -lt 30; $i++) {
try {{ try {
Copy-Item -LiteralPath $src -Destination $staged -Force -ErrorAction Stop Copy-Item -LiteralPath $src -Destination $staged -Force -ErrorAction Stop
if ($expected) {{ if ($expected) {
$actual = (Get-FileHash -LiteralPath $staged -Algorithm SHA256).Hash $actual = (Get-FileHash -LiteralPath $staged -Algorithm SHA256).Hash
if ($actual -ne $expected) {{ throw "SHA256 mismatch" }} if ($actual -ne $expected) { throw "SHA256 mismatch" }
}} }
$verified = $true $verified = $true
Write-UpdateLog "staged copy verified" Write-UpdateLog "staged copy verified"
break break
}} catch {{ } catch {
Write-UpdateLog ("stage attempt " + $i + " failed: " + $_.Exception.Message) Write-UpdateLog ("stage attempt " + $i + " failed: " + $_.Exception.Message)
Start-Sleep -Milliseconds 500 Start-Sleep -Milliseconds 500
}} }
}} }
if (-not $verified) {{
Write-UpdateLog "stage failed; launching downloaded executable" if (-not $verified) {
if (Test-Path -LiteralPath $src) {{ Write-UpdateLog "stage failed; launching downloaded copy"
Start-Process -FilePath $src -WorkingDirectory (Split-Path -Parent $src) if (-not (Start-App $src)) { Start-App $dst }
}} Remove-Item -LiteralPath $staged -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue
exit exit
}} }
$swapped = $false $swapped = $false
for ($i = 0; $i -lt 30; $i++) {{ for ($i = 0; $i -lt 30; $i++) {
try {{ try {
if (Test-Path -LiteralPath $dst) {{ if (Test-Path -LiteralPath $dst) {
Move-Item -LiteralPath $dst -Destination $backup -Force -ErrorAction Stop Move-Item -LiteralPath $dst -Destination $backup -Force -ErrorAction Stop
}} }
$swapped = $true $swapped = $true
break break
}} catch {{ } catch {
Write-UpdateLog ("rename old attempt " + $i + " failed: " + $_.Exception.Message) Write-UpdateLog ("rename old attempt " + $i + " failed: " + $_.Exception.Message)
Start-Sleep -Milliseconds 500 Start-Sleep -Milliseconds 500
}} }
}} }
if (-not $swapped) {{
Write-UpdateLog "cannot rename running exe; launching staged copy in place" if (-not $swapped) {
try {{ Write-UpdateLog "cannot rename running exe; starting downloaded copy"
Move-Item -LiteralPath $staged -Destination $dst -Force -ErrorAction Stop if (-not (Start-App $src)) { Start-App $dst }
Start-Process -FilePath $dst -WorkingDirectory $dstDir Remove-Item -LiteralPath $staged -Force -ErrorAction SilentlyContinue
}} 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 Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue
exit exit
}} }
try {{ $installed = $false
try {
Move-Item -LiteralPath $staged -Destination $dst -Force -ErrorAction Stop Move-Item -LiteralPath $staged -Destination $dst -Force -ErrorAction Stop
Start-Process -FilePath $dst -WorkingDirectory $dstDir -ErrorAction Stop $installed = $true
Write-UpdateLog "new version started" Write-UpdateLog "new file in place"
for ($i = 0; $i -lt 15; $i++) {{ } catch {
try {{ Write-UpdateLog ("swap failed: " + $_.Exception.Message)
if (Test-Path -LiteralPath $backup) {{ if ((Test-Path -LiteralPath $backup) -and -not (Test-Path -LiteralPath $dst)) {
[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 Move-Item -LiteralPath $backup -Destination $dst -Force -ErrorAction SilentlyContinue
}} Write-UpdateLog "rolled back from backup"
if (Test-Path -LiteralPath $src) {{ }
Start-Process -FilePath $src -WorkingDirectory (Split-Path -Parent $src) }
}}
}} 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 Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue
""".format( """.replace("__SRC__", _ps_quote(downloaded)) \
src=_ps_quote(downloaded), .replace("__DST__", _ps_quote(current)) \
dst=_ps_quote(current), .replace("__EXPECTED__", _ps_quote(digest or "")) \
expected=_ps_quote(digest or ""), .replace("__LOG_DIR__", _ps_quote(log_dir)) \
log_dir=_ps_quote(log_dir), .replace("__LOG__", _ps_quote(log_path)) \
log=_ps_quote(log_path), .replace("__PIDS_TEXT__", ",".join(str(p) for p in pids)) \
pid=int(pid), .replace("__PIDS__", ", ".join(str(p) for p in pids))
)
return script, log_path return script, log_path
@@ -476,8 +507,8 @@ class UpdateDialog(QDialog):
if action == "retry": if action == "retry":
self.primary.setProperty("action", "") self.primary.setProperty("action", "")
self.start_download() self.start_download()
elif action == "install": elif action == "page":
self.install() self._open_page()
else: else:
self.install() self.install()
@@ -491,41 +522,74 @@ class UpdateDialog(QDialog):
self.status.setText("安装包已打开") self.status.setText("安装包已打开")
return return
current = os.path.abspath(sys.executable) current = os.path.abspath(sys.executable)
script, log_path = build_install_script( parent_pid = None
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)
try: 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", powershell_exe(), "-NoProfile", "-NonInteractive",
"-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden",
"-File", script_path, "-File", script_path,
], ],
creationflags=0x08000000 | 0x00000008, creationflags=CREATE_NO_WINDOW,
close_fds=True, close_fds=True,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
) )
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
# 更新脚本无法启动时,至少直接运行已下载的新版本 self._manual_install(path, "无法启动更新程序:%s" % exc)
try: return
subprocess.Popen( # PowerShell 被安全策略或参数问题打断时会立刻退出,必须确认它真的活着
[path], deadline = time.time() + 1.5
cwd=os.path.dirname(path), while time.time() < deadline:
creationflags=0x00000008, if proc.poll() is not None:
close_fds=True, break
) time.sleep(0.1)
except Exception: if proc.poll() is not None:
self.status.setText("无法启动更新程序:%s" % exc) self._manual_install(path, "更新脚本未能运行")
self.primary.setText("重试更新") return
self.primary.setEnabled(True)
self.primary.setProperty("action", "install")
return
QApplication.quit()
self.status.setText("正在重启并安装,新版本会自动打开…") self.status.setText("正在重启并安装,新版本会自动打开…")
self._notify("installing", "正在安装更新") self._notify("installing", "正在安装更新")
QApplication.quit() 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): def _notify(self, state, text):
if self.on_state: if self.on_state:
self.on_state(state, text) self.on_state(state, text)
+1 -1
View File
@@ -1,7 +1,7 @@
"""应用版本与更新地址。""" """应用版本与更新地址。"""
APP_NAME = "网易云播放器" APP_NAME = "网易云播放器"
APP_VERSION = "1.2.5" APP_VERSION = "1.2.6"
BUILD_DATE = "2026-10-31" BUILD_DATE = "2026-10-31"
UPDATE_ENDPOINT = "https://music.yuncishu.xyz/api/app/update" UPDATE_ENDPOINT = "https://music.yuncishu.xyz/api/app/update"
DOWNLOAD_PAGE = "https://music.yuncishu.xyz/download/" DOWNLOAD_PAGE = "https://music.yuncishu.xyz/download/"