Files
xieyi-weixin-/installer_payload.py
2026-11-02 14:05:03 +08:00

132 lines
6.4 KiB
Python

"""Verified installer payloads and reversible same-volume installation swaps."""
from __future__ import annotations
import hashlib
import json
import os
import shutil
import tempfile
import uuid
from pathlib import Path
MANIFEST_NAME = "installation-manifest.json"
REQUIRED_FILES = (
"ZhenAIUpdater.exe", "_internal/base_library.zip",
"_internal/shiboken6/Shiboken.pyd", "_internal/shiboken6/shiboken6.abi3.dll",
"_internal/PySide6/QtCore.pyd", "_internal/PySide6/QtWidgets.pyd",
"_internal/PySide6/QtWebEngineCore.pyd", "_internal/PySide6/QtWebEngineProcess.exe",
"_internal/PySide6/plugins/platforms/qwindows.dll",
"_internal/PySide6/resources/qtwebengine_resources.pak",
"_internal/assets/ui/zhenyang-ai-console.html",
"_internal/assets/ui/console-app.js",
"_internal/assets/ui/console-responsive.css",
"_internal/assets/ui/console-review.css",
"_internal/assets/ui/console-design.css",
"_internal/assets/ui/proactive-contact.js",
"_internal/assets/ui/proactive-contact.css",
)
def file_hash(path: Path) -> str:
result = hashlib.sha256()
with path.open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
result.update(chunk)
return result.hexdigest()
def fresh_process_environment() -> dict[str, str]:
environment = {key: value for key, value in os.environ.items()
if not key.startswith("_PYI_") and key != "_MEIPASS2"}
environment["PYINSTALLER_RESET_ENVIRONMENT"] = "1"
return environment
def _contained_file(root: Path, relative: str) -> Path:
path = root / relative
if not relative or Path(relative).is_absolute() or ".." in Path(relative).parts:
raise RuntimeError(f"Invalid payload path: {relative}")
if not path.resolve().is_relative_to(root.resolve()):
raise RuntimeError(f"Payload path escapes its directory: {relative}")
return path
def validate_installation(root: Path, version: str = "", *, hashes: bool = True) -> dict:
root = root.resolve()
manifest = json.loads((root / MANIFEST_NAME).read_text(encoding="utf-8"))
if version and manifest.get("version") != version:
raise RuntimeError("Installation payload version does not match the update")
expected_exe = f"ZhenAI-WeCom-Assistant-v{manifest['version']}.exe"
if manifest.get("executable") != expected_exe:
raise RuntimeError("Invalid application executable in installation manifest")
files = manifest.get("files")
if not isinstance(files, dict) or not files:
raise RuntimeError("Installation manifest contains no files")
for name in (expected_exe, *REQUIRED_FILES):
if name not in files:
raise RuntimeError(f"Required runtime missing from payload: {name}")
if not any(name.startswith("_internal/python3") and name.endswith(".dll") for name in files):
raise RuntimeError("Python runtime missing from payload")
for name, expected in files.items():
path = _contained_file(root, name)
if not path.is_file() or path.stat().st_size != expected['size']:
raise RuntimeError(f"Installation file missing or incomplete: {name}")
if hashes and file_hash(path) != expected['sha256']:
raise RuntimeError(f"Installation checksum mismatch: {name}")
return manifest
def write_manifest(root: Path, version: str) -> Path:
root = root.resolve()
files = {}
for path in sorted(root.rglob("*")):
if path.is_file() and path.name != MANIFEST_NAME:
files[path.relative_to(root).as_posix()] = {"size": path.stat().st_size, "sha256": file_hash(path)}
manifest = {"version": version, "executable": f"ZhenAI-WeCom-Assistant-v{version}.exe", "files": files}
target = root / MANIFEST_NAME
target.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
validate_installation(root, version)
return target
def apply_payload(payload: Path, install_dir: Path, version: str, close_application, *, move_payload: bool = False) -> Path | None:
payload = payload.resolve()
install_dir = install_dir.absolute()
if install_dir == install_dir.parent or install_dir.resolve() != install_dir:
raise RuntimeError("Installation directory must not be a drive root or directory link")
if payload.is_relative_to(install_dir) or install_dir.is_relative_to(payload):
raise RuntimeError("Installer payload must be outside the installation directory")
validate_installation(payload, version)
if install_dir.exists() and any(install_dir.iterdir()) and not any(install_dir.glob("ZhenAI-WeCom-Assistant-v*.exe")):
raise RuntimeError("Refusing to replace a directory that is not an application installation")
install_dir.parent.mkdir(parents=True, exist_ok=True)
stage = Path(tempfile.mkdtemp(prefix=f".{install_dir.name}.stage-", dir=install_dir.parent))
backup = install_dir.with_name(f".{install_dir.name}.backup-{uuid.uuid4().hex[:12]}")
moved_old = False
committed = False
try:
if move_payload and payload.drive.lower() == install_dir.drive.lower():
# NSIS has already unpacked onto this volume; rename instead of
# duplicating more than a gigabyte of runtime files.
stage.rmdir()
payload.rename(stage)
else:
required = sum(item['size'] for item in validate_installation(payload, version, hashes=False)['files'].values())
if shutil.disk_usage(install_dir.parent).free < required + 64 * 1024 * 1024:
raise RuntimeError("Not enough disk space to stage the update; existing installation preserved")
shutil.copytree(payload, stage, dirs_exist_ok=True)
validate_installation(stage, version)
# All slow extraction, copying and checks finish before the old app stops.
close_application()
if install_dir.exists():
install_dir.rename(backup)
moved_old = True
try:
stage.rename(install_dir)
committed = True
except BaseException:
if moved_old:
backup.rename(install_dir)
raise
return backup if moved_old else None
finally:
# Delete only the exact temporary directory created by this invocation.
if not committed and stage.exists():
assert stage.resolve().parent == install_dir.parent.resolve()
assert stage.name.startswith(f".{install_dir.name}.stage-")
shutil.rmtree(stage)