zapret-kvn/xray_fluent/application/worker_keeper.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

63 lines
2.6 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.

"""Время жизни короткоживущих ``QThread``-воркеров.
Правило одно: сильную ссылку на поток отпускает только его собственный
``QThread.finished`` — он испускается после выхода из ``run()``. Слот любого
своего сигнала воркера (``completed``, ``result``, ``done``…) вызывается,
пока ``run()`` ещё не вернулся: если там обнулить последнюю ссылку, Python
удалит работающий поток, и Qt завершит процесс
(``QThread: Destroyed while thread is still running``).
Поля контроллера вроде ``_speed_worker`` остаются «текущей задачей» для
проверок занятости и фильтра ``sender()``; жизнь потока держит корзина.
"""
from __future__ import annotations
from typing import Any
def keep_until_finished(bucket: list, worker: Any) -> None:
"""Держать ``worker`` в ``bucket`` до его ``finished``, затем ``deleteLater``."""
if any(item is worker for item in bucket):
return
bucket.append(worker)
finished = getattr(worker, "finished", None)
if finished is None:
return
def _release(_done: list[bool] = []) -> None:
if _done:
return
_done.append(True)
# finished испускается из хвоста QThreadPrivate::finish, когда поток
# формально ещё «running»; этот хвост — микросекунды, дождаться его.
is_running = getattr(worker, "isRunning", None)
if callable(is_running) and is_running():
worker.wait()
for index, item in enumerate(bucket):
if item is worker:
del bucket[index]
break
delete_later = getattr(worker, "deleteLater", None)
if callable(delete_later):
delete_later()
finished.connect(_release)
def start_kept(bucket: list, worker: Any) -> Any:
"""Взять воркер под охрану и запустить его."""
keep_until_finished(bucket, worker)
worker.start()
return worker
def wait_all(bucket: list, timeout_ms: int) -> None:
"""Выход из приложения: дождаться оставшихся потоков (блокирующий путь)."""
for worker in list(bucket):
is_running = getattr(worker, "isRunning", None)
if callable(is_running) and is_running():
worker.wait(timeout_ms)