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() в тестах раздувал память).
82 lines
3.6 KiB
Python
82 lines
3.6 KiB
Python
"""Проверка конфигурации ядром до запуска процесса.
|
||
|
||
Без этой проверки единственным признаком негодного конфига был вывод самого
|
||
ядра: строка с 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))
|