From 2123764b181a22a6c2ebe52294a2167155f9b86d Mon Sep 17 00:00:00 2001 From: yuncishu <3053860096@qq.com> Date: Sun, 1 Nov 2026 09:51:37 +0800 Subject: [PATCH] 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 --- src/updater.py | 96 ++++++++++++++++++++++++++++++++++++++++++++------ src/version.py | 2 +- 2 files changed, 87 insertions(+), 11 deletions(-) diff --git a/src/updater.py b/src/updater.py index 8778faa..78bab84 100644 --- a/src/updater.py +++ b/src/updater.py @@ -125,6 +125,22 @@ def 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): """生成 Unicode 安全的更新脚本:校验副本后再交换文件,任何失败路径都会重新拉起程序。""" 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) } 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) { 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 + Clear-FrozenEnv + Prepare-Runtime + for ($attempt = 1; $attempt -le 3; $attempt++) { + try { + $proc = Start-Process -FilePath $path -WorkingDirectory (Split-Path -Parent $path) -PassThru -ErrorAction Stop + Start-Sleep -Milliseconds 2000 + if (-not $proc.HasExited) { + Write-UpdateLog ("started: " + $path + " (pid=" + $proc.Id + ")") + return $true + } + Write-UpdateLog ("process exited at once, attempt " + $attempt) + } catch { + Write-UpdateLog ("start failed: " + $path + " :: " + $_.Exception.Message) + } + Start-Sleep -Milliseconds 1500 } + return $false } Write-UpdateLog "=== update begin; pids=__PIDS_TEXT__ ===" @@ -171,7 +232,19 @@ Start-Sleep -Seconds 2 foreach ($p in @(__PIDS__)) { 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" $backup = $dst + ".bak" @@ -182,8 +255,9 @@ 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" } + # 用 .NET 而不是 Get-FileHash,兼容没有 PowerShell 4.0 的老系统 + $actual = Get-Sha256 $staged + if ($actual -ne $expected.ToLower()) { throw "SHA256 mismatch" } } $verified = $true Write-UpdateLog "staged copy verified" @@ -548,6 +622,7 @@ class UpdateDialog(QDialog): ], creationflags=CREATE_NO_WINDOW, close_fds=True, + env=clean_env(), stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, @@ -576,6 +651,7 @@ class UpdateDialog(QDialog): cwd=os.path.dirname(path), creationflags=CREATE_NO_WINDOW, close_fds=True, + env=clean_env(), stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, diff --git a/src/version.py b/src/version.py index bf1a4b2..ca66013 100644 --- a/src/version.py +++ b/src/version.py @@ -2,6 +2,6 @@ APP_NAME = "网易云播放器" APP_VERSION = "1.2.7" -BUILD_DATE = "2026-10-31" +BUILD_DATE = "2026-11-01" UPDATE_ENDPOINT = "https://music.yuncishu.xyz/api/app/update" DOWNLOAD_PAGE = "https://music.yuncishu.xyz/download/"