zapret-kvn/xray_fluent/ui/qt_lifecycle.py
loop-uh 308f6da16a
Some checks failed
Windows project source guards / test (push) Failing after 38s
Маршрутизация: структурный редактор sing-box по схеме ядра
Раздел «Маршрутизация» с подпунктами (Обзор, Правила, Наборы правил, DNS,
Исходящие, Система, JSON) редактирует тот же активный native JSON.
Формы строятся из JSON Schema закреплённого ядра (sing-box schema),
своей модели маршрутизации нет; неизвестные ключи и форма Listable
сохраняются. «Проверить» запускает sing-box check в фоне.

DNS больше не перезаписывается принудительно: при обновлении шаблонов
нетронутые верхние секции следуют за стоком, изменённые остаются.
Сброс к стоку — вручную, целиком или по разделу.
2026-09-26 20:30:17 +03:00

109 lines
4.8 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.

"""Детерминированное завершение Qt: окна разрушаются, пока жив ``QApplication``.
Если оставить окна сборщику мусора, их C++-деструкторы срабатывают при
финализации интерпретатора в неопределённом порядке, иногда уже после
``QApplication``, и процесс падает на выходе (segfault / access violation).
Надеяться, что GC успеет раньше, нельзя: qfluentwidgets подключает к
глобальному ``qconfig.themeChanged`` лямбды, замкнутые на виджет
(``FluentLabelBase._init``), и такие обёртки живут до разрушения ``qconfig``.
Окно, у которого ещё работает дочерний ``QThread`` или ``QProcess``, не
удаляется: разрушение живого потока — ``qFatal``, а ``~QProcess`` убивает
процесс и ждёт его. Такое окно остаётся прежнему порядку выхода.
"""
from __future__ import annotations
import gc
import logging
from PyQt6 import sip
from PyQt6.QtCore import QProcess, QThread, QTimer
from PyQt6.QtWidgets import QApplication, QWidget
def dispose_windows(windows: list[QWidget], logger: logging.Logger | None = None) -> list[QWidget]:
"""Удаляет окна из ``windows``; возвращает те, что пришлось оставить.
На время каскада C++-деструкторов циклический GC выключен: иначе он может
собрать Python-обёртку виджета, который как раз разрушается (так падали
Windows-тесты при пакетном удалении страниц, см. test_app_scrollable_page).
"""
kept: list[QWidget] = []
gc_was_enabled = gc.isenabled()
gc.disable()
try:
for window in windows:
if sip.isdeleted(window):
continue
running = [thread for thread in window.findChildren(QThread) if thread.isRunning()]
running += [
process for process in window.findChildren(QProcess)
if process.state() != QProcess.ProcessState.NotRunning
]
if running:
if logger is not None:
logger.warning(
"Window kept until interpreter exit: %d background thread(s)/process(es) still running: %s",
len(running),
", ".join(type(item).__name__ for item in running),
)
kept.append(window)
continue
window.hide()
sip.delete(window)
finally:
if gc_was_enabled:
gc.enable()
windows[:] = kept
gc.collect()
return kept
def dispose_all_widgets(windows: list[QWidget], logger: logging.Logger | None = None) -> list[QWidget]:
"""Сначала главные окна, затем все оставшиеся виджеты верхнего уровня.
Вторым проходом уходят меню трея без родителя, всплывающие подсказки и
прочие окна qfluentwidgets, которые иначе дожили бы до финализации.
"""
kept = dispose_windows(windows, logger)
app = QApplication.instance()
if app is not None:
rest = [widget for widget in app.topLevelWidgets() if not any(widget is k for k in kept)]
kept += dispose_windows(rest, logger)
return kept
_disposal_queue: list[QWidget] = []
def dispose_later(widget: QWidget) -> None:
"""Hide ``widget`` now and destroy it on the next event-loop turn.
Replaces ``deleteLater`` for subtrees that hold qfluentwidgets scroll
delegates (``ScrollArea``, ``PlainTextEdit``, ``RoundMenu``…): those patch
bound methods onto their widget, so a cyclic-GC pass racing the Qt
destructors can touch a half-destroyed wrapper (see :func:`dispose_windows`).
The deferral lets the signal that triggered a rebuild return first; the
destruction itself runs synchronously with cyclic GC disabled.
"""
if sip.isdeleted(widget):
return
widget.hide()
_disposal_queue.append(widget)
if len(_disposal_queue) == 1:
QTimer.singleShot(0, _flush_disposals)
def _flush_disposals() -> None:
pending = list(_disposal_queue)
_disposal_queue.clear()
gc_was_enabled = gc.isenabled()
gc.disable()
try:
for widget in pending:
if not sip.isdeleted(widget):
sip.delete(widget)
finally:
if gc_was_enabled:
gc.enable()