fix: new instance must not reuse the old _MEI unpack dir (1.2.7)

The update script is spawned from inside the frozen app, so the child
inherits _PYI_APPLICATION_HOME_DIR / _PYI_ARCHIVE_FILE. Combined with
MEI dir names being derived from the pid, a new instance could end up
loading python313.dll from the directory the old process was deleting,
giving 'Failed to load Python DLL ... module not found'.

- strip _PYI_* and PYINSTALLER_* from the environment of every child
- run the new instance with its own TEMP under updates/runtime
- wait for the old processes to actually exit before swapping files
- retry the launch and verify the process is still alive
- replace Get-FileHash / Get-ChildItem -Directory with PS 2.0 safe code
This commit is contained in:
yuncishu committed 2026-11-01 09:51:37 +08:00
1 parent 8528aad7e6
commit 2123764b18
2 files changed
+83 -7

No files matched your search

+82 -6
View File
@@ -125,6 +125,22 @@ def powershell_exe():
return path if os.path.exists(path) else "powershell.exe" return path if os.path.exists(path) else "powershell.exe"
def clean_env():
"""去掉 PyInstaller 的 _PYI_* / PYINSTALLER_* 变量后再启动子进程。
单文件程序解压到 %TEMP%\\_MEIxxxxxx,并通过 _PYI_APPLICATION_HOME_DIR
把这个路径告诉子进程。更新脚本本身是从播放器内部启动的,会一路继承到
新拉起的 exe 上,导致新实例复用旧解压目录;而旧进程退出时正在删除该
目录,于是出现 "Failed to load Python DLL ... 找不到指定的模块"。
"""
env = os.environ.copy()
for key in list(env):
upper = str(key).upper()
if upper.startswith("_PYI") or upper.startswith("PYINSTALLER_"):
env.pop(key, None)
return env
def build_install_script(current, downloaded, pid, digest="", parent_pid=None): def build_install_script(current, downloaded, pid, digest="", parent_pid=None):
"""生成 Unicode 安全的更新脚本:校验副本后再交换文件,任何失败路径都会重新拉起程序。""" """生成 Unicode 安全的更新脚本:校验副本后再交换文件,任何失败路径都会重新拉起程序。"""
current = os.path.abspath(current) current = os.path.abspath(current)
@@ -148,20 +164,65 @@ function Write-UpdateLog($message) {
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 {}
} }
# 新实例必须自己解压,不能复用本进程(以及旧播放器)的 _MEI 目录。
# 而且 _MEI 目录名是按 pid 编码的,旧进程延迟清理时可能撞掉新进程刚建好的
# 同名目录(pid 复用),于是出现 "Failed to load Python DLL"。把新实例的运行
# 临时目录挪到我们自己的目录下,彻底避开这种竞争。
$runtime = Join-Path (Split-Path -Parent $logDir) "runtime"
function Prepare-Runtime {
try {
New-Item -ItemType Directory -Force -Path $runtime | Out-Null
# 不用 Get-ChildItem -Directory,兼容 Windows 7 自带的 PowerShell 2.0
Get-ChildItem -LiteralPath $runtime -Force -ErrorAction SilentlyContinue |
Where-Object { $_.PSIsContainer -and $_.Name -like "_MEI*" } |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
$env:TEMP = $runtime
$env:TMP = $runtime
Write-UpdateLog ("runtime temp -> " + $runtime)
} catch {
Write-UpdateLog ("runtime temp failed: " + $_.Exception.Message)
}
}
function Get-Sha256($path) {
$stream = [System.IO.File]::Open($path, 'Open', 'Read', 'Read')
try {
$sha = New-Object System.Security.Cryptography.SHA256Managed
$bytes = $sha.ComputeHash($stream)
$sb = New-Object System.Text.StringBuilder
foreach ($b in $bytes) { [void]$sb.Append($b.ToString('x2')) }
return $sb.ToString()
} finally {
$stream.Close()
}
}
function Clear-FrozenEnv {
foreach ($name in @('_PYI_APPLICATION_HOME_DIR','_PYI_ARCHIVE_FILE','_PYI_PARENT_PROCESS_LEVEL','_PYI_SPLASH_IPC','PYINSTALLER_RESET_ENVIRONMENT','PYINSTALLER_STRICT_UNPACK_MODE','PYINSTALLER_SUPPRESS_SPLASH_SCREEN')) {
Remove-Item -Path ("Env:\" + $name) -Force -ErrorAction SilentlyContinue
}
}
function Start-App($path) { function Start-App($path) {
if (-not $path) { return $false } if (-not $path) { return $false }
if (-not (Test-Path -LiteralPath $path)) { if (-not (Test-Path -LiteralPath $path)) {
Write-UpdateLog ("missing target: " + $path) Write-UpdateLog ("missing target: " + $path)
return $false return $false
} }
Clear-FrozenEnv
Prepare-Runtime
for ($attempt = 1; $attempt -le 3; $attempt++) {
try { try {
Start-Process -FilePath $path -WorkingDirectory (Split-Path -Parent $path) -ErrorAction Stop $proc = Start-Process -FilePath $path -WorkingDirectory (Split-Path -Parent $path) -PassThru -ErrorAction Stop
Write-UpdateLog ("started: " + $path) Start-Sleep -Milliseconds 2000
if (-not $proc.HasExited) {
Write-UpdateLog ("started: " + $path + " (pid=" + $proc.Id + ")")
return $true return $true
}
Write-UpdateLog ("process exited at once, attempt " + $attempt)
} catch { } catch {
Write-UpdateLog ("start failed: " + $path + " :: " + $_.Exception.Message) Write-UpdateLog ("start failed: " + $path + " :: " + $_.Exception.Message)
return $false
} }
Start-Sleep -Milliseconds 1500
}
return $false
} }
Write-UpdateLog "=== update begin; pids=__PIDS_TEXT__ ===" Write-UpdateLog "=== update begin; pids=__PIDS_TEXT__ ==="
@@ -171,7 +232,19 @@ Start-Sleep -Seconds 2
foreach ($p in @(__PIDS__)) { foreach ($p in @(__PIDS__)) {
try { Stop-Process -Id $p -Force -ErrorAction SilentlyContinue } catch {} try { Stop-Process -Id $p -Force -ErrorAction SilentlyContinue } catch {}
} }
Start-Sleep -Milliseconds 1200 $gone = $false
$waited = 0
for ($i = 0; $i -lt 80; $i++) {
$alive = 0
foreach ($p in @(__PIDS__)) {
if (Get-Process -Id $p -ErrorAction SilentlyContinue) { $alive++ }
}
if ($alive -eq 0) { $gone = $true; break }
Start-Sleep -Milliseconds 250
$waited += 250
}
Write-UpdateLog ("old processes stopped=" + $gone + " after " + $waited + " ms")
Start-Sleep -Milliseconds 1500
$staged = $dst + ".update" $staged = $dst + ".update"
$backup = $dst + ".bak" $backup = $dst + ".bak"
@@ -182,8 +255,9 @@ 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 # 用 .NET 而不是 Get-FileHash,兼容没有 PowerShell 4.0 的老系统
if ($actual -ne $expected) { throw "SHA256 mismatch" } $actual = Get-Sha256 $staged
if ($actual -ne $expected.ToLower()) { throw "SHA256 mismatch" }
} }
$verified = $true $verified = $true
Write-UpdateLog "staged copy verified" Write-UpdateLog "staged copy verified"
@@ -548,6 +622,7 @@ class UpdateDialog(QDialog):
], ],
creationflags=CREATE_NO_WINDOW, creationflags=CREATE_NO_WINDOW,
close_fds=True, close_fds=True,
env=clean_env(),
stdin=subprocess.DEVNULL, stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
@@ -576,6 +651,7 @@ class UpdateDialog(QDialog):
cwd=os.path.dirname(path), cwd=os.path.dirname(path),
creationflags=CREATE_NO_WINDOW, creationflags=CREATE_NO_WINDOW,
close_fds=True, close_fds=True,
env=clean_env(),
stdin=subprocess.DEVNULL, stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
+1 -1
View File
@@ -2,6 +2,6 @@
APP_NAME = "网易云播放器" APP_NAME = "网易云播放器"
APP_VERSION = "1.2.7" APP_VERSION = "1.2.7"
BUILD_DATE = "2026-10-31" BUILD_DATE = "2026-11-01"
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/"