PixelLauncher 2.1.0 — стабильная сборка
Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров (Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
This commit is contained in:
commit
ec6f13ddcc
33 files changed
+10445
No files matched your search
@@ -0,0 +1,130 @@
|
||||
# -*- 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
|
||||
Reference in new issue
Block a user