refactor(xray-proxy): срезана accidental-сложность CLI, тестов и спеки
- Зачем:
- CLI и спека были переусложнены относительно принятой модели угроз
(один пользователь, доверенная машина): лишняя ось версий Xray и
защита от входов, которые не возникают при ручном редактировании.
- Что:
- убрана поддержка Xray 25.12.8 — поддерживается только 26.3.27;
allowInsecure валидируется, но никогда не эмитится в JSON.
- удалена проверка владения сокетом через /proc/net/tcp; идентичность
процесса и сверка адрес/порт со state сохранены.
- валидация INI/профиля/адреса/query сведена к отказам на реальные
опечатки; scoped IPv6 отклоняется одинаково на Python 3.8 и 3.9+.
- удалены fsync/O_EXCL-церемония, SIGALRM-таймер, zombie-инспекция,
мёртвый отпечаток профиля и двойной разбор extra/alpn.
- shell.bash принимает вывод env без ре-валидации, сохранив атомарность;
спека и issue 01–05 приведены к новому объёму (тикет 06, леджер).
- Проверка:
- python3 -m unittest discover -s tests — 34 теста OK.
- bash tests/test_proxy_launchers.sh — 8 проверок PASS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
71e16a795a
commit
98bfd6338a
12 files changed
+384
-508
No files matched your search
@@ -11,11 +11,9 @@ if sys.version_info < (3, 8):
|
||||
|
||||
import argparse
|
||||
import configparser
|
||||
import hashlib
|
||||
import http.client
|
||||
import ipaddress
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
@@ -34,8 +32,7 @@ DEFAULT_NAME = "Hiddify"
|
||||
DEFAULT_ADDRESS = "hproxy.local"
|
||||
DEFAULT_PORT = 12334
|
||||
DEFAULT_NO_PROXY_EXTRA = (".concept-software.ru",)
|
||||
DNS_LABEL = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$")
|
||||
VERSION_LINE = re.compile(r"^Xray (25\.12\.8|26\.3\.27)(?: .*)?$")
|
||||
VERSION_LINE = re.compile(r"^Xray (26\.3\.27)(?: .*)?$")
|
||||
QUERY_KEYS = {
|
||||
"type",
|
||||
"security",
|
||||
@@ -66,7 +63,6 @@ STATE_KEYS = {
|
||||
"config_path",
|
||||
"address",
|
||||
"port",
|
||||
"profile_fingerprint",
|
||||
}
|
||||
|
||||
|
||||
@@ -74,10 +70,6 @@ class ProxyError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PreflightExpired(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProxyConfig:
|
||||
def __init__(self, proxy_type, name, address, port, no_proxy_extra):
|
||||
self.proxy_type = proxy_type
|
||||
@@ -88,11 +80,13 @@ class ProxyConfig:
|
||||
|
||||
|
||||
class VlessProfile:
|
||||
def __init__(self, server, port, profile_id, query):
|
||||
def __init__(self, server, port, profile_id, query, alpn, extra):
|
||||
self.server = server
|
||||
self.port = port
|
||||
self.profile_id = profile_id
|
||||
self.query = query
|
||||
self.alpn = alpn
|
||||
self.extra = extra
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
@@ -133,27 +127,21 @@ def generated_config_path():
|
||||
|
||||
|
||||
def validate_address(value):
|
||||
if not value or not value.isascii():
|
||||
raise ProxyError("поле address должно содержать IPv4, IPv6 или ASCII DNS-имя")
|
||||
if any(character.isspace() for character in value):
|
||||
raise ProxyError("поле address не должно содержать пробелы")
|
||||
if any(separator in value for separator in ("%", "[", "]", "/", "\\", "@", "?", "#")):
|
||||
raise ProxyError(
|
||||
"поле address должно быть адресом без zone ID, скобок и URL-разделителей"
|
||||
)
|
||||
|
||||
try:
|
||||
return ipaddress.ip_address(value)
|
||||
parsed = ipaddress.ip_address(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if len(value) > 253:
|
||||
raise ProxyError("DNS-имя в поле address слишком длинное")
|
||||
labels = value.split(".")
|
||||
if not labels or any(not DNS_LABEL.fullmatch(label) for label in labels):
|
||||
raise ProxyError(
|
||||
"поле address должно быть адресом без схемы, порта, скобок и URL-разделителей"
|
||||
)
|
||||
else:
|
||||
if "%" in value:
|
||||
raise ProxyError("поле address не поддерживает IPv6 zone ID")
|
||||
return parsed
|
||||
if (
|
||||
not value
|
||||
or not value.isascii()
|
||||
or any(character.isspace() for character in value)
|
||||
or any(separator in value for separator in ("://", "@", "/", "[", "]", "%"))
|
||||
):
|
||||
raise ProxyError("поле address должно быть IP-адресом или простым ASCII-именем")
|
||||
return None
|
||||
|
||||
|
||||
@@ -186,15 +174,9 @@ def load_config():
|
||||
raise ProxyError("машинный конфиг не является обычным файлом: {}".format(path))
|
||||
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except OSError as error:
|
||||
raise ProxyError("не удалось прочитать машинный конфиг {}: {}".format(path, error))
|
||||
if b"\0" in raw:
|
||||
raise ProxyError("машинный конфиг содержит запрещённый NUL")
|
||||
try:
|
||||
text = raw.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
raise ProxyError("машинный конфиг должен быть в UTF-8")
|
||||
|
||||
parser = configparser.ConfigParser(
|
||||
interpolation=None,
|
||||
@@ -202,14 +184,11 @@ def load_config():
|
||||
allow_no_value=False,
|
||||
empty_lines_in_values=False,
|
||||
)
|
||||
parser.optionxform = str
|
||||
try:
|
||||
parser.read_string(text, source=str(path))
|
||||
except configparser.Error as error:
|
||||
raise ProxyError("неверный машинный INI: {}".format(error))
|
||||
|
||||
if parser.defaults():
|
||||
raise ProxyError("машинный INI не допускает секцию [DEFAULT]")
|
||||
if parser.sections() != ["proxy"]:
|
||||
raise ProxyError("машинный INI должен содержать только секцию [proxy]")
|
||||
|
||||
@@ -226,8 +205,6 @@ def load_config():
|
||||
raise ProxyError("неверные ключи [proxy] ({})".format("; ".join(details)))
|
||||
|
||||
values = {key: section.get(key, raw=True) for key in CONFIG_KEYS}
|
||||
if any("\n" in value or "\r" in value or "\0" in value for value in values.values()):
|
||||
raise ProxyError("многострочные значения в машинном INI запрещены")
|
||||
|
||||
proxy_type = values["type"].strip()
|
||||
if proxy_type not in ("external", "xray"):
|
||||
@@ -255,35 +232,17 @@ def preflight_timeout():
|
||||
raw = os.environ.get("PROXY_PREFLIGHT_TIMEOUT", "2")
|
||||
try:
|
||||
timeout = float(raw)
|
||||
if not timeout > 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
raise ProxyError("PROXY_PREFLIGHT_TIMEOUT должен быть положительным числом секунд")
|
||||
if not math.isfinite(timeout) or timeout <= 0:
|
||||
raise ProxyError("PROXY_PREFLIGHT_TIMEOUT должен быть положительным числом секунд")
|
||||
return timeout
|
||||
|
||||
|
||||
def check_reachable(config, timeout):
|
||||
def expire_preflight(_signal_number, _frame):
|
||||
raise PreflightExpired()
|
||||
|
||||
connection = None
|
||||
previous_handler = signal.signal(signal.SIGALRM, expire_preflight)
|
||||
try:
|
||||
signal.setitimer(signal.ITIMER_REAL, timeout)
|
||||
connection = socket.create_connection((config.address, config.port), timeout=timeout)
|
||||
signal.setitimer(signal.ITIMER_REAL, 0)
|
||||
except PreflightExpired:
|
||||
raise ProxyError(
|
||||
"прокси {} ({}:{}) недоступен: таймаут {} с".format(
|
||||
config.name, config.address, config.port, timeout
|
||||
)
|
||||
)
|
||||
except (OverflowError, ValueError) as error:
|
||||
raise ProxyError(
|
||||
"PROXY_PREFLIGHT_TIMEOUT={} нельзя применить на этой платформе: {}".format(
|
||||
timeout, error
|
||||
)
|
||||
)
|
||||
except OSError as error:
|
||||
raise ProxyError(
|
||||
"прокси {} ({}:{}) недоступен: {}".format(
|
||||
@@ -291,8 +250,6 @@ def check_reachable(config, timeout):
|
||||
)
|
||||
)
|
||||
finally:
|
||||
signal.setitimer(signal.ITIMER_REAL, 0)
|
||||
signal.signal(signal.SIGALRM, previous_handler)
|
||||
if connection is not None:
|
||||
connection.close()
|
||||
|
||||
@@ -341,14 +298,9 @@ def private_directory(path):
|
||||
|
||||
def write_private_json(path, value):
|
||||
private_directory(path.parent)
|
||||
temporary = path.with_name(path.name + ".tmp.{}".format(os.getpid()))
|
||||
descriptor = None
|
||||
try:
|
||||
descriptor = os.open(
|
||||
str(temporary), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600
|
||||
)
|
||||
descriptor = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
|
||||
descriptor = None
|
||||
json.dump(
|
||||
value,
|
||||
stream,
|
||||
@@ -357,19 +309,9 @@ def write_private_json(path, value):
|
||||
allow_nan=False,
|
||||
)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(str(temporary), str(path))
|
||||
path.chmod(0o600)
|
||||
except (OSError, ValueError) as error:
|
||||
raise ProxyError("не удалось записать закрытый служебный файл: {}".format(error))
|
||||
finally:
|
||||
if descriptor is not None:
|
||||
os.close(descriptor)
|
||||
try:
|
||||
temporary.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def read_profile(path):
|
||||
@@ -385,25 +327,16 @@ def read_profile(path):
|
||||
"конечный файл профиля доступен группе или другим пользователям; выполните chmod 600"
|
||||
)
|
||||
try:
|
||||
raw = resolved.read_bytes()
|
||||
text = resolved.read_text(encoding="utf-8")
|
||||
except OSError as error:
|
||||
raise ProxyError("не удалось прочитать файл профиля: {}".format(error))
|
||||
if b"\0" in raw:
|
||||
raise ProxyError("файл профиля содержит запрещённый NUL")
|
||||
try:
|
||||
text = raw.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
raise ProxyError("файл профиля должен быть в UTF-8")
|
||||
if text.endswith("\n"):
|
||||
text = text[:-1]
|
||||
if not text or "\n" in text or "\r" in text or text != text.strip():
|
||||
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
||||
if len(lines) != 1:
|
||||
raise ProxyError("файл профиля должен содержать ровно одну непустую VLESS-ссылку")
|
||||
return text
|
||||
return lines[0]
|
||||
|
||||
|
||||
def parse_query(raw_query):
|
||||
if re.search(r"%(?![0-9A-Fa-f]{2})", raw_query):
|
||||
raise ProxyError("профиль содержит неверное URL-кодирование")
|
||||
try:
|
||||
pairs = parse_qsl(
|
||||
raw_query,
|
||||
@@ -416,8 +349,6 @@ def parse_query(raw_query):
|
||||
raise ProxyError("профиль содержит неверную строку параметров")
|
||||
query = {}
|
||||
for key, value in pairs:
|
||||
if any(ord(character) < 32 or ord(character) == 127 for character in key + value):
|
||||
raise ProxyError("профиль содержит управляющий символ")
|
||||
if key in query:
|
||||
raise ProxyError("профиль содержит повтор query-параметра")
|
||||
if key not in QUERY_KEYS:
|
||||
@@ -471,7 +402,7 @@ def parse_json_object(value):
|
||||
return parsed
|
||||
|
||||
|
||||
def parse_vless(url, version):
|
||||
def parse_vless(url):
|
||||
try:
|
||||
parsed = urlsplit(url)
|
||||
except ValueError:
|
||||
@@ -509,6 +440,8 @@ def parse_vless(url, version):
|
||||
if "core" in query and query["core"] != "xray":
|
||||
raise ProxyError("поддерживается только метаданные core=xray")
|
||||
legacy_insecure = parse_boolean_fields(query)
|
||||
alpn = None
|
||||
extra = None
|
||||
|
||||
if query["type"] == "tcp" and query["security"] == "reality":
|
||||
required(query, "flow", "pbk", "sid")
|
||||
@@ -519,8 +452,6 @@ def parse_vless(url, version):
|
||||
required(query, "host", "path")
|
||||
if "flow" in query or "pbk" in query or "sid" in query:
|
||||
raise ProxyError("параметры профиля несовместимы с XHTTP/TLS")
|
||||
if version == "25.12.8" and ("pcs" in query or "vcn" in query):
|
||||
raise ProxyError("pcs/vcn несовместимы с Xray 25.12.8")
|
||||
if "pcs" in query:
|
||||
hashes = list_value(query["pcs"])
|
||||
if any(re.fullmatch(r"[0-9A-Fa-f]{64}", item) is None for item in hashes):
|
||||
@@ -528,15 +459,15 @@ def parse_vless(url, version):
|
||||
if "vcn" in query:
|
||||
list_value(query["vcn"])
|
||||
if "alpn" in query:
|
||||
alpn = query["alpn"].split(",")
|
||||
alpn = list_value(query["alpn"])
|
||||
if not alpn or any(item not in ALPN_VALUES for item in alpn):
|
||||
raise ProxyError("профиль содержит неподдерживаемый ALPN")
|
||||
if "extra" in query:
|
||||
parse_json_object(query["extra"])
|
||||
extra = parse_json_object(query["extra"])
|
||||
else:
|
||||
raise ProxyError("поддерживаются только TCP/REALITY и XHTTP/TLS")
|
||||
|
||||
profile = VlessProfile(server, port, profile_id, query)
|
||||
profile = VlessProfile(server, port, profile_id, query, alpn, extra)
|
||||
profile.legacy_insecure = legacy_insecure
|
||||
return profile
|
||||
|
||||
@@ -548,7 +479,7 @@ def list_value(value):
|
||||
return items
|
||||
|
||||
|
||||
def build_xray_config(profile, local_address, local_port, version):
|
||||
def build_xray_config(profile, local_address, local_port):
|
||||
query = profile.query
|
||||
settings = {
|
||||
"address": profile.server,
|
||||
@@ -574,18 +505,15 @@ def build_xray_config(profile, local_address, local_port, version):
|
||||
xhttp = {"host": query["host"], "path": query["path"]}
|
||||
if "mode" in query:
|
||||
xhttp["mode"] = query["mode"]
|
||||
if "extra" in query:
|
||||
xhttp["extra"] = parse_json_object(query["extra"])
|
||||
if profile.extra is not None:
|
||||
xhttp["extra"] = profile.extra
|
||||
tls = {"serverName": query["sni"], "fingerprint": query["fp"]}
|
||||
if "alpn" in query:
|
||||
tls["alpn"] = list_value(query["alpn"])
|
||||
if version == "26.3.27":
|
||||
if "pcs" in query:
|
||||
tls["pinnedPeerCertSha256"] = query["pcs"]
|
||||
if "vcn" in query:
|
||||
tls["verifyPeerCertByName"] = query["vcn"]
|
||||
elif profile.legacy_insecure:
|
||||
tls["allowInsecure"] = True
|
||||
if profile.alpn is not None:
|
||||
tls["alpn"] = profile.alpn
|
||||
if "pcs" in query:
|
||||
tls["pinnedPeerCertSha256"] = query["pcs"]
|
||||
if "vcn" in query:
|
||||
tls["verifyPeerCertByName"] = query["vcn"]
|
||||
stream = {
|
||||
"network": "xhttp",
|
||||
"security": "tls",
|
||||
@@ -660,12 +588,9 @@ def run_xray_test(path, config_path):
|
||||
|
||||
def process_alive(pid):
|
||||
try:
|
||||
stat_fields = Path("/proc/{}/stat".format(pid)).read_text(encoding="ascii").split()
|
||||
if len(stat_fields) > 2 and stat_fields[2] == "Z":
|
||||
return False
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except (OSError, ValueError):
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
@@ -706,60 +631,6 @@ def process_argument_resolves_to(pid, arguments, expected_path):
|
||||
return False
|
||||
|
||||
|
||||
def process_socket_inodes(pid):
|
||||
inodes = set()
|
||||
try:
|
||||
descriptors = Path("/proc/{}/fd".format(pid)).iterdir()
|
||||
for descriptor in descriptors:
|
||||
try:
|
||||
target = os.readlink(str(descriptor))
|
||||
except OSError:
|
||||
continue
|
||||
match = re.fullmatch(r"socket:\[([0-9]+)\]", target)
|
||||
if match is not None:
|
||||
inodes.add(match.group(1))
|
||||
except OSError:
|
||||
return set()
|
||||
return inodes
|
||||
|
||||
|
||||
def decode_proc_address(encoded):
|
||||
raw = bytes.fromhex(encoded)
|
||||
if len(raw) == 4:
|
||||
raw = raw[::-1]
|
||||
elif len(raw) == 16:
|
||||
raw = b"".join(raw[index : index + 4][::-1] for index in range(0, 16, 4))
|
||||
else:
|
||||
raise ValueError("unexpected proc address size")
|
||||
return ipaddress.ip_address(raw)
|
||||
|
||||
|
||||
def process_owns_listener(pid, address, port):
|
||||
inodes = process_socket_inodes(pid)
|
||||
if not inodes:
|
||||
return False
|
||||
expected_address = ipaddress.ip_address(address)
|
||||
for table_name in ("tcp", "tcp6"):
|
||||
table = Path("/proc/{}/net/{}".format(pid, table_name))
|
||||
try:
|
||||
lines = table.read_text(encoding="ascii").splitlines()[1:]
|
||||
except OSError:
|
||||
continue
|
||||
for line in lines:
|
||||
fields = line.split()
|
||||
if len(fields) < 10 or fields[3] != "0A" or fields[9] not in inodes:
|
||||
continue
|
||||
encoded_address, encoded_port = fields[1].split(":", 1)
|
||||
try:
|
||||
local_address = decode_proc_address(encoded_address)
|
||||
local_port = int(encoded_port, 16)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
if local_address == expected_address and local_port == port:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def read_managed_state():
|
||||
path = state_path()
|
||||
try:
|
||||
@@ -779,7 +650,7 @@ def read_managed_state():
|
||||
raise ProxyError("состояние управляемого Xray не подтверждено")
|
||||
if not all(isinstance(value[key], int) and not isinstance(value[key], bool) for key in ("supervisor_pid", "xray_pid", "port")):
|
||||
raise ProxyError("состояние управляемого Xray не подтверждено")
|
||||
if not all(isinstance(value[key], str) for key in ("xray_path", "config_path", "address", "profile_fingerprint")):
|
||||
if not all(isinstance(value[key], str) for key in ("xray_path", "config_path", "address")):
|
||||
raise ProxyError("состояние управляемого Xray не подтверждено")
|
||||
return value
|
||||
|
||||
@@ -803,11 +674,8 @@ def verify_managed_state(config=None, require_port=True):
|
||||
or "-c" not in child_args
|
||||
or state["config_path"] not in child_args
|
||||
or process_parent(child) != supervisor
|
||||
or not process_owns_listener(child, state["address"], state["port"])
|
||||
):
|
||||
raise ProxyError(
|
||||
"личность процесса или listener управляемого Xray не подтверждены"
|
||||
)
|
||||
raise ProxyError("личность процесса управляемого Xray не подтверждена")
|
||||
if config is not None and (
|
||||
config.proxy_type != "xray"
|
||||
or config.address != state["address"]
|
||||
@@ -819,9 +687,8 @@ def verify_managed_state(config=None, require_port=True):
|
||||
return state
|
||||
|
||||
|
||||
def safe_summary(profile, local_port, xray_path, version):
|
||||
def safe_summary(profile, local_port):
|
||||
query = profile.query
|
||||
print("Xray: {} ({})".format(xray_path, version), flush=True)
|
||||
print(
|
||||
"Профиль: сервер {}:{}, транспорт {}, защита {}, SNI {}, fingerprint {}, локальный порт {}".format(
|
||||
profile.server,
|
||||
@@ -848,15 +715,11 @@ def safe_summary(profile, local_port, xray_path, version):
|
||||
print("UUID и ключи: скрыты", flush=True)
|
||||
|
||||
|
||||
def warn_legacy(profile, version):
|
||||
if version == "26.3.27" and profile.legacy_insecure:
|
||||
def warn_legacy(profile):
|
||||
if profile.legacy_insecure:
|
||||
sys.stderr.write(
|
||||
"Предупреждение: legacy insecure не переносится; сертификат проверяется обычным способом.\n"
|
||||
)
|
||||
if version == "25.12.8" and profile.legacy_insecure:
|
||||
sys.stderr.write(
|
||||
"Предупреждение: Xray 25.12.8 использует allowInsecure=true; проверка сертификата отключена.\n"
|
||||
)
|
||||
|
||||
|
||||
def diagnose_ip(config):
|
||||
@@ -967,13 +830,14 @@ def command_start(config, profile_file):
|
||||
raw_profile = read_profile(profile_path)
|
||||
executable = resolve_xray()
|
||||
version = xray_version(executable)
|
||||
profile = parse_vless(raw_profile, version)
|
||||
generated = build_xray_config(profile, config.address, config.port, version)
|
||||
profile = parse_vless(raw_profile)
|
||||
generated = build_xray_config(profile, config.address, config.port)
|
||||
write_private_json(generated_config_path(), generated)
|
||||
try:
|
||||
run_xray_test(executable, generated_config_path())
|
||||
safe_summary(profile, config.port, executable, version)
|
||||
warn_legacy(profile, version)
|
||||
print("Xray: {} ({})".format(executable, version), flush=True)
|
||||
safe_summary(profile, config.port)
|
||||
warn_legacy(profile)
|
||||
stopping = {"signal": None}
|
||||
|
||||
def request_stop(signal_number, _frame):
|
||||
@@ -993,7 +857,6 @@ def command_start(config, profile_file):
|
||||
"config_path": str(generated_config_path()),
|
||||
"address": config.address,
|
||||
"port": config.port,
|
||||
"profile_fingerprint": hashlib.sha256(raw_profile.encode("utf-8")).hexdigest(),
|
||||
}
|
||||
write_private_json(state_path(), state)
|
||||
deadline = time.monotonic() + max(1.0, preflight_timeout())
|
||||
@@ -1003,19 +866,13 @@ def command_start(config, profile_file):
|
||||
raise ProxyError("Xray завершился до готовности локального порта")
|
||||
try:
|
||||
check_endpoint(config.address, config.port)
|
||||
if not process_owns_listener(child.pid, config.address, config.port):
|
||||
raise ProxyError(
|
||||
"настроенный listener не принадлежит дочернему Xray"
|
||||
)
|
||||
last_error = None
|
||||
break
|
||||
except ProxyError as error:
|
||||
last_error = error
|
||||
time.sleep(0.03)
|
||||
if last_error is not None:
|
||||
raise ProxyError(
|
||||
"дочерний Xray не подтвердил владение настроенным listener"
|
||||
)
|
||||
raise ProxyError("локальный порт дочернего Xray не стал доступен")
|
||||
diagnose_ip(config)
|
||||
while child.poll() is None and stopping["signal"] is None:
|
||||
time.sleep(0.05)
|
||||
@@ -1050,10 +907,10 @@ def stop_timeout():
|
||||
raw = os.environ.get("XRAY_PROXY_STOP_TIMEOUT", "5")
|
||||
try:
|
||||
value = float(raw)
|
||||
if not value > 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
raise ProxyError("XRAY_PROXY_STOP_TIMEOUT должен быть положительным числом секунд")
|
||||
if not math.isfinite(value) or value <= 0:
|
||||
raise ProxyError("XRAY_PROXY_STOP_TIMEOUT должен быть положительным числом секунд")
|
||||
return value
|
||||
|
||||
|
||||
@@ -1066,11 +923,7 @@ def command_stop():
|
||||
raise ProxyError("не удалось послать сигнал подтверждённому супервизору")
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if (
|
||||
not process_alive(state["supervisor_pid"])
|
||||
and not process_alive(state["xray_pid"])
|
||||
and not state_path().exists()
|
||||
):
|
||||
if not process_alive(state["xray_pid"]) and not state_path().exists():
|
||||
print("Управляемый Xray остановлен")
|
||||
return 0
|
||||
time.sleep(0.05)
|
||||
|
||||
Reference in new issue
Block a user