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