#!/usr/bin/env python3
"""Prove that a container shares the measuring host's CLOCK_MONOTONIC.

    clock_gate.py <container> [--max-offset-ms 5] [--samples 20] [--json]

The whole black-box glass-to-glass metric is one subtraction across a process
boundary: a probe inside the streamed desktop stamps CLOCK_MONOTONIC into the
picture, an observer on this machine reads the picture and subtracts its own
CLOCK_MONOTONIC. That is only arithmetic if both sides read the same clock. If
they do not, every number is silently wrong by the offset and nothing in the
picture says so -- which is precisely the class of failure this repo's rules
exist to catch, so it is gated per run and the evidence is recorded.

Two checks, and the order matters:

1. `/proc/1/timens_offsets` inside the container, which must read
   `monotonic 0 0`. This is authoritative and exact -- it is the kernel's own
   statement of what it adds to CLOCK_MONOTONIC for that namespace.

   Comparing `/proc/1/ns/time` inodes, the obvious first cut, is WRONG here and
   was measured to be wrong on this box: a healthy kwin-host container sits in
   its own time namespace (different inode from the host) whose offsets are all
   zero, so the clocks are identical while an inode comparison reports a
   mismatch. An inode test would refuse to run a perfectly valid measurement.

2. A sandwiched clock read as corroboration, bounded loosely. `docker exec`
   costs ~50 ms of wall time and is asymmetric -- the interpreter inside starts
   nearer the end of the call than the middle -- so this method cannot resolve
   better than about ±15 ms and must not be given a tight threshold. It exists
   to catch gross skew (a VM with an independent clock, a namespace with a real
   offset), which shows up as seconds or hours, not milliseconds. Check 1 is
   what actually decides.
"""
import argparse
import json
import re
import subprocess
import sys
import time

MONOTONIC_RE = re.compile(r"^\s*monotonic\s+(-?\d+)\s+(-?\d+)\s*$", re.M)
BOOTTIME_RE = re.compile(r"^\s*boottime\s+(-?\d+)\s+(-?\d+)\s*$", re.M)


def sh(*cmd, timeout=20):
    r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
    if r.returncode != 0:
        raise SystemExit(f"{' '.join(cmd)} failed: {r.stderr.strip()}")
    return r.stdout


def timens_offsets(container):
    """The kernel's own statement of this namespace's clock offsets."""
    out = sh("docker", "exec", container, "cat", "/proc/1/timens_offsets")
    m, b = MONOTONIC_RE.search(out), BOOTTIME_RE.search(out)
    if not m or not b:
        raise SystemExit(f"could not parse /proc/1/timens_offsets:\n{out}")
    return {
        "monotonic_sec": int(m.group(1)), "monotonic_nsec": int(m.group(2)),
        "boottime_sec": int(b.group(1)), "boottime_nsec": int(b.group(2)),
        "raw": out.strip(),
    }


def sandwich(container, samples):
    """Median offset of the container's clock relative to this host's.

    Bounded by the asymmetry of `docker exec`, not by clock precision. Reported
    with its spread so a reader can see how little it resolves.
    """
    offsets, exec_ms = [], []
    prog = "import time; print(time.clock_gettime(time.CLOCK_MONOTONIC))"
    for _ in range(samples):
        a = time.clock_gettime(time.CLOCK_MONOTONIC)
        c = float(sh("docker", "exec", container, "python3", "-c", prog).strip())
        b = time.clock_gettime(time.CLOCK_MONOTONIC)
        offsets.append((c - (a + b) / 2) * 1000.0)
        exec_ms.append((b - a) * 1000.0)
    offsets.sort()
    return {
        "median_ms": offsets[len(offsets) // 2],
        "min_ms": offsets[0],
        "max_ms": offsets[-1],
        "exec_median_ms": sorted(exec_ms)[len(exec_ms) // 2],
        "samples": samples,
    }


def check(container, max_offset_ms=5.0, samples=20):
    ns = timens_offsets(container)
    sw = sandwich(container, samples)
    ns_ok = (ns["monotonic_sec"] == 0 and ns["monotonic_nsec"] == 0)
    # Loose: this method's own resolution is ~15 ms (see the module docstring).
    sw_ok = abs(sw["median_ms"]) < max(100.0, sw["exec_median_ms"] * 2)
    return {
        "container": container,
        "timens_offsets": ns,
        "sandwich": sw,
        "shared_clock": bool(ns_ok and sw_ok),
        "decided_by": "timens_offsets" if ns_ok else "timens_offsets (nonzero)",
        "host_ns_time": subprocess.run(
            ["readlink", "/proc/self/ns/time"], capture_output=True, text=True
        ).stdout.strip(),
        "container_ns_time": sh("docker", "exec", container, "readlink",
                                "/proc/1/ns/time").strip(),
    }


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("container")
    ap.add_argument("--max-offset-ms", type=float, default=5.0)
    ap.add_argument("--samples", type=int, default=20)
    ap.add_argument("--json", action="store_true")
    a = ap.parse_args()

    res = check(a.container, a.max_offset_ms, a.samples)
    if a.json:
        print(json.dumps(res, indent=2))
    else:
        ns, sw = res["timens_offsets"], res["sandwich"]
        print(f"container            {res['container']}")
        print(f"timens monotonic     {ns['monotonic_sec']}s {ns['monotonic_nsec']}ns")
        print(f"timens boottime      {ns['boottime_sec']}s {ns['boottime_nsec']}ns")
        print(f"time namespace       host {res['host_ns_time']} / "
              f"container {res['container_ns_time']} "
              f"({'same' if res['host_ns_time'] == res['container_ns_time'] else 'different -- not a problem by itself'})")
        print(f"sandwiched offset    {sw['median_ms']:+.2f} ms "
              f"(min {sw['min_ms']:+.2f}, max {sw['max_ms']:+.2f}, "
              f"docker exec {sw['exec_median_ms']:.0f} ms -- this method cannot "
              f"resolve better than that)")
        print(f"shared clock         {'YES' if res['shared_clock'] else 'NO'}")

    if not res["shared_clock"]:
        print("\nThe container does not share this host's CLOCK_MONOTONIC. The "
              "draw-to-visible metric is invalid here; use the input-to-photon "
              "instrument instead, which reads one clock on one machine.",
              file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())
