ctf-ad-mirror-tool-rust/scripts/ui_fixture.py

96 lines
3.2 KiB
Python

#!/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())