zapret-kvn/xray_fluent/engines/singbox/config_check.py
loop-uh ab0384c1a5 Менеджеры ядер на шаговой модели: без блокировок и processEvents в GUI
sing-box, Hysteria, Amnezia и Zapret получили start_steps/stop_steps по
образцу XrayManager: ожидания процессов — сигналы QProcess (wait_process_*),
пробы портов/релея, sing-box check, поиск осиротевших процессов (PowerShell),
WinAPI-резолв аплинка и PUT Clash API — в run_in_worker, паузы — таймеры.
Синхронные start()/stop() остались драйверами тех же шагов для выхода из
приложения и тестов.

- sing-box: запись конфига + check в воркере, готовность прокси/TUN и снятие
  TUN-адаптера — шагами; should_continue прерывает устаревший старт без
  ложной ошибки; request_stop() — kill без ожидания для обработчиков сигналов.
- Clash API: select_outbound_when_ready_steps (попытка в воркере, пауза таймером).
- Hysteria: повтор без Chrome-parrot выполняется внутри идущего start_steps,
  а вне старта — собственным TransitionRunner, без вложенного start().
- Amnezia: physical_network_steps, ожидание relay_ready/рукопожатия шагами.
- Zapret: start() асинхронный (ожидание выхода прежнего winws2, поиск
  осиротевших, пауза драйвера — шаги), stop() без waitForFinished; блокирующее
  ожидание оставлено только для выхода (wait=True).
- async_steps: run_in_worker(executor=...), жёсткий дедлайн шага (60 с) в
  обоих драйверах; run_steps_blocking падает TypeError на не-генераторе и на
  не-шаге вместо бесконечного цикла (Mock.send() в тестах раздувал память).
2026-09-26 13:25:55 +03:00

82 lines
3.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.

"""Проверка конфигурации ядром до запуска процесса.
Без этой проверки единственным признаком негодного конфига был вывод самого
ядра: строка с ANSI-кодами вида ``[31mFATAL[0m[0000] decode config at ...``,
после которой процесс просто останавливался. Пользователь видел «не
запускается», а какое поле не понравилось ядру — приходилось вычитывать из лога.
"""
from __future__ import annotations
from pathlib import Path
import re
ANSI_ESCAPE = re.compile(r"\x1b?\[[0-9;]*m")
#: `outbounds[0].Tag: json: unknown field "Tag"` — путь к полю и его имя.
UNKNOWN_FIELD = re.compile(
r"(?P<path>[\w\[\]\.]*?):?\s*json: unknown field \"(?P<field>[^\"]+)\""
)
DECODE_PREFIX = re.compile(r"^.*?decode config at [^:]*:\s*", re.S)
def strip_ansi(text: str) -> str:
"""Убрать управляющие последовательности, которыми ядро красит вывод."""
return ANSI_ESCAPE.sub("", text).replace("\x1b", "")
def describe_config_failure(output: str) -> str:
"""Превратить вывод ядра в объяснение для пользователя."""
text = strip_ansi(str(output or "")).strip()
if not text:
return "Ядро не приняло конфигурацию и не объяснило причину."
first_line = text.splitlines()[0].strip()
body = DECODE_PREFIX.sub("", first_line).strip() or first_line
unknown = UNKNOWN_FIELD.search(body)
if unknown:
field = unknown.group("field")
path = unknown.group("path").strip(". ")
location = _location(path, field)
return (
f"Ядро не знает поле «{field}» {location}. "
"Оно не входит в схему sing-box: уберите его из конфигурации "
"или исправьте написание."
)
return f"Ядро не приняло конфигурацию: {body}"
def _location(path: str, field: str) -> str:
"""Описать, где именно лежит поле, по пути из сообщения ядра."""
trimmed = path
if trimmed.endswith(field):
trimmed = trimmed[: -len(field)].strip(". ")
if not trimmed:
return "в корне конфигурации"
return f"в «{trimmed}»"
def check_config(exe: Path, config_path: Path, *, timeout: float = 15.0) -> tuple[bool, str]:
"""Спросить ядро, годится ли конфигурация. Возвращает (ок, объяснение).
Блокирующий вызов: выполняется только в worker-пуле (SingBoxManager
отдаёт его в ``run_in_worker``), поэтому без прокачки Qt-событий.
"""
from ...platform.windows.subprocess_utils import CREATE_NO_WINDOW, result_output_text, run_text
try:
result = run_text(
[str(exe), "check", "-D", str(exe.parent), "-c", str(config_path)],
timeout=timeout,
creationflags=CREATE_NO_WINDOW or None,
)
except Exception as exc: # ядро может отсутствовать или не запуститься
return True, f"Проверка конфигурации не выполнена: {type(exc).__name__}: {exc}"
if result.returncode == 0:
return True, ""
return False, describe_config_failure(result_output_text(result))