"""Run a synthetic envelope demo end-to-end. Spawns ``tools/load_mimic.py`` as a subprocess, drives the orchestrator through an XMRig-shaped phase schedule, and writes a complete episode directory. Real telemetry from the host /proc collector; load is simulated so we can validate the pipeline before a real VM is involved. Usage: uv run python tools/run_envelope_demo.py [--data-root data] After it finishes, plot with: uv run python tools/plot_envelope.py """ from __future__ import annotations import argparse import logging import subprocess import sys from pathlib import Path # Allow running as a script: add repo root to sys.path before project imports. sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from orchestrator.episode import EpisodeConfig, EpisodeRunner # noqa: E402 # (phase, duration_seconds). Total ~85s. DEFAULT_SCHEDULE = [ ("clean", 10.0), ("armed", 2.0), ("infecting", 3.0), ("infected_running", 25.0), ("dormant", 15.0), ("infected_running", 20.0), ("dormant", 5.0), ("clean", 5.0), ] def main() -> int: parser = argparse.ArgumentParser(prog="run_envelope_demo") parser.add_argument("--data-root", default="data") parser.add_argument("--interval-ms", type=int, default=100) args = parser.parse_args() logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s", ) here = Path(__file__).parent load_mimic = here / "load_mimic.py" proc = subprocess.Popen( [sys.executable, str(load_mimic)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1, text=True, ) # First line: "load_mimic pid= scratch=" first = proc.stdout.readline().strip() if not first.startswith("load_mimic pid="): proc.kill() print(f"unexpected load_mimic startup: {first!r}", file=sys.stderr) return 2 pid = int(first.split()[1].split("=", 1)[1]) print(first) def on_phase(p: str) -> None: proc.stdin.write(p + "\n") proc.stdin.flush() # Drain the echo line so the pipe doesn't fill up. proc.stdout.readline() print(f" >>> phase = {p}") cfg = EpisodeConfig( target_pid=pid, duration_s=sum(d for _, d in DEFAULT_SCHEDULE), interval_ms=args.interval_ms, data_root=Path(args.data_root), phase_schedule=DEFAULT_SCHEDULE, image_name="load_mimic.py", snapshot_name="(synthetic)", ) try: result = EpisodeRunner(cfg, on_phase=on_phase).run() finally: try: proc.stdin.write("quit\n") proc.stdin.flush() except Exception: pass try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() print() print(f"episode_id = {result.episode_id}") print(f"path = {result.episode_dir}") print(f"rows_proc = {result.rows_proc}") print(f"phases = {result.phases_observed}") print() print("To plot:") print(f" uv run python tools/plot_envelope.py {result.episode_dir}") return 0 if __name__ == "__main__": sys.exit(main())