"""Lane producer runner for the #304 Rust allocation.

Samples volume capacity and a scoped builder census around one producer, enforces a
hard deadline, and retains the real exit code, raw stderr, stdout and the observed
descendant identities. No retry, no kill-by-image, no PID-only kill.
"""
import ctypes, json, os, subprocess, sys, tempfile, time
import psutil

# Producer output is nextest's, which draws box characters. A cp1252 stdout
# raises UnicodeEncodeError on them and kills the driver AFTER the producer has
# already written its evidence — losing every later producer in the sequence.
for _stream in (sys.stdout, sys.stderr):
    try:
        _stream.reconfigure(encoding="utf-8", errors="replace")
    except (AttributeError, ValueError):
        pass

EVID = os.path.dirname(os.path.abspath(__file__))
VOLUME = "C:\\"
BUILDER_NAMES = {
    "cargo.exe", "rustc.exe", "link.exe", "cargo-nextest.exe",
    "rustdoc.exe", "lld-link.exe", "runner.worker.exe",
}
# Allocation gates (304-RUST-ALLOCATION.txt)
STOP_FREE = 34_359_738_368          # stop if free <= 32 GiB
MAX_GROWTH = 68_719_476_736         # stop if start_free - free >= 64 GiB


def free_bytes(path=VOLUME):
    f = ctypes.c_ulonglong(0)
    ok = ctypes.windll.kernel32.GetDiskFreeSpaceExW(
        ctypes.c_wchar_p(path), ctypes.byref(f), None, None)
    if not ok:
        raise OSError("GetDiskFreeSpaceExW failed on %s" % path)
    return f.value


# THE POOL IS THE LANE'S, NAMED BY THE LANE, NEVER INHERITED. This was a literal
# path to the hertz-304 worktree's target. A copy of this rig driving a DIFFERENT
# lane would then have censused, and reported growth for, a pool it was not
# running in -- the same class as the admission record named after a sha it did
# not describe, and as the two cells this lane guards. There is no default: an
# unset var REFUSES, because a rig that silently measures the wrong subject is
# worse than one that will not start.
POOL = os.environ.get("HERTZ_LANE_POOL")
if not POOL:
    raise SystemExit(
        "HERTZ_LANE_POOL is unset. Set it to THIS lane's target dir, e.g.\n"
        "  HERTZ_LANE_POOL=<worktree>/target\n"
        "The rig refuses to guess: censusing another lane's pool reports growth "
        "and executions that are not this lane's.")
if not os.path.isdir(POOL):
    raise SystemExit("HERTZ_LANE_POOL is not a directory: %s" % POOL)


def pool_bytes(root=POOL):
    """Logical bytes and file count of THIS lane's pool subtree.

    A volume free-space meter is unattributable whenever any other lane moves
    bytes (doyle, 2026-09-11); this number is the lane's own regardless of what
    the volume does. Unreadable entries are counted, never silently skipped.
    """
    # A missing root would walk to a clean zero and read as "no growth".
    if not os.path.isdir(root):
        raise OSError("pool subtree is absent, not empty: %s" % root)
    total, files, unreadable = 0, 0, 0
    for base, _dirs, names in os.walk(root, onerror=lambda _e: None):
        for name in names:
            try:
                total += os.lstat(os.path.join(base, name)).st_size
                files += 1
            except OSError:
                unreadable += 1
    return {"bytes": total, "files": files, "unreadable": unreadable}


def _attribute(proc):
    """Who owns this builder: CI, an editor's analyzer, this session, or unknown.

    A bare count is not readable (todlando, 2026-09-11): an editor-driven
    `cargo check --workspace --all-targets` counts exactly like a CI leg, so a
    nonzero census reads as "CI or a peer" when it may be a language server, and
    a zero leases nothing. Attribute, and carry the manifest when it is legible.
    """
    chain, manifest = [], None
    try:
        manifest = next((a for a in proc.cmdline() if a.endswith("Cargo.toml")), None)
    except (psutil.Error, OSError):
        pass
    q = proc
    try:
        for _ in range(8):
            q = q.parent()
            if q is None:
                break
            chain.append(q.name().lower())
    except (psutil.Error, OSError):
        chain.append("<unreadable>")
    if any("runner." in c for c in chain):
        owner = "ci"
    elif any("rust-analyzer" in c for c in chain):
        owner = "analyzer"
    elif "<unreadable>" in chain:
        owner = "unknown"
    else:
        owner = "agent"
    return owner, chain, manifest


def builder_census():
    seen, unreadable = [], []
    for p in psutil.process_iter(["pid", "name", "create_time"]):
        try:
            name = (p.info["name"] or "").lower()
        except Exception as exc:
            unreadable.append(str(exc))
            continue
        if name in BUILDER_NAMES:
            owner, chain, manifest = _attribute(p)
            seen.append({"pid": p.info["pid"], "name": name,
                         "birth": p.info["create_time"], "owner": owner,
                         "manifest": manifest, "ancestry": chain})
    return {"builders": seen, "unreadable": unreadable,
            "by_owner": {o: sum(1 for b in seen if b["owner"] == o)
                         for o in ("ci", "analyzer", "agent", "unknown")}}


