Files
PixelLauncher/launcher/selfupdate.py
T
kosta2014p ec6f13ddcc PixelLauncher 2.1.0 — стабильная сборка
Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров
(Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление
модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
2026-10-18 22:44:19 +07:00

131 lines
5.0 KiB
Python

# -*- coding: utf-8 -*-
"""Автообновление лаунчера: проверка манифеста, скачивание установщика.
Манифест на сервере — /update.json:
{"version": "2.1.0", "url": "/download/PixelLauncherSetup2.1.0.exe",
"sha256": "…", "size": 15418378, "notes": "…"}
url относительный (база — UPDATE_BASE), sha256/size необязательны,
но sha256 проверяется, если указан.
check() — манифест, если версия новее текущей; None — обновления нет;
ошибки сети/формата — RuntimeError.
download() — скачать установщик и проверить SHA-256, вернуть путь.
run_installer() — запустить установщик (caller закрывает лаунчер).
"""
import hashlib
import json
import os
import sys
import tempfile
from . import LAUNCHER_VERSION, http
# База обновлений — официальный домен (HTTPS, сертификат Let's Encrypt).
UPDATE_BASE = "https://pixellauncher.ru"
MANIFEST_URL = UPDATE_BASE + "/update.json"
def _vtuple(version):
""""1.10" -> (1, 10); нечисловые куски — ноль; хвостовые нули срезаем
(1.0.0.0 == 1.0)."""
out = []
for part in str(version).strip().split("."):
part = part.strip()
out.append(int(part) if part.isdigit() else 0)
while len(out) > 1 and out[-1] == 0:
out.pop()
return tuple(out)
def is_newer(version):
"""Есть ли в version что-то новее, чем установлено."""
return _vtuple(version) > _vtuple(LAUNCHER_VERSION)
def check(timeout=15):
"""Манифест с версией новее текущей, иначе None. Бросает RuntimeError."""
try:
raw = http.get_bytes(MANIFEST_URL, timeout=timeout)
manifest = json.loads(raw.decode("utf-8"))
except Exception as e: # noqa: BLE001
raise RuntimeError(f"нет связи с сервером обновлений: {e}") from e
if not isinstance(manifest, dict) \
or not str(manifest.get("version") or "").strip():
raise RuntimeError("сервер прислал некорректный манифест")
if not is_newer(manifest["version"]):
return None
return manifest
def sha256_file(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 16), b""):
h.update(chunk)
return h.hexdigest()
def _installer_path(url):
name = url.split("?")[0].rstrip("/").split("/")[-1] or "update.exe"
if not name.lower().endswith(".exe"):
name = "update.exe"
return os.path.join(tempfile.gettempdir(), name)
def download(manifest, progress=None, cancel=None):
"""Скачивает установщик из манифеста, сверяет SHA-256. Возвращает путь.
progress(done_bytes, total_bytes|None); cancel — threading.Event.
"""
url = str(manifest.get("url") or "")
if not url:
raise RuntimeError("в манифесте обновления нет url")
fetch = url
if os.path.isabs(url):
from pathlib import Path
fetch = Path(url).as_uri() # C:/… -> file:///C:/…
elif url.startswith("file:"):
fetch = url
elif "://" not in url:
url = UPDATE_BASE + url # относительный путь /download/…
fetch = url
path = _installer_path(url)
want = str(manifest.get("sha256") or "").strip().lower()
size = int(manifest.get("size") or 0) or None
if os.path.isfile(path):
if want:
if sha256_file(path) == want:
if progress:
total = os.path.getsize(path)
progress(total, total)
return path
try: # битый/устаревший остаток — качаем заново
os.remove(path)
except OSError:
pass
elif size and os.path.getsize(path) == size:
return path
http.download_file(fetch, path, size=size, progress=progress,
cancel=cancel)
if want:
got = sha256_file(path)
if got != want:
try:
os.remove(path)
except OSError:
pass
raise RuntimeError("SHA-256 не совпал — файл обновления битый")
return path
def run_installer(path):
"""Запускает установщик. После вызова лаунчер нужно завершить."""
if not os.path.isfile(path):
raise RuntimeError("нет файла установщика: " + str(path))
if sys.platform != "win32":
raise RuntimeError("автоустановка поддерживается только в Windows")
os.startfile(path) # noqa: S606