zapret-kvn/xray_fluent/updates/installer/entry.py
loop-uh ced48cb34c
Some checks failed
Windows project source guards / test (push) Failing after 25s
Установщик обновления — на общем ядре zapret_update, том же, что у Zapret 2 GUI
Замена файлов, порядок шагов, проверка запуска и откат теперь в общем
ядре (байт-в-байт копия). Своё у Zapret KVN — только прослойка runner:
план прежнего вида (его пишет ещё прежняя версия), журнал update.log,
хвост startup.log упавшего запуска и сообщение об ошибке. Журнал замены
теперь пишется на диск: замену, оборванную выключением, можно откатить.
Замок окна обновления — общая переменная ZAPRET_UPDATE_HOLD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:54:26 +03:00

118 lines
4.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Режим ``ZapretKVN.exe --apply-update <план>``: установить и выйти."""
from __future__ import annotations
import logging
import subprocess
import sys
from pathlib import Path
from zapret_update import handoff, processes
from .plan import WORK_DIR_PREFIX, InstallPlan
from .runner import Stage, run_install, setup_logging
_log = logging.getLogger("xray_fluent.update_installer")
def main(plan_path: str) -> int:
plan = InstallPlan.load(Path(plan_path))
setup_logging(plan.app_dir)
try:
ok = _install(plan)
except Exception:
_log.exception("Установщик завершился с ошибкой")
ok = False
finally:
# Окно обновления закрыто: приложение может показать своё.
handoff.release(plan.hold_marker)
_remove_work_dir_later(plan)
return 0 if ok else 1
def _install(plan: InstallPlan) -> bool:
try:
session = _WindowSession(plan)
except Exception:
# Окно — удобство, а не условие: без него установка идёт так же.
_log.exception("Окно обновления недоступно, установка продолжается без него")
_confirm_ready(plan)
return run_install(plan)
_confirm_ready(plan)
return session.run()
def _confirm_ready(plan: InstallPlan) -> None:
# Приложение ждёт этот файл и только после него закрывается: окно
# обновления к этому моменту уже на экране.
plan.ready_marker.write_text("ready", encoding="utf-8")
class _WindowSession:
"""Окно в главном потоке, установка — в рабочем."""
def __init__(self, plan: InstallPlan):
from PyQt6.QtCore import QThread, pyqtSignal
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QApplication
from ...constants import APP_ICON_PATH, APP_NAME
from ...ui.theme import apply_theme
from .window import UpdateWindow
class Worker(QThread):
progress = pyqtSignal(object, float)
def __init__(self) -> None:
super().__init__()
self.ok = False
def run(self) -> None:
try:
self.ok = run_install(plan, self.progress.emit)
except Exception:
_log.exception("Установка прервана")
self.progress.emit(Stage.FAILED, 1.0)
self._app = QApplication.instance() or QApplication([sys.argv[0]])
self._app.setApplicationName(APP_NAME)
self._app.setWindowIcon(QIcon(str(APP_ICON_PATH)))
self._app.setQuitOnLastWindowClosed(True)
apply_theme(plan.theme, plan.accent, force=True)
self._window = UpdateWindow(plan)
self._worker = Worker()
self._worker.progress.connect(self._window.on_progress)
self._window.show()
def run(self) -> bool:
self._worker.start()
try:
self._app.exec()
finally:
# Что бы ни случилось с окном, установка доводится до конца:
# выход посреди замены оставил бы приложение разобранным.
self._worker.wait()
return self._worker.ok
def _remove_work_dir_later(plan: InstallPlan) -> None:
"""Удалить временный каталог загрузки после выхода установщика.
Установщик запущен из этого каталога и удалить его сам не может; без
уборки каждое обновление оставляло бы на диске сотни мегабайт до
следующего запуска приложения.
"""
work_dir = plan.work_dir
if not processes.IS_WINDOWS:
return
if not work_dir.name.startswith(WORK_DIR_PREFIX) or any(c in str(work_dir) for c in '"%'):
return
try:
subprocess.Popen(
f'cmd.exe /d /c "ping -n 4 127.0.0.1 >nul & rmdir /s /q "{work_dir}""',
cwd=str(work_dir.parent),
creationflags=processes.CREATE_NO_WINDOW,
close_fds=True,
)
except OSError:
_log.warning("Не удалось запланировать удаление %s", work_dir, exc_info=True)