zapret-kvn/xray_fluent/application/update_service.py
loop-uh d301e8a49a Hysteria: рабочий сервер не заменяется из-за случайной ошибки; подписки — гонка маршрутов
Инцидент 01.10.2026 (диагностика 0.8.4): простаивавшая QUIC-сессия Hysteria2
оборвалась («timeout: no recent network activity»), официальный клиент
переподключился в ту же секунду, но приложение по одной строке лога запустило
восстановление: остановило туннель, взяло первый по порядку сервер (он
отвечал «authentication error, HTTP 301») и осталось отключённым до ручного
подключения. Пользователь видел это как «интернет отваливается и не встаёт».

Hysteria
- Ошибка транспорта в логе работающего туннеля — повод для проверки, а не
  приговор (HysteriaManager._begin_tunnel_check). Сервер остаётся, если он
  снова авторизовал клиента («connected to server») или HTTPS-проба через
  relay прошла; failure публикуется только после двух проваленных волн.
  Ошибки безопасности (TLS/pin/auth/obfs) по-прежнему публикуются сразу.
- Если замена всё же понадобилась и резервный сервер отказал, он исключается
  (auth/TLS — на час, сеть — на 5 минут), а приложение возвращается на
  выбранный пользователем сервер вместо отключения. То же при отсутствии
  резервного сервера. Ручное действие отменяет отложенный возврат.
- Кандидат с is_alive=False в замену не берётся.

Подписки
- fetch_subscription: прямой маршрут и VPN-прокси идут гонкой (фора прямого
  1 с) под общим дедлайном 30 с вместо последовательных попыток со сложением
  таймаутов; чтение тела ограничено тем же сроком. Ответ сервера напрямую
  остаётся окончательным, отказ через прокси ждёт прямой маршрут 3 с.
- Статус «Обновление…» не зависает: любая ошибка применения результата
  завершает обновление; в колонке виден этап и секунды; обновления пишутся
  в лог; выход из приложения отменяет загрузку.

Потоки
- Пинг — долгоживущая служба PingService без QThread; короткие QThread-воркеры
  живут в корзине до своего finished (worker_keeper). Убирает вылет
  «QThread: Destroyed while thread is still running».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:20:34 +03:00

94 lines
4.2 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.

from __future__ import annotations
from typing import TYPE_CHECKING
from ..engines.xray import XrayCoreUpdateResult, XrayCoreUpdateWorker
from .worker_keeper import start_kept
if TYPE_CHECKING:
from .controller import AppController
def run_xray_core_update(controller: AppController, apply_update: bool, silent: bool = False) -> None:
if controller._xray_update_worker and controller._xray_update_worker.isRunning():
if not silent:
controller.status.emit("info", "Обновление Xray уже выполняется")
return
if silent and apply_update and controller.connected:
controller._log("[core-update] silent auto-update skipped while connected")
return
if apply_update and controller.connected:
# Остановка перед заменой xray.exe — шаг координатора переходов, не
# синхронный disconnect_current() в GUI-потоке; воркер обновления
# стартует, когда подключение действительно остановлено.
def after_stop(stopped: bool) -> None:
if not stopped:
controller._reconnect_after_xray_update = False
if silent:
controller._log("[core-update] update cancelled: failed to stop active connection")
else:
controller.status.emit("error", "Не удалось остановить активное подключение перед обновлением Xray")
return
controller._reconnect_after_xray_update = True
_start_update_worker(controller, apply_update, silent)
if not controller._run_coordinated_stop("core update", after_stop):
message = "Дождитесь завершения текущего переключения и повторите обновление Xray"
if silent:
controller._log(f"[core-update] {message}")
else:
controller.status.emit("warning", message)
return
controller._reconnect_after_xray_update = False
_start_update_worker(controller, apply_update, silent)
def _start_update_worker(controller: AppController, apply_update: bool, silent: bool) -> None:
controller._xray_update_silent = silent
controller._xray_update_worker = XrayCoreUpdateWorker(
controller.state.settings.xray_path,
controller.state.settings.xray_release_channel,
controller.state.settings.xray_update_feed_url,
apply_update=apply_update,
)
controller._xray_update_worker.done.connect(controller._on_xray_update_worker_done)
start_kept(controller._background_workers, controller._xray_update_worker)
if not silent:
message = "Обновление Xray..." if apply_update else "Проверка обновлений Xray..."
controller.status.emit("info", message)
def on_xray_update_worker_done(controller: AppController, result: XrayCoreUpdateResult) -> None:
controller._xray_update_worker = None
controller.xray_update_result.emit(result)
if result.status == "error":
if not controller._xray_update_silent:
controller.status.emit("error", result.message)
else:
controller._log(f"[core-update] error: {result.message}")
elif result.status == "updated":
if not controller._xray_update_silent:
controller.status.emit("success", result.message)
controller._log(f"[core-update] {result.message}")
elif result.status == "available":
if not controller._xray_update_silent:
controller.status.emit("warning", result.message)
else:
controller._log(f"[core-update] {result.message}")
elif result.status == "up_to_date":
if not controller._xray_update_silent:
controller.status.emit("info", result.message)
else:
controller._log(f"[core-update] {result.message}")
if controller._reconnect_after_xray_update:
controller._reconnect_after_xray_update = False
controller._desired_connected = True
controller._request_transition("core update reconnect")
controller._xray_update_silent = False