Инцидент 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>
94 lines
4.2 KiB
Python
94 lines
4.2 KiB
Python
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
|