Some checks failed
Windows project source guards / test (push) Failing after 46s
Сеанс zapret-kvn-33 поймал на Android редкий SIGSEGV (addr 0) на первом
запросе движка; первым шёл DoH POST. Воспроизведено на Linux: стресс 200
POST с частым GC падает в каждом из 10 прогонов в bodyUploadProvider.Read
→ UploadDataSink.OnReadSucceeded (cronet-go, purego); тот же стресс GET —
2000/2000 без сбоя.
- browserfetch.Fetch отклоняет запросы с телом понятной ошибкой.
- Один Executor на время жизни движка: RoundTripper без своего вешал
финализатор, уничтожавший исполнитель при сборке мусора.
- DoH Windows через браузерный стек — GET ?dns= (RFC 8484), как у Chrome
для шаблона dns.google{?dns}. Затронут выпущенный 0.8.37: там DoH шёл
POST, при падении помощник перезапускался, запрос — к следующему резолверу.
Проверено: win10 — 200/200 DoH за 2,6 с в 8 потоков, один процесс-помощник
без перезапусков, запасной путь Python не понадобился; цепочка Android
0001–0006 + этот 0007 + 0008 ложится на чистую базу.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
3.1 KiB
Python
79 lines
3.1 KiB
Python
"""Клиент браузерного стека (xray_fluent/network/browser_fetch.py) на подделке процесса."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import threading
|
|
import unittest
|
|
|
|
from xray_fluent.network.browser_fetch import (
|
|
BrowserFetchError,
|
|
BrowserFetchService,
|
|
BrowserFetchUnavailable,
|
|
)
|
|
|
|
FAKE = Path(__file__).resolve().parent / "fixtures" / "fake_browser_fetch.py"
|
|
|
|
|
|
def _service() -> BrowserFetchService:
|
|
return BrowserFetchService(
|
|
executable=lambda: FAKE, launcher=lambda exe: [sys.executable, "-I", str(exe)], enabled=True,
|
|
)
|
|
|
|
|
|
class BrowserFetchServiceTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.service = _service()
|
|
self.addCleanup(self.service.close)
|
|
|
|
def test_request_round_trip_keeps_method_headers_and_body(self) -> None:
|
|
response = self.service.fetch(
|
|
"https://8.8.8.8/dns-query?dns=AAE", method="GET",
|
|
headers=(("accept", "application/dns-message"),), timeout=5,
|
|
)
|
|
self.assertEqual(response.status, 200)
|
|
self.assertEqual(response.header("content-type"), "application/json")
|
|
echo = json.loads(response.body)
|
|
self.assertEqual(echo["method"], "GET")
|
|
self.assertEqual(echo["headers"], [["accept", "application/dns-message"]])
|
|
self.assertEqual(echo["body"], "") # тела браузерный стек не принимает
|
|
|
|
def test_parallel_requests_share_one_process(self) -> None:
|
|
results: list[int] = []
|
|
|
|
def run() -> None:
|
|
results.append(self.service.fetch("https://example.com/", timeout=5).status)
|
|
|
|
threads = [threading.Thread(target=run) for _ in range(8)]
|
|
for thread in threads:
|
|
thread.start()
|
|
for thread in threads:
|
|
thread.join(10)
|
|
self.assertEqual(results, [200] * 8)
|
|
|
|
def test_network_error_is_reported(self) -> None:
|
|
with self.assertRaises(BrowserFetchError) as caught:
|
|
self.service.fetch("https://fail.example/", timeout=5)
|
|
self.assertNotIsInstance(caught.exception, BrowserFetchUnavailable)
|
|
|
|
def test_crashed_process_fails_pending_and_restarts(self) -> None:
|
|
with self.assertRaises(BrowserFetchError):
|
|
self.service.fetch("https://crash.example/", timeout=5)
|
|
self.assertEqual(self.service.fetch("https://example.com/", timeout=5).status, 200)
|
|
|
|
def test_core_without_chromium_library_is_unavailable(self) -> None:
|
|
with self.assertRaises(BrowserFetchUnavailable):
|
|
self.service.fetch("https://nolib.example/", timeout=5)
|
|
|
|
def test_missing_core_is_unavailable(self) -> None:
|
|
service = BrowserFetchService(executable=lambda: Path("/nonexistent/sing-box.exe"), enabled=True)
|
|
self.assertFalse(service.available())
|
|
self.assertFalse(BrowserFetchService(executable=lambda: FAKE).available()) # не включена
|
|
with self.assertRaises(BrowserFetchUnavailable):
|
|
service.fetch("https://example.com/", timeout=1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|