#!/usr/bin/env python3 """Run the browser against an isolated real backend/core and a local subscription fixture.""" import contextlib import http.server import json import os import shutil import socket import sqlite3 import subprocess import tempfile import threading import time import urllib.request from pathlib import Path root = Path(__file__).resolve().parent.parent for port in (18080, 9090, 7890, 1053): with socket.socket() as probe: if probe.connect_ex(('127.0.0.1', port)) == 0: raise SystemExit(f'Port {port} is occupied. E2E aborted to avoid changing another service.') class Fixture(http.server.BaseHTTPRequestHandler): protocol_version = 'HTTP/1.1' def do_CONNECT(self): self.send_response(200, 'Connection Established') self.end_headers() self.close_connection = False self.handle_one_request() def do_HEAD(self): # Mihomo's unified-delay check repeats HEAD on the established tunnel. # Supply a framed keep-alive response, rather than the default HTTP 501. time.sleep(0.03) self.send_response(200) self.send_header('Content-Length', '0') self.end_headers() def do_GET(self): if self.path == '/subscription': data = 'proxies:\n' for name in ['香港 · 测试节点', '日本 · 测试节点', '新加坡 · 测试节点']: data += f' - name: {name}\n type: http\n server: 127.0.0.1\n port: {self.server.server_port}\n' body = data.encode() else: # Keep successful local health checks above Mihomo's integer-ms zero. time.sleep(0.03) body = b'local-e2e-proxy' self.send_response(200) self.send_header('Content-Length', str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, *args): pass fixture = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Fixture) threading.Thread(target=fixture.serve_forever, daemon=True).start() def wait_url(url, timeout=20): until = time.monotonic() + timeout while time.monotonic() < until: try: with urllib.request.urlopen(url, timeout=1) as response: return response.status except Exception: time.sleep(0.1) raise RuntimeError(f'Service failed to start: {url}') root.joinpath('.cache').mkdir(exist_ok=True) with tempfile.TemporaryDirectory(prefix='e2e-', dir=root / '.cache') as temporary: work = Path(temporary) core = work / 'mihomo' shutil.copytree(root / 'data/mihomo/rules', core / 'rules') command = [str(root / 'bin/clashweb'), '--data', str(work / 'app'), '--core-dir', str(core), '--mihomo', str(root / 'bin/mihomo')] initialized = subprocess.run(command + ['--init'], text=True, capture_output=True) if initialized.returncode: raise SystemExit('Initialization failed: ' + initialized.stderr) with contextlib.closing(sqlite3.connect(work / 'app/clashweb.sqlite')) as db: token = db.execute("SELECT value FROM kv WHERE key='setup_token'").fetchone()[0] processes = [] try: with open(work / 'core.log', 'w') as log: processes.append(subprocess.Popen([str(root / 'bin/mihomo'), '-d', str(core), '-f', str(core / 'config.yaml')], stdout=log, stderr=log)) with open(work / 'app.log', 'w') as log: processes.append(subprocess.Popen(command + ['--listen', '127.0.0.1:18080'], stdout=log, stderr=log)) wait_url('http://127.0.0.1:18080/api/v1/auth/status') environment = dict(os.environ, CW_E2E_URL='http://127.0.0.1:18080', CW_E2E_SETUP_TOKEN=token, CW_E2E_SUBSCRIPTION_URL=f'http://127.0.0.1:{fixture.server_port}/subscription') result = subprocess.run(['npm', 'run', 'test:e2e'], cwd=root / 'web', env=environment) if result.returncode: # This harness contains only credential-free local proxy fixtures. print('Local fixture core log:', (work / 'core.log').read_text()[-8000:]) raise SystemExit(result.returncode) finally: for process in reversed(processes): process.terminate() try: process.wait(timeout=10) except subprocess.TimeoutExpired: process.kill() process.wait() fixture.shutdown()