Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52aee3b5d5 |
No files matched your search
+169
-105
@@ -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
@@ -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/"
|
||||||
Reference in new issue
Block a user