#!/usr/bin/env python3 """Disposable loopback-only UI fixture. Never reads legacy configuration. Usage: python3 scripts/ui_fixture.py target/debug/mirror-tool Public test-only login: local-ui-fixture-only. Default state is disposable. Use --data-dir data/demo for persistent state; --port-offset 100 isolates a test. """ import argparse import asyncio from contextlib import nullcontext import os from pathlib import Path import signal import tempfile async def echo(reader, writer): try: while data := await reader.read(65536): writer.write(data) await writer.drain() except (OSError, asyncio.CancelledError): pass finally: writer.close() try: await writer.wait_closed() except OSError: pass async def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("executable", type=Path) parser.add_argument("--data-dir", type=Path, help="Preserve config/state here; never overwrite an existing config") parser.add_argument("--port-offset", type=int, default=0, help="Add to all fixture ports (0..30000)") args = parser.parse_args() if not 0 <= args.port_offset <= 30000: parser.error("port offset must be in 0..30000") executable = str(args.executable.resolve()) ui_port, proxy_port, echo_port = (port + args.port_offset for port in (16700, 18081, 18082)) if args.data_dir: args.data_dir.mkdir(parents=True, exist_ok=True) context = nullcontext(args.data_dir.resolve()) if args.data_dir else tempfile.TemporaryDirectory(prefix="mirror-ui-") with context as folder: root = Path(folder) config = root / "mirror.toml" if not config.exists(): config.write_text(f'''[web] listen = "127.0.0.1:{ui_port}" state_file = "state.json" [network] drain_timeout_ms = 500 [[teams]] id = "a" name = "Loopback A" address = "127.0.0.1" [[teams]] id = "b" name = "Loopback B" address = "::1" [[services]] id = "echo" name = "Echo" listen = "127.0.0.1:{proxy_port}" upstream_port = {echo_port} primary = "a" replicas = ["b"] enabled = false auto_switch = false replica_policy = "isolate" ''') servers = [await asyncio.start_server(echo, address, echo_port) for address in ("127.0.0.1", "::1")] env = os.environ.copy() env["MIRROR_PASSWORD"] = "local-ui-fixture-only" process = await asyncio.create_subprocess_exec(executable, "--config", str(config), env=env) stop = asyncio.Event() for sig in (signal.SIGINT, signal.SIGTERM): asyncio.get_running_loop().add_signal_handler(sig, stop.set) print(f"UI fixture: http://127.0.0.1:{ui_port}; config: {config} (loopback only)", flush=True) waiter = asyncio.create_task(process.wait()) stopper = asyncio.create_task(stop.wait()) await asyncio.wait([waiter, stopper], return_when=asyncio.FIRST_COMPLETED) if process.returncode is None: process.terminate() await process.wait() stopper.cancel() await asyncio.gather(stopper, return_exceptions=True) for server in servers: server.close() await server.wait_closed() if __name__ == "__main__": asyncio.run(main())