feat(xray-proxy): добавлен безопасный жизненный цикл управляемого Xray
- Зачем:
- stop/status опирались на обход argv и могли счесть чужой или
переиспользованный PID управляемым процессом, а параллельный start
и аварийная гибель супервизора не имели определённого поведения.
- Что:
- введена модель идентичности: PID + starttime (поле 22 /proc/PID/stat),
/proc/PID/exe для ребёнка, канонический путь скрипта для супервизора,
boot_id как граница загрузки; перечитка перед каждым сигналом.
- добавлены lifetime-flock супервизора, три исхода проверки состояния
(здоровая пара / подтверждённый сирота / непроверяемое), отказ при
занятом порте пробным bind и очистка stale только под lock.
- тесты расширены до 49 (blank flock, подмена starttime, чужой exe,
boot_id, SIGKILL супервизора, занятый порт, парсер stat); спека
приведена к модели идентичности; тикет 03 закрыт, леджер дополнен.
- Проверка:
- python3 -m unittest discover -s tests — 49 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
f2d303c233
commit
5ace0cfae0
6 files changed
+890
-100
No files matched your search
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import runpy
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -154,6 +156,88 @@ class XrayProxyCliTest(unittest.TestCase):
|
||||
self.assertIn("требует Python 3.8", result.stderr)
|
||||
self.assertIn("Python 3.7", result.stderr)
|
||||
|
||||
def test_proc_stat_parser_handles_spaces_and_closing_parentheses_in_comm(self):
|
||||
module = runpy.run_path(str(CLI))
|
||||
parse_starttime = module["parse_proc_stat_starttime"]
|
||||
tail = ["S"] + [str(value) for value in range(4, 22)] + ["987654"]
|
||||
|
||||
for comm in ("worker", "worker name", "worker name ) with parens"):
|
||||
with self.subTest(comm=comm):
|
||||
self.assertEqual(
|
||||
987654,
|
||||
parse_starttime("321 ({}) ".format(comm) + " ".join(tail)),
|
||||
)
|
||||
for malformed in ("321 worker S 1 2", "321 (worker) S 1 2"):
|
||||
with self.subTest(malformed=malformed):
|
||||
with self.assertRaises(ValueError):
|
||||
parse_starttime(malformed)
|
||||
|
||||
def test_process_permission_error_is_not_treated_as_dead(self):
|
||||
module = runpy.run_path(str(CLI))
|
||||
with mock.patch.object(module["Path"], "stat", side_effect=PermissionError()):
|
||||
self.assertTrue(module["process_alive"](12345))
|
||||
|
||||
def test_same_pid_cannot_be_a_healthy_managed_pair(self):
|
||||
module = runpy.run_path(str(CLI))
|
||||
function_globals = module["managed_state_outcome"].__globals__
|
||||
state = {"supervisor_pid": 12345, "xray_pid": 12345, "boot_id": "boot"}
|
||||
replacements = {
|
||||
"read_boot_id": mock.Mock(return_value="boot"),
|
||||
"process_alive": mock.Mock(return_value=True),
|
||||
"supervisor_identity_matches": mock.Mock(return_value=True),
|
||||
"xray_identity_matches": mock.Mock(return_value=True),
|
||||
}
|
||||
with mock.patch.dict(function_globals, replacements):
|
||||
self.assertEqual("unverifiable", module["managed_state_outcome"](state))
|
||||
|
||||
def test_identity_reads_starttime_after_process_paths(self):
|
||||
module = runpy.run_path(str(CLI))
|
||||
function_globals = module["xray_identity_matches"].__globals__
|
||||
state = {
|
||||
"supervisor_pid": 101,
|
||||
"supervisor_starttime": 201,
|
||||
"supervisor_path": "/cli",
|
||||
"xray_pid": 102,
|
||||
"xray_starttime": 202,
|
||||
"xray_path": "/xray",
|
||||
}
|
||||
|
||||
xray_events = []
|
||||
xray_replacements = {
|
||||
"process_executable": lambda _pid: xray_events.append("exe") or "/xray",
|
||||
"process_starttime": lambda _pid: xray_events.append("stat") or 202,
|
||||
}
|
||||
with mock.patch.dict(function_globals, xray_replacements):
|
||||
self.assertTrue(module["xray_identity_matches"](state))
|
||||
self.assertEqual(["exe", "stat"], xray_events)
|
||||
|
||||
supervisor_events = []
|
||||
supervisor_replacements = {
|
||||
"process_cmdline": lambda _pid: supervisor_events.append("cmdline")
|
||||
or ["python", "/cli"],
|
||||
"process_argument_resolves_to": lambda *_args: supervisor_events.append(
|
||||
"script"
|
||||
)
|
||||
or True,
|
||||
"process_starttime": lambda _pid: supervisor_events.append("stat") or 201,
|
||||
}
|
||||
with mock.patch.dict(function_globals, supervisor_replacements):
|
||||
self.assertTrue(module["supervisor_identity_matches"](state))
|
||||
self.assertEqual(["cmdline", "script", "stat"], supervisor_events)
|
||||
|
||||
def test_initial_child_starttime_retries_while_child_is_alive(self):
|
||||
module = runpy.run_path(str(CLI))
|
||||
wait_for_starttime = module["wait_for_child_starttime"]
|
||||
function_globals = wait_for_starttime.__globals__
|
||||
child = mock.Mock(pid=12345)
|
||||
child.poll.return_value = None
|
||||
replacements = {
|
||||
"process_starttime": mock.Mock(side_effect=[None, 7654321]),
|
||||
"time": mock.Mock(),
|
||||
}
|
||||
with mock.patch.dict(function_globals, replacements):
|
||||
self.assertEqual(7654321, wait_for_starttime(child))
|
||||
|
||||
def test_broken_config_symlink_is_not_treated_as_missing_config(self):
|
||||
config_dir = self.config_home / "proxy"
|
||||
config_dir.mkdir()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
@@ -22,8 +24,7 @@ PCS_A = "a" * 64
|
||||
PCS_B = "b" * 64
|
||||
|
||||
|
||||
FAKE_XRAY = r'''#!/usr/bin/env python3
|
||||
import json
|
||||
FAKE_XRAY_OPERAND = r'''import json
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
@@ -32,21 +33,22 @@ import sys
|
||||
import threading
|
||||
|
||||
log_path = os.environ["FAKE_XRAY_LOG"]
|
||||
operation = os.path.basename(sys.argv[0])
|
||||
|
||||
def record(kind, **values):
|
||||
values["kind"] = kind
|
||||
values["argv"] = sys.argv[1:]
|
||||
values["executable"] = os.path.realpath(sys.argv[0])
|
||||
values["argv"] = [operation] + sys.argv[1:]
|
||||
values["executable"] = os.path.realpath("/proc/self/exe")
|
||||
with open(log_path, "a", encoding="utf-8") as stream:
|
||||
stream.write(json.dumps(values, sort_keys=True) + "\n")
|
||||
|
||||
if sys.argv[1:] == ["version"]:
|
||||
if operation == "version" and not sys.argv[1:]:
|
||||
record("version")
|
||||
print(os.environ["FAKE_XRAY_VERSION"])
|
||||
raise SystemExit(int(os.environ.get("FAKE_VERSION_EXIT", "0")))
|
||||
|
||||
is_test = len(sys.argv) == 5 and sys.argv[1:4] == ["run", "-test", "-c"]
|
||||
is_run = len(sys.argv) == 4 and sys.argv[1:3] == ["run", "-c"]
|
||||
is_test = operation == "run" and len(sys.argv) == 4 and sys.argv[1:3] == ["-test", "-c"]
|
||||
is_run = operation == "run" and len(sys.argv) == 3 and sys.argv[1] == "-c"
|
||||
if not (is_test or is_run):
|
||||
record("bad-argv")
|
||||
raise SystemExit(64)
|
||||
@@ -73,7 +75,7 @@ if os.environ["FAKE_XRAY_VERSION"].startswith("Xray 26.3.27"):
|
||||
):
|
||||
raise SystemExit(65)
|
||||
|
||||
if sys.argv[2] == "-test":
|
||||
if is_test:
|
||||
record("test", config=config)
|
||||
if os.environ.get("FAKE_TEST_FAIL") == "1":
|
||||
sys.stderr.write(os.environ.get("FAKE_TEST_SECRET", "test failed"))
|
||||
@@ -92,6 +94,8 @@ stopping = False
|
||||
def stop(signum, _frame):
|
||||
global stopping
|
||||
record("signal", signum=signum)
|
||||
if signum == signal.SIGTERM and os.environ.get("FAKE_IGNORE_TERM") == "1":
|
||||
return
|
||||
stopping = True
|
||||
|
||||
signal.signal(signal.SIGTERM, stop)
|
||||
@@ -162,8 +166,10 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
self.bin_dir.mkdir()
|
||||
self.log = self.root / "xray.log"
|
||||
self.xray = self.bin_dir / "xray"
|
||||
self.xray.write_text(FAKE_XRAY, encoding="utf-8")
|
||||
shutil.copy2(sys.executable, self.xray)
|
||||
self.xray.chmod(0o755)
|
||||
for operand in ("version", "run"):
|
||||
(self.root / operand).write_text(FAKE_XRAY_OPERAND, encoding="utf-8")
|
||||
self.environment = os.environ.copy()
|
||||
self.environment.update(
|
||||
{
|
||||
@@ -238,6 +244,7 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
env.update(environment)
|
||||
return subprocess.run(
|
||||
[str(CLI)] + list(args),
|
||||
cwd=str(self.root),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=env,
|
||||
@@ -264,12 +271,25 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
return []
|
||||
return [json.loads(line) for line in self.log.read_text().splitlines()]
|
||||
|
||||
def managed_state_path(self):
|
||||
return self.state_home / "xray-proxy/state.json"
|
||||
|
||||
def read_state(self):
|
||||
return json.loads(self.managed_state_path().read_text(encoding="utf-8"))
|
||||
|
||||
def write_state(self, state):
|
||||
self.managed_state_path().write_text(
|
||||
json.dumps(state) + "\n", encoding="utf-8"
|
||||
)
|
||||
self.managed_state_path().chmod(0o600)
|
||||
|
||||
def start(self, profile=None):
|
||||
args = [str(CLI), "start"]
|
||||
if profile is not None:
|
||||
args.extend(["-f", str(profile)])
|
||||
process = subprocess.Popen(
|
||||
args,
|
||||
cwd=str(self.root),
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
@@ -373,10 +393,13 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
process = self.start()
|
||||
state_path = self.state_home / "xray-proxy/state.json"
|
||||
config_path = self.state_home / "xray-proxy/client.json"
|
||||
lock_path = self.state_home / "xray-proxy/lock"
|
||||
state = json.loads(state_path.read_text(encoding="utf-8"))
|
||||
config = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(0o600, state_path.stat().st_mode & 0o777)
|
||||
self.assertEqual(0o600, config_path.stat().st_mode & 0o777)
|
||||
self.assertEqual(0o600, lock_path.stat().st_mode & 0o777)
|
||||
self.assertEqual("", lock_path.read_text(encoding="ascii"))
|
||||
self.assertEqual(0o700, state_path.parent.stat().st_mode & 0o777)
|
||||
self.assertEqual(str(self.xray.resolve()), state["xray_path"])
|
||||
self.assertEqual("127.0.0.1", state["address"])
|
||||
@@ -389,9 +412,20 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
"config_path",
|
||||
"address",
|
||||
"port",
|
||||
"supervisor_path",
|
||||
"supervisor_starttime",
|
||||
"xray_starttime",
|
||||
"boot_id",
|
||||
},
|
||||
set(state),
|
||||
)
|
||||
self.assertEqual(str(CLI.resolve()), state["supervisor_path"])
|
||||
self.assertIsInstance(state["supervisor_starttime"], int)
|
||||
self.assertIsInstance(state["xray_starttime"], int)
|
||||
self.assertEqual(
|
||||
Path("/proc/sys/kernel/random/boot_id").read_text(encoding="ascii").strip(),
|
||||
state["boot_id"],
|
||||
)
|
||||
state_text = state_path.read_text(encoding="utf-8")
|
||||
self.assert_no_secrets(state_text, profile_text)
|
||||
stream = config["outbounds"][0]["streamSettings"]
|
||||
@@ -415,6 +449,283 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
argv_text = "\0".join(entry["argv"])
|
||||
self.assert_no_secrets(argv_text, profile_text)
|
||||
|
||||
def test_held_lock_rejects_start_without_changing_managed_files(self):
|
||||
profile_text = self.tcp_profile()
|
||||
self.write_profile(profile_text)
|
||||
state_dir = self.state_home / "xray-proxy"
|
||||
state_dir.mkdir(parents=True, mode=0o700)
|
||||
state_path = state_dir / "state.json"
|
||||
config_path = state_dir / "client.json"
|
||||
state_path.write_text("state sentinel\n", encoding="ascii")
|
||||
config_path.write_text("config sentinel\n", encoding="ascii")
|
||||
lock_path = state_dir / "lock"
|
||||
with lock_path.open("w", encoding="ascii") as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
result = self.run_cli("start")
|
||||
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
self.assertIn("блокиров", result.stderr)
|
||||
self.assertEqual("state sentinel\n", state_path.read_text(encoding="ascii"))
|
||||
self.assertEqual("config sentinel\n", config_path.read_text(encoding="ascii"))
|
||||
self.assertEqual([], self.log_entries())
|
||||
self.assert_no_secrets(
|
||||
result.stdout + result.stderr + lock_path.read_text(encoding="ascii"),
|
||||
profile_text,
|
||||
)
|
||||
|
||||
def test_stale_cleanup_waits_for_start_lock_and_preserves_files(self):
|
||||
state_dir = self.state_home / "xray-proxy"
|
||||
state_dir.mkdir(parents=True, mode=0o700)
|
||||
state = {
|
||||
"supervisor_pid": 999999998,
|
||||
"xray_pid": 999999999,
|
||||
"xray_path": str(self.xray.resolve()),
|
||||
"config_path": str(state_dir / "client.json"),
|
||||
"address": "127.0.0.1",
|
||||
"port": self.port,
|
||||
"supervisor_path": str(CLI.resolve()),
|
||||
"supervisor_starttime": 1,
|
||||
"xray_starttime": 1,
|
||||
"boot_id": Path("/proc/sys/kernel/random/boot_id")
|
||||
.read_text(encoding="ascii")
|
||||
.strip(),
|
||||
}
|
||||
self.write_state(state)
|
||||
config_path = state_dir / "client.json"
|
||||
config_path.write_text("stale sentinel\n", encoding="ascii")
|
||||
lock_path = state_dir / "lock"
|
||||
with lock_path.open("w", encoding="ascii") as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
status = self.run_cli("status")
|
||||
|
||||
self.assertNotEqual(0, status.returncode)
|
||||
self.assertIn("блокиров", status.stderr)
|
||||
self.assertTrue(self.managed_state_path().exists())
|
||||
self.assertEqual("stale sentinel\n", config_path.read_text(encoding="ascii"))
|
||||
|
||||
def test_reused_pid_starttime_is_not_signalled_and_state_is_preserved(self):
|
||||
profile_text = self.tcp_profile()
|
||||
self.write_profile(profile_text)
|
||||
process = self.start()
|
||||
original = self.read_state()
|
||||
changed = dict(original)
|
||||
changed["xray_starttime"] += 1
|
||||
self.write_state(changed)
|
||||
marker = self.root / "reused-pid-child-ran"
|
||||
|
||||
for command in (
|
||||
("status",),
|
||||
("env",),
|
||||
("run", "--", "touch", str(marker)),
|
||||
("stop",),
|
||||
):
|
||||
with self.subTest(command=command):
|
||||
result = self.run_cli(*command)
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
self.assertIn("вручную", result.stderr)
|
||||
self.assertTrue(self.managed_state_path().exists())
|
||||
self.assertFalse(marker.exists())
|
||||
self.assertTrue(process.poll() is None)
|
||||
|
||||
self.write_state(original)
|
||||
stdout, stderr = self.stop_and_collect(process)
|
||||
self.assert_no_secrets(stdout + stderr, profile_text)
|
||||
|
||||
def test_wrong_xray_executable_is_not_signalled_and_state_is_preserved(self):
|
||||
profile_text = self.tcp_profile()
|
||||
self.write_profile(profile_text)
|
||||
process = self.start()
|
||||
original = self.read_state()
|
||||
changed = dict(original)
|
||||
changed["xray_path"] = str(Path("/bin/false").resolve())
|
||||
self.write_state(changed)
|
||||
|
||||
stopped = self.run_cli("stop")
|
||||
self.assertNotEqual(0, stopped.returncode)
|
||||
self.assertIn("вручную", stopped.stderr)
|
||||
self.assertTrue(self.managed_state_path().exists())
|
||||
self.assertIsNone(process.poll())
|
||||
|
||||
self.write_state(original)
|
||||
stdout, stderr = self.stop_and_collect(process)
|
||||
self.assert_no_secrets(stdout + stderr, profile_text)
|
||||
|
||||
def test_boot_id_mismatch_cleans_stale_files_without_signalling_processes(self):
|
||||
profile_text = self.tcp_profile()
|
||||
self.write_profile(profile_text)
|
||||
process = self.start()
|
||||
state = self.read_state()
|
||||
state["boot_id"] = "00000000-0000-0000-0000-000000000000"
|
||||
self.write_state(state)
|
||||
os.kill(state["supervisor_pid"], signal.SIGKILL)
|
||||
process.wait(timeout=2)
|
||||
|
||||
status = self.run_cli("status")
|
||||
self.assertNotEqual(0, status.returncode)
|
||||
self.assertIn("устаревшее", status.stderr)
|
||||
self.assertFalse(self.managed_state_path().exists())
|
||||
self.assertFalse((self.state_home / "xray-proxy/client.json").exists())
|
||||
self.assertTrue(Path("/proc/{}".format(state["xray_pid"])).exists())
|
||||
|
||||
os.kill(state["xray_pid"], signal.SIGKILL)
|
||||
stdout, stderr = process.communicate(timeout=4)
|
||||
self.assert_no_secrets(stdout + stderr, profile_text)
|
||||
|
||||
def test_dead_pids_allow_stale_state_and_config_cleanup(self):
|
||||
state_dir = self.state_home / "xray-proxy"
|
||||
state_dir.mkdir(parents=True, mode=0o700)
|
||||
state = {
|
||||
"supervisor_pid": 999999998,
|
||||
"xray_pid": 999999999,
|
||||
"xray_path": str(self.xray.resolve()),
|
||||
"config_path": str(state_dir / "client.json"),
|
||||
"address": "127.0.0.1",
|
||||
"port": self.port,
|
||||
"supervisor_path": str(CLI.resolve()),
|
||||
"supervisor_starttime": 1,
|
||||
"xray_starttime": 1,
|
||||
"boot_id": Path("/proc/sys/kernel/random/boot_id")
|
||||
.read_text(encoding="ascii")
|
||||
.strip(),
|
||||
}
|
||||
self.write_state(state)
|
||||
(state_dir / "client.json").write_text("stale\n", encoding="ascii")
|
||||
|
||||
status = self.run_cli("status")
|
||||
self.assertNotEqual(0, status.returncode)
|
||||
self.assertIn("устаревшее", status.stderr)
|
||||
self.assertFalse(self.managed_state_path().exists())
|
||||
self.assertFalse((state_dir / "client.json").exists())
|
||||
|
||||
def test_sigkill_supervisor_stop_terminates_confirmed_orphan_and_cleans(self):
|
||||
profile_text = self.tcp_profile()
|
||||
self.write_profile(profile_text)
|
||||
process = self.start()
|
||||
state = self.read_state()
|
||||
run_count = len(
|
||||
[entry for entry in self.log_entries() if entry["kind"] == "run"]
|
||||
)
|
||||
alternate_profile = self.write_profile(self.xhttp_profile(), explicit=True)
|
||||
repeated = self.run_cli("start")
|
||||
switched = self.run_cli("start", "-f", str(alternate_profile))
|
||||
for result in (repeated, switched):
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
self.assertIn("блокиров", result.stderr)
|
||||
self.assertEqual(
|
||||
run_count,
|
||||
len([entry for entry in self.log_entries() if entry["kind"] == "run"]),
|
||||
)
|
||||
|
||||
lock_path = self.state_home / "xray-proxy/lock"
|
||||
with lock_path.open("r+", encoding="ascii") as lock:
|
||||
with self.assertRaises(OSError):
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
|
||||
os.kill(state["supervisor_pid"], signal.SIGKILL)
|
||||
process.wait(timeout=2)
|
||||
with lock_path.open("r+", encoding="ascii") as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
fcntl.flock(lock, fcntl.LOCK_UN)
|
||||
|
||||
status = self.run_cli("status")
|
||||
env = self.run_cli("env")
|
||||
marker = self.root / "orphan-child-ran"
|
||||
run = self.run_cli("run", "--", "touch", str(marker))
|
||||
for result in (status, env, run):
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
self.assertIn("сирот", result.stderr)
|
||||
self.assertFalse(marker.exists())
|
||||
|
||||
stopped = self.run_cli("stop")
|
||||
self.assertEqual(0, stopped.returncode, stopped.stderr)
|
||||
self.assertIn("сирот", stopped.stdout)
|
||||
self.assertFalse(self.managed_state_path().exists())
|
||||
self.assertFalse((self.state_home / "xray-proxy/client.json").exists())
|
||||
stdout, stderr = process.communicate(timeout=2)
|
||||
self.assert_no_secrets(stdout + stderr + stopped.stdout, profile_text)
|
||||
|
||||
self.port = self.free_port()
|
||||
self.write_machine_config("xray")
|
||||
replacement = self.start()
|
||||
replacement_stdout, replacement_stderr = self.stop_and_collect(replacement)
|
||||
self.assert_no_secrets(
|
||||
replacement_stdout + replacement_stderr, profile_text
|
||||
)
|
||||
|
||||
def test_unverifiable_live_child_preserves_state_and_config(self):
|
||||
profile_text = self.tcp_profile()
|
||||
self.write_profile(profile_text)
|
||||
self.environment["XRAY_PROXY_STOP_TIMEOUT"] = "0.2"
|
||||
process = self.start()
|
||||
state = self.read_state()
|
||||
self.xray.unlink()
|
||||
|
||||
os.kill(state["supervisor_pid"], signal.SIGTERM)
|
||||
process.wait(timeout=3)
|
||||
state_exists = self.managed_state_path().exists()
|
||||
config_path = self.state_home / "xray-proxy/client.json"
|
||||
config_exists = config_path.exists()
|
||||
child_alive = Path("/proc/{}".format(state["xray_pid"])).exists()
|
||||
if child_alive:
|
||||
os.kill(state["xray_pid"], signal.SIGKILL)
|
||||
deadline = time.monotonic() + 2
|
||||
while (
|
||||
Path("/proc/{}".format(state["xray_pid"])).exists()
|
||||
and time.monotonic() < deadline
|
||||
):
|
||||
time.sleep(0.02)
|
||||
stdout, stderr = process.communicate(timeout=2)
|
||||
|
||||
self.assertTrue(child_alive)
|
||||
self.assertTrue(state_exists)
|
||||
self.assertTrue(config_exists)
|
||||
self.assertNotEqual(0, process.returncode)
|
||||
self.assertIn("вручную", stderr)
|
||||
self.assert_no_secrets(stdout + stderr, profile_text)
|
||||
|
||||
def test_orphan_ignoring_sigterm_is_rechecked_killed_and_cleaned(self):
|
||||
profile_text = self.tcp_profile()
|
||||
self.write_profile(profile_text)
|
||||
self.environment["FAKE_IGNORE_TERM"] = "1"
|
||||
process = self.start()
|
||||
state = self.read_state()
|
||||
os.kill(state["supervisor_pid"], signal.SIGKILL)
|
||||
process.wait(timeout=2)
|
||||
|
||||
stopped = self.run_cli(
|
||||
"stop", environment={"XRAY_PROXY_STOP_TIMEOUT": "0.2"}
|
||||
)
|
||||
self.assertEqual(0, stopped.returncode, stopped.stderr)
|
||||
self.assertFalse(self.managed_state_path().exists())
|
||||
self.assertFalse((self.state_home / "xray-proxy/client.json").exists())
|
||||
signals = [
|
||||
entry["signum"]
|
||||
for entry in self.log_entries()
|
||||
if entry["kind"] == "signal"
|
||||
]
|
||||
self.assertEqual([signal.SIGTERM], signals)
|
||||
stdout, stderr = process.communicate(timeout=2)
|
||||
self.assert_no_secrets(stdout + stderr + stopped.stdout, profile_text)
|
||||
|
||||
def test_occupied_port_fails_without_touching_listener_or_leaving_generated_files(self):
|
||||
profile_text = self.tcp_profile()
|
||||
self.write_profile(profile_text)
|
||||
with socket.socket() as listener:
|
||||
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
listener.bind(("127.0.0.1", self.port))
|
||||
listener.listen(1)
|
||||
result = self.run_cli("start")
|
||||
with socket.create_connection(("127.0.0.1", self.port), timeout=1):
|
||||
pass
|
||||
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
self.assertIn("порт {}".format(self.port), result.stderr)
|
||||
self.assertIn("занят", result.stderr)
|
||||
self.assertEqual([], self.log_entries())
|
||||
self.assertFalse(self.managed_state_path().exists())
|
||||
self.assertFalse((self.state_home / "xray-proxy/client.json").exists())
|
||||
self.assert_no_secrets(result.stdout + result.stderr, profile_text)
|
||||
|
||||
def test_versions_and_profile_failures_stop_before_test_or_run_without_leaks(self):
|
||||
invalid_versions = (
|
||||
"Xray 25.12.8",
|
||||
@@ -556,6 +867,7 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
config.write_text('{"value":' + constant + '}\n', encoding="utf-8")
|
||||
result = subprocess.run(
|
||||
[str(self.xray), "run", "-test", "-c", str(config)],
|
||||
cwd=str(self.root),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=self.environment,
|
||||
@@ -576,6 +888,7 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
self.write_machine_config("xray")
|
||||
process = subprocess.Popen(
|
||||
[str(CLI), "start"],
|
||||
cwd=str(self.root),
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
@@ -744,8 +1057,8 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
proxy_bin.mkdir()
|
||||
(proxy_bin / "xray-proxy").symlink_to(CLI)
|
||||
invocations = (
|
||||
([str(CLI)], REPO_ROOT, self.environment),
|
||||
([os.path.relpath(str(CLI), str(REPO_ROOT))], REPO_ROOT, self.environment),
|
||||
([str(CLI)], self.root, self.environment),
|
||||
([os.path.relpath(str(CLI), str(self.root))], self.root, self.environment),
|
||||
(
|
||||
["xray-proxy"],
|
||||
self.root,
|
||||
|
||||
Reference in new issue
Block a user