zapret-kvn/xray_fluent/network/ping_worker.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

50 lines
1.7 KiB
Python

"""TCP-замер одного сервера. Очередь и потоки — в ``ping_service``."""
from __future__ import annotations
import socket
import time
from ..engines.singbox.config_builder import is_singbox_endpoint_node
from ..profiles.models import Node
def tcp_observation(host: str, port: int, timeout: float = 2.0) -> tuple[int | None, tuple[str, ...]]:
if not host or not port:
return None, ()
start = time.perf_counter()
try:
with socket.create_connection((host, port), timeout=timeout) as connection:
elapsed = int((time.perf_counter() - start) * 1000.0)
try:
peer = (str(connection.getpeername()[0]),)
except OSError:
peer = ()
return elapsed, peer
except OSError:
return None, ()
def tcp_ping(host: str, port: int, timeout: float = 2.0) -> int | None:
return tcp_observation(host, port, timeout)[0]
def ping_node(node: Node, timeout: float = 2.0) -> int | None:
"""TCP-пинг ноды; endpoint-ноды (UDP-only, напр. WireGuard/AWG) не пингуются."""
if is_singbox_endpoint_node(node):
return None
return tcp_ping(node.server, node.port, timeout)
def apply_ping_measurement(node: Node, ping_ms: int | None) -> None:
"""Применяет результат TCP-пинга к ноде.
Endpoint-ноды (UDP-only) не измеряются по TCP: ping_ms остаётся None,
is_alive не сбрасывается в False.
"""
if is_singbox_endpoint_node(node):
node.ping_ms = None
return
node.ping_ms = ping_ms
if ping_ms is not None or node.is_alive is None:
node.is_alive = ping_ms is not None