zapret-kvn/xray_fluent/application/startup_service.py
loop-uh f9971d5a1f
Some checks failed
Windows project source guards / test (push) Failing after 31s
Zapret: модуль и страница переписаны — проще, понятнее, строже
Движок разложен на части с одной задачей каждая:
- endpoint: вид сервера (TCP / QUIC / WireGuard), хосты, порты, DNS;
- strategies: каталоги winws2 и правило для выбранного сервера;
- presets: файлы пресетов;
- command: сборка командной строки winws2 (чистая функция);
- manager: процесс winws2 с одним входом apply(план) и ревизиями —
  готовность только по started процесса нужной ревизии, таймаут 5 с.
Склейка с подключением (~450 строк в контроллере, два наслоенных
механизма «UDP-защиты» и «профиля сервера», кэш и прогрев DNS) заменена
шлюзом application/server_bypass.py. Сохранены: DNS только в воркере,
остановка туннеля перед сменой правила (кроме восстановления Hysteria),
забор перед стартом ядра, fail-closed при потере winws2 (теперь и по
таймауту перезапуска), маскировка IP сервера в логах до старта ядра;
адреса серверов пула изучаются в фоне после подключения.

Поведение:
- у QUIC и WireGuard теперь свои стратегии; ключ zapret_target совместим,
  старые сборки после отката читают udp_* как раньше;
- в каталогах вернулись 82 составные стратегии с ветками --payload
  (валидатор принимал только --lua-desync и молча их выбрасывал);
- смена пресета под защищённой сессией идёт через переход: туннель
  останавливается, winws2 перезапускается, VPN подключается снова;
- пропавший файл выбранного пресета — откат к Default, а не отказ;
- правка работающего пресета перезапускает его; переименование больше
  не оставляет копию, занятое имя отклоняется;
- настройки Zapret не откатываются при сохранении страницы «Настройки».

Интерфейс: карточка состояния (работает/нет, пресет, одна кнопка,
автозапуск), выбранный VPN-сервер с живым правилом, три строки видов
серверов с подстраницей стратегии, пресеты — отдельной подстраницей.
Объяснение — в окне «Как это б#&^ь работает?» с иллюстрацией.
2026-09-28 20:53:25 +03:00

120 lines
4.4 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.

"""Two-phase startup preparation. Workers never construct Qt widgets/managers."""
from __future__ import annotations
import logging
import time
from pathlib import Path
from PyQt6.QtCore import QObject, QThread, pyqtSignal
from ..profiles.models import AppState, AppSettings
from ..profiles.storage import StateStorage, PassphraseRequired
from ..diagnostics.traffic_history import TrafficHistoryStorage
class StartupWorker(QThread):
early = pyqtSignal(object, bool)
loaded = pyqtSignal(object, object)
password_needed = pyqtSignal()
failed = pyqtSignal(str)
def __init__(self, storage, prepare=None, parent=None):
super().__init__(parent)
self.storage = storage
self.prepare = prepare
def run(self):
started = time.perf_counter()
payload = None
try:
if not hasattr(self.storage, "_startup_raw"):
self.storage._startup_raw = self.storage.state_file.read_text(encoding="utf-8") if self.storage.state_file.exists() else ""
payload = self.storage.load_payload(self.storage._startup_raw)
settings = AppSettings.from_dict(payload.get("settings") or {})
locked = bool((payload.get("security") or {}).get("enabled"))
self.early.emit(settings, locked)
if self.isInterruptionRequested():
return
if self.prepare:
self.prepare()
state = self.storage._normalize_state_paths(AppState.from_dict(payload))
history = TrafficHistoryStorage()
if not self.isInterruptionRequested():
self.loaded.emit(state, history)
except PassphraseRequired:
self.early.emit(AppSettings(), True)
self.password_needed.emit()
except Exception:
if payload is None and self.storage.passphrase:
self.password_needed.emit()
return
self.failed.emit("Не удалось загрузить данные приложения")
logging.getLogger("xray_fluent.bootstrap").error("Startup data preparation failed")
finally:
logging.getLogger("xray_fluent.bootstrap").info("startup_data_ms=%.1f", (time.perf_counter() - started) * 1000)
class StartupLoader(QObject):
early = pyqtSignal(object, bool)
loaded = pyqtSignal(object, object)
password_needed = pyqtSignal()
failed = pyqtSignal(str)
def __init__(self, parent=None, prepare=None):
super().__init__(parent)
self.storage = StateStorage()
self.prepare = prepare
self._workers = set()
self._closed = False
def start(self, passphrase=""):
if self._closed:
return
self.storage.passphrase = passphrase
worker = StartupWorker(self.storage, self.prepare, self)
self._workers.add(worker)
worker.early.connect(self.early)
worker.loaded.connect(self.loaded)
worker.password_needed.connect(self.password_needed)
worker.failed.connect(self.failed)
worker.finished.connect(lambda: self._finished(worker))
worker.start()
def _finished(self, worker):
self._workers.discard(worker)
worker.deleteLater()
def cancel(self):
self._closed = True
for worker in self._workers:
worker.requestInterruption()
def finish_shutdown(self):
self.cancel()
# Called after the GUI loop has exited: keep QThreads alive until completion.
for worker in tuple(self._workers):
worker.wait()
class MetadataWorker(QThread):
ready = pyqtSignal(object)
def __init__(self, xray_path, singbox_path, parent=None):
super().__init__(parent)
self.paths = (xray_path, singbox_path)
def run(self):
from ..engines.xray import get_xray_version
from ..engines.singbox.core_updater import installed_version
from ..engines.zapret import presets
result = {"xray": "", "singbox": "", "presets": []}
for key, call in (
("xray", lambda: get_xray_version(self.paths[0]) or ""),
("singbox", lambda: installed_version(Path(self.paths[1])) or ""),
("presets", presets.list_preset_infos),
):
if self.isInterruptionRequested():
return
try:
result[key] = call()
except Exception:
pass
self.ready.emit(result)