Files
music123-desktop/src/updater.py
T
yuncishu 2123764b18 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
2026-11-01 09:51:37 +08:00

680 lines
26 KiB
Python

"""版本检查、自动下载与安装。"""
import hashlib
import os
import re
import subprocess
import sys
import tempfile
import time
import urllib.parse
import requests
from PySide6.QtCore import Qt, QUrl, QTimer
from PySide6.QtGui import QColor, QDesktopServices
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from PySide6.QtWidgets import (
QApplication, QDialog, QFrame, QHBoxLayout, QLabel, QProgressBar, QPushButton,
QVBoxLayout, QWidget, QGraphicsDropShadowEffect,
)
from version import APP_VERSION, UPDATE_ENDPOINT
from widgets import make_icon, icon_button
UPDATE_QSS = """
#updateCard { background:#fffefa; border:1px solid #e6e1da; border-radius:18px; }
QPushButton#iconBtn { background:transparent; border:none; border-radius:7px; outline:none; }
QPushButton#iconBtn:hover { background:#f2efea; }
QPushButton:focus { outline:none; }
#updateEyebrow { color:#e6493f; font-size:9px; font-weight:700; letter-spacing:1.7px; }
#updateTitle { color:#22211f; font-size:20px; font-weight:700; }
#updateSub { color:#8c877f; font-size:12px; }
#updateMeta { color:#aaa59c; font-size:10.5px; }
#updateNotesTitle { color:#8c877f; font-size:10px; font-weight:600; letter-spacing:1px; }
#updateNote { color:#5f5b55; font-size:12px; }
#updateStatus { color:#8c877f; font-size:11px; }
QProgressBar#updateProgress {
border:none; border-radius:3px; background:#eeeae4; height:6px; text-align:center;
color:transparent;
}
QProgressBar#updateProgress::chunk { background:#e6493f; border-radius:3px; }
QPushButton#updateLater {
background:transparent; border:1px solid #e0dcd5; border-radius:8px;
min-height:36px; color:#6d6962; font-size:12px; padding:0 18px;
}
QPushButton#updateLater:hover { background:#f3f0eb; color:#242322; }
QPushButton#updatePrimary {
background:#e6493f; border:none; border-radius:8px; min-height:36px;
color:#ffffff; font-size:12px; font-weight:600; padding:0 20px;
}
QPushButton#updatePrimary:hover { background:#d93e35; }
QPushButton#updatePrimary:disabled { background:#e8e4de; color:#aaa59c; }
QPushButton#updateLink {
background:transparent; border:none; color:#9b968e; font-size:10.5px;
text-align:left; padding:0;
}
QPushButton#updateLink:hover { color:#625f5a; }
"""
def parse_version(value):
text = str(value or "").strip()
text = re.split(r"[-+]", text, maxsplit=1)[0]
return tuple(int(part) for part in re.findall(r"\d+", text))
def is_newer(latest, current=APP_VERSION):
left, right = parse_version(latest), parse_version(current)
if not left:
return False
size = max(len(left), len(right))
return left + (0,) * (size - len(left)) > right + (0,) * (size - len(right))
def fetch_update_info(endpoint=UPDATE_ENDPOINT):
response = requests.get(
endpoint,
timeout=10,
headers={"User-Agent": "NeteasePlayer/%s" % APP_VERSION, "Cache-Control": "no-cache"},
)
response.raise_for_status()
info = response.json()
if not isinstance(info, dict):
raise RuntimeError("更新信息格式错误")
if not info.get("version") or not info.get("download_url"):
raise RuntimeError("更新信息缺少必要字段")
return info
def human_size(size):
try:
size = float(size)
except (TypeError, ValueError):
return ""
if size >= 1024 ** 3:
return "%.2f GB" % (size / 1024 ** 3)
if size >= 1024 ** 2:
return "%.1f MB" % (size / 1024 ** 2)
return "%.0f KB" % (size / 1024)
def _safe_suffix(url):
suffix = os.path.splitext(urllib.parse.urlparse(url).path)[1].lower()
return suffix if suffix in (".exe", ".msi", ".zip") else ".exe"
CREATE_NO_WINDOW = 0x08000000
def _offset_dir(sub=""):
base = os.environ.get("LOCALAPPDATA") or tempfile.gettempdir()
path = os.path.join(base, "NeteasePlayer", "updates")
if sub:
path = os.path.join(path, sub)
os.makedirs(path, exist_ok=True)
return path
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 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)
downloaded = os.path.abspath(downloaded)
log_dir = _offset_dir("logs")
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"""
$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 {}
}
# 新实例必须自己解压,不能复用本进程(以及旧播放器)的 _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
}
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__ ==="
Write-UpdateLog ("src=" + $src)
Write-UpdateLog ("dst=" + $dst)
Start-Sleep -Seconds 2
foreach ($p in @(__PIDS__)) {
try { Stop-Process -Id $p -Force -ErrorAction SilentlyContinue } catch {}
}
$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"
Remove-Item -LiteralPath $staged -Force -ErrorAction SilentlyContinue
$verified = $false
for ($i = 0; $i -lt 30; $i++) {
try {
Copy-Item -LiteralPath $src -Destination $staged -Force -ErrorAction Stop
if ($expected) {
# 用 .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"
break
} catch {
Write-UpdateLog ("stage attempt " + $i + " failed: " + $_.Exception.Message)
Start-Sleep -Milliseconds 500
}
}
if (-not $verified) {
Write-UpdateLog "stage failed; launching downloaded copy"
if (-not (Start-App $src)) { Start-App $dst }
Remove-Item -LiteralPath $staged -Force -ErrorAction SilentlyContinue
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; starting downloaded copy"
if (-not (Start-App $src)) { Start-App $dst }
Remove-Item -LiteralPath $staged -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue
exit
}
$installed = $false
try {
Move-Item -LiteralPath $staged -Destination $dst -Force -ErrorAction Stop
$installed = $true
Write-UpdateLog "new file in place"
} catch {
Write-UpdateLog ("swap failed: " + $_.Exception.Message)
if ((Test-Path -LiteralPath $backup) -and -not (Test-Path -LiteralPath $dst)) {
Move-Item -LiteralPath $backup -Destination $dst -Force -ErrorAction SilentlyContinue
Write-UpdateLog "rolled back from backup"
}
}
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
""".replace("__SRC__", _ps_quote(downloaded)) \
.replace("__DST__", _ps_quote(current)) \
.replace("__EXPECTED__", _ps_quote(digest or "")) \
.replace("__LOG_DIR__", _ps_quote(log_dir)) \
.replace("__LOG__", _ps_quote(log_path)) \
.replace("__PIDS_TEXT__", ",".join(str(p) for p in pids)) \
.replace("__PIDS__", ", ".join(str(p) for p in pids))
return script, log_path
class UpdateDialog(QDialog):
"""发现新版本后立即自动下载,下载完成可一键重启安装。"""
def __init__(self, info, parent=None, on_state=None):
super().__init__(parent)
self.info = info
self.on_state = on_state
self.reply = None
self.file = None
self.ready_path = None
self.downloaded = 0
self.total = int(info.get("size") or 0)
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setModal(True)
self.setStyleSheet(UPDATE_QSS)
self._build_ui()
QTimer.singleShot(300, self.start_download)
def _build_ui(self):
outer = QVBoxLayout(self)
outer.setContentsMargins(18, 18, 18, 18)
card = QFrame(objectName="updateCard")
shadow = QGraphicsDropShadowEffect(card)
shadow.setBlurRadius(38)
shadow.setOffset(0, 12)
shadow.setColor(QColor(28, 27, 31, 65))
card.setGraphicsEffect(shadow)
outer.addWidget(card)
layout = QVBoxLayout(card)
layout.setContentsMargins(26, 22, 26, 22)
layout.setSpacing(0)
top = QHBoxLayout()
top.setSpacing(13)
icon_box = QLabel()
icon_box.setFixedSize(42, 42)
icon_box.setPixmap(make_icon("refresh", "#ffffff", 20).pixmap(42, 42))
icon_box.setStyleSheet("background:#e6493f; border-radius:12px;")
icon_box.setAlignment(Qt.AlignCenter)
top.addWidget(icon_box)
titles = QVBoxLayout()
titles.setSpacing(2)
titles.addWidget(QLabel("NEW VERSION", objectName="updateEyebrow"))
self.title = QLabel("发现新版本", objectName="updateTitle")
titles.addWidget(self.title)
top.addLayout(titles, 1)
close = icon_button("close", 32, 15, tooltip="稍后再说")
close.clicked.connect(self.hide)
top.addWidget(close)
layout.addLayout(top)
layout.addSpacing(16)
latest = self.info.get("version", "")
self.sub = QLabel("正在为你自动下载新版本", objectName="updateSub")
layout.addWidget(self.sub)
notes = self.info.get("notes") or []
date = self.info.get("release_date") or ""
size = human_size(self.info.get("size"))
meta_parts = ["v%s → v%s" % (APP_VERSION, latest)]
if date:
meta_parts.append(date)
if size:
meta_parts.append(size)
self.meta = QLabel(" · ".join(meta_parts), objectName="updateMeta")
layout.addWidget(self.meta)
layout.addSpacing(16)
notes_title = QLabel("本次更新", objectName="updateNotesTitle")
layout.addWidget(notes_title)
layout.addSpacing(7)
box = QWidget()
box_layout = QVBoxLayout(box)
box_layout.setContentsMargins(0, 0, 0, 0)
box_layout.setSpacing(6)
if not notes:
notes = ["稳定性与性能优化"]
for note in notes[:6]:
row = QHBoxLayout()
row.setSpacing(8)
dot = QLabel("·")
dot.setStyleSheet("color:#e6493f; font-size:16px; font-weight:700;")
dot.setFixedWidth(8)
item = QLabel(str(note), objectName="updateNote")
item.setWordWrap(True)
row.addWidget(dot, 0, Qt.AlignTop)
row.addWidget(item, 1)
box_layout.addLayout(row)
layout.addWidget(box)
layout.addSpacing(18)
self.progress = QProgressBar(objectName="updateProgress")
self.progress.setRange(0, 100)
self.progress.setValue(0)
self.progress.setTextVisible(False)
layout.addWidget(self.progress)
layout.addSpacing(7)
self.status = QLabel("准备下载…", objectName="updateStatus")
layout.addWidget(self.status)
layout.addSpacing(16)
footer = QHBoxLayout()
footer.setSpacing(9)
self.link = QPushButton("查看下载页", objectName="updateLink")
self.link.setCursor(Qt.PointingHandCursor)
self.link.clicked.connect(self._open_page)
footer.addWidget(self.link)
footer.addStretch()
self.later = QPushButton("稍后再说", objectName="updateLater")
self.later.setCursor(Qt.PointingHandCursor)
self.later.clicked.connect(self.hide)
footer.addWidget(self.later)
self.primary = QPushButton("正在下载…", objectName="updatePrimary")
self.primary.setCursor(Qt.PointingHandCursor)
self.primary.setEnabled(False)
self.primary.clicked.connect(self._primary_action)
footer.addWidget(self.primary)
layout.addLayout(footer)
def _open_page(self):
QDesktopServices.openUrl(QUrl(self.info.get("page_url") or "https://music.yuncishu.xyz/download/"))
def start_download(self):
if self.ready_path:
return
if self.reply:
self.reply.abort()
self.reply.deleteLater()
self.reply = None
if self.file:
try:
self.file.close()
except Exception:
pass
self.file = None
url = self.info.get("download_url")
filename = "NeteasePlayer-%s%s" % (self.info.get("version", "new"), _safe_suffix(url))
self.ready_path = os.path.join(_offset_dir(self.info.get("version", "new")), filename)
self.file = open(self.ready_path + ".part", "wb")
self.downloaded = 0
self.progress.setValue(0)
self.status.setText("正在连接下载服务器…")
self.primary.setText("正在下载…")
self.primary.setEnabled(False)
self._notify("downloading", "正在下载")
self.manager = QNetworkAccessManager(self)
request = QNetworkRequest(QUrl(url))
request.setRawHeader(b"User-Agent", ("NeteasePlayer/%s" % APP_VERSION).encode())
try:
request.setAttribute(
QNetworkRequest.Attribute.RedirectPolicyAttribute,
QNetworkRequest.RedirectPolicy.NoLessSafeRedirectPolicy,
)
except Exception:
pass
self.reply = self.manager.get(request)
self.reply.readyRead.connect(self._read)
self.reply.downloadProgress.connect(self._progress)
self.reply.finished.connect(self._finished)
def _read(self):
if not self.reply or not self.file:
return
data = bytes(self.reply.readAll())
if data:
self.file.write(data)
self.downloaded += len(data)
def _progress(self, received, total):
if total > 0:
self.total = total
percent = int(received * 100 / total)
self.progress.setValue(max(0, min(100, percent)))
self.status.setText("正在下载 %s / %s" % (human_size(received), human_size(total)))
else:
self.status.setText("正在下载 %s" % human_size(received))
def _finished(self):
reply = self.reply
if not reply:
return
self._read()
if self.file:
self.file.flush()
self.file.close()
self.file = None
if reply.error() != QNetworkReply.NetworkError.NoError:
self.status.setText("下载失败 · " + reply.errorString())
self.primary.setText("重新下载")
self.primary.setEnabled(True)
self.primary.setProperty("action", "retry")
self._notify("error", "更新下载失败")
reply.deleteLater()
self.reply = None
return
part_path = self.ready_path + ".part"
expected = int(self.info.get("size") or 0)
if expected and os.path.getsize(part_path) != expected:
self.status.setText("文件大小校验失败,请重新下载")
self.primary.setText("重新下载")
self.primary.setEnabled(True)
self.primary.setProperty("action", "retry")
self._notify("error", "更新文件不完整")
reply.deleteLater()
self.reply = None
return
digest = self.info.get("sha256")
if digest:
hasher = hashlib.sha256()
with open(part_path, "rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
hasher.update(chunk)
if hasher.hexdigest().lower() != str(digest).lower():
self.status.setText("安全校验失败,已阻止安装")
self.primary.setText("重新下载")
self.primary.setEnabled(True)
self.primary.setProperty("action", "retry")
self._notify("error", "更新文件校验失败")
reply.deleteLater()
self.reply = None
return
os.replace(part_path, self.ready_path)
self.ready_path_final = self.ready_path
self.progress.setValue(100)
self.status.setText("下载完成 · 已通过完整性校验")
self.sub.setText("新版本已准备好,重启后即可使用")
self.primary.setText("重启并安装")
self.primary.setEnabled(True)
self.primary.setProperty("action", "install")
self._notify("ready", "更新已就绪")
reply.deleteLater()
self.reply = None
def _primary_action(self):
action = self.primary.property("action")
if action == "retry":
self.primary.setProperty("action", "")
self.start_download()
elif action == "page":
self._open_page()
else:
self.install()
def install(self):
path = getattr(self, "ready_path_final", None) or self.ready_path
if not path or not os.path.exists(path):
self.status.setText("安装包不存在,请重新下载")
return
if not getattr(sys, "frozen", False):
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
self.status.setText("安装包已打开")
return
current = os.path.abspath(sys.executable)
parent_pid = None
try:
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",
"-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden",
"-File", script_path,
],
creationflags=CREATE_NO_WINDOW,
close_fds=True,
env=clean_env(),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except Exception as exc: # noqa: BLE001
self._manual_install(path, "无法启动更新程序:%s" % exc)
return
# PowerShell 被安全策略或参数问题打断时会立刻退出,必须确认它真的活着
deadline = time.time() + 1.5
while time.time() < deadline:
if proc.poll() is not None:
break
time.sleep(0.1)
if proc.poll() is not None:
self._manual_install(path, "更新脚本未能运行")
return
self.status.setText("正在重启并安装,新版本会自动打开…")
self._notify("installing", "正在安装更新")
QApplication.quit()
def _manual_install(self, path, reason):
"""更新脚本无法代劳时,至少把新版本直接跑起来,不让用户只剩一个关掉的窗口。"""
try:
subprocess.Popen(
[path],
cwd=os.path.dirname(path),
creationflags=CREATE_NO_WINDOW,
close_fds=True,
env=clean_env(),
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):
if self.on_state:
self.on_state(state, text)
def showEvent(self, event):
super().showEvent(event)
parent = self.parentWidget()
if parent:
x = parent.x() + (parent.width() - self.width()) // 2
y = parent.y() + (parent.height() - self.height()) // 2
self.move(max(0, x), max(0, y))