Инцидент 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>
63 lines
2.6 KiB
Python
63 lines
2.6 KiB
Python
"""Время жизни короткоживущих ``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)
|