def priv_for(label):
    """The private TMP base for one producer. ONE definition, two callers - the
    runner that exports it and the window driver that records which base an arm
    actually got. Window 6's arm B was a non-measurement because the driver
    drove the switch through env_extra (child-only) while this branch reads the
    PARENT env; a second copy of this rule is how that stays possible.
    """
    if os.environ.get("HERTZ_RIG_TMP_IN_REPO") == "1":
        return os.path.join(EVID, "temp", label)
    base = (os.environ.get("TEMP") or os.environ.get("TMP")
            or tempfile.gettempdir())
    # Named after THIS rig dir, not a literal lane tag: two lanes running from
    # two copies of this rig must not share one TMP base, or a stale fixture
    # from the other lane is reachable under a name this lane believes is its own.
    return os.path.join(base, os.path.basename(EVID) + "-rig", label)


def run(label, argv, cwd, deadline_s, env_extra=None, stdout_path=None):
    env = dict(os.environ)
    for k in ("SPT_ENDPOINT", "SPT_AGENT", "SPT_SESSION_ID", "SPT_HOME",
              "CARGO_TARGET_DIR", "RUSTFLAGS"):
        env.pop(k, None)
    # THE RIG'S OWN TMP REDIRECT WAS A DEFECT, measured 2026-09-11 after
    # w5-lib-full: this base used to be EVID/temp/<label>, which lives INSIDE
    # the spt-core checkout. Every tempfile::tempdir() fixture therefore sat
    # under a git work tree, git discovery walked up and found the repo, and
    # project_id_for_dir (remote-URL FIRST) handed back THIS repo's identity
    # to cells whose fixture requires a dir outside any repo. Two cells read
    # the real slug instead of their folder names and failed; the same cells
    # passed in a run with the default TMP 8 minutes earlier, and pass in CI.
    # PROVEN, with a control: git -C <old priv> rev-parse --show-toplevel ==
    # the spt-core checkout and its remote is the bs-core URL, while the same
    # probe from the default TMP answers "not a git repository".
    # The base is now OUTSIDE the tree; per-label isolation and reapability
    # are unchanged. HERTZ_RIG_TMP_IN_REPO=1 restores the old in-repo
    # placement deliberately, as the negative control for that measurement.
    priv = priv_for(label)
    os.makedirs(priv, exist_ok=True)
    for k in ("TMP", "TEMP", "TMPDIR", "RUNNER_TEMP"):
        env[k] = priv
    env["SPT_HOME"] = os.path.join(priv, "spt-home")
    env["SPT_INSTALL_NO_FIREWALL"] = "1"
    env["PYTHONDONTWRITEBYTECODE"] = "1"
    if env_extra:
        env.update(env_extra)

    start_free = free_bytes()
    start_pool = pool_bytes()
    pre = builder_census()
    json.dump({"argv": argv, "cwd": cwd, "deadline_s": deadline_s},
              open(os.path.join(EVID, label + ".command.json"), "w"), indent=2)

    so = open(stdout_path or os.path.join(EVID, label + ".stdout"), "wb")
    se = open(os.path.join(EVID, label + ".raw"), "wb")
    t0 = time.monotonic()
    proc = subprocess.Popen(argv, cwd=cwd, env=env, stdout=so, stderr=se,
                            stdin=subprocess.DEVNULL)
    root = psutil.Process(proc.pid)
    identities, stop_reason = {}, None
    min_free = start_free
    while True:
        rc = proc.poll()
        if rc is not None:
            break
        try:
            for c in root.children(recursive=True):
                identities.setdefault(c.pid, c.create_time())
        except psutil.Error:
            pass
        f = free_bytes()
        min_free = min(min_free, f)
        if f <= STOP_FREE:
            stop_reason = "free_floor"
        elif start_free - f >= MAX_GROWTH:
            stop_reason = "growth_ceiling"
        elif time.monotonic() - t0 > deadline_s:
            stop_reason = "deadline"
        if stop_reason:
            for pid, birth in list(identities.items()):
                try:
                    p = psutil.Process(pid)
                    if p.create_time() == birth:
                        p.kill()
                except psutil.Error:
                    pass
            try:
                if root.is_running():
                    root.kill()
            except psutil.Error:
                pass
            break
        time.sleep(0.1)
    rc = proc.wait()
    elapsed = time.monotonic() - t0
    so.close(); se.close()
    end_free = free_bytes()
    end_pool = pool_bytes()
    obs = {
        "label": label,
        "producer_exit": rc,
        "stop_reason": stop_reason,
        "elapsed_seconds": elapsed,
        "start_free": start_free, "min_free": min_free, "end_free": end_free,
        # Volume delta: a LOWER BOUND on this lane's use whenever another lane
        # moves bytes on C: concurrently. The pool delta below is the lane's own.
        "growth_bytes": start_free - min_free,
        "start_pool": start_pool, "end_pool": end_pool,
        "pool_growth_bytes": end_pool["bytes"] - start_pool["bytes"],
        "pre_census": pre,
        "post_census": builder_census(),
        "observed_identities": [{"pid": k, "birth": v}
                                for k, v in sorted(identities.items())],
        "survivors": [],
        "utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
    }
    for pid, birth in identities.items():
        try:
            p = psutil.Process(pid)
            if p.create_time() == birth and p.is_running():
                obs["survivors"].append({"pid": pid, "birth": birth})
        except psutil.Error:
            pass
    open(os.path.join(EVID, label + ".exit"), "w").write(str(rc))
    json.dump(obs, open(os.path.join(EVID, label + ".observation.json"), "w"),
              indent=2)
    return obs


if __name__ == "__main__":
    print(json.dumps({"utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
                      "volume": VOLUME, "free_bytes": free_bytes(),
                      "census": builder_census()}, indent=2))
