"""Small local process helpers. No release, source or version verification.""" from __future__ import annotations import contextlib import json import os from pathlib import Path import subprocess import time import urllib.request RUNTIME_RPC_SECONDS = 14 def read_json(path): return json.loads(Path(path).read_text()) def write_json(path, value): write(path, json.dumps(value, indent=2) + "\n") def write(path, value, mode=0o600): path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) temporary = path.with_suffix(path.suffix + ".tmp") temporary.write_text(value) temporary.chmod(mode) temporary.replace(path) def read_env(path): return dict(line.split("=", 1) for line in Path(path).read_text().splitlines() if line.strip() and not line.lstrip().startswith("#")) def write_env(path, values): if any("\n" in str(value) or "\r" in str(value) for value in values.values()): raise ValueError("environment values cannot contain newlines") write(path, "".join(f"{key}={value}\n" for key, value in values.items())) def run(command, *, capture=False, env=None, timeout=120): return subprocess.run([str(part) for part in command], check=True, text=True, capture_output=capture, env=env, timeout=timeout).stdout def compose(root, owner, *arguments): return run(("docker", "compose", "-f", Path(root) / owner / "compose.json", *arguments), timeout=180) def worker_image_identity(reference, info): """Read local ownership inputs without imposing release/provenance policy.""" digest = reference.split("@")[-1] if "@sha256:" in reference else info["Id"] labels = info["Config"].get("Labels") or {} return digest, labels.get("org.opencontainers.image.revision") or "local" def image_file(image, source, destination): container = run(("docker", "create", image, "extract-only"), capture=True).strip() try: Path(destination).parent.mkdir(parents=True, exist_ok=True) run(("docker", "cp", f"{container}:{source}", destination)) finally: run(("docker", "rm", container), capture=True) def bind(source, destination, readonly=True): return {"type": "bind", "source": str(Path(source).resolve()), "target": destination, "read_only": readonly} def wait_http(url, timeout=90): deadline = time.monotonic() + timeout while time.monotonic() < deadline: try: with urllib.request.urlopen(url, timeout=2) as response: if response.status == 200: return except (OSError, ValueError): pass time.sleep(1) raise RuntimeError(f"service did not become ready: {url}") @contextlib.contextmanager def phase(root, name): """Record operation duration and outcome; propagate the original failure.""" started = time.monotonic() outcome = "failed" print(f"{name}: starting", flush=True) try: yield outcome = "ok" finally: elapsed = round(time.monotonic() - started, 2) with (Path(root) / "timings.jsonl").open("a") as output: output.write(json.dumps({"phase": name, "seconds": elapsed, "outcome": outcome}) + "\n") print(f"{name}: {elapsed}s", flush=True) def cli(main): try: main() except (RuntimeError, ValueError, OSError, subprocess.SubprocessError) as error: raise SystemExit(str(error)) from None