#!/usr/bin/env python3
"""Read-only Linux host memory sampler. Emit JSONL; change no host settings."""
import argparse
import json
import math
import os
from pathlib import Path
import sys
import time

def integer(path):
    try:
        return int(Path(path).read_text().strip())
    except (OSError, ValueError):
        return None

def scanner_cpu_ticks():
    total = 0
    found = False
    for entry in Path("/proc").iterdir():
        if not entry.name.isdigit():
            continue
        try:
            if (entry / "comm").read_text().strip() != "ksmd":
                continue
            fields = (entry / "stat").read_text().rsplit(")", 1)[1].split()
            total += int(fields[11]) + int(fields[12])
            found = True
        except (OSError, ValueError, IndexError):
            continue
    return total if found else None

def sample(label):
    mem = {}
    for line in Path("/proc/meminfo").read_text().splitlines():
        key, value = line.split(":", 1)
        if key in ("MemTotal", "MemAvailable", "SwapTotal", "SwapFree"):
            mem[key] = int(value.split()[0]) * 1024
    ksm = {
        name: integer("/sys/kernel/mm/ksm/" + name)
        for name in ("run", "pages_to_scan", "sleep_millisecs", "pages_shared",
                     "pages_sharing", "ksm_zero_pages", "full_scans", "general_profit")
    }
    page_size = os.sysconf("SC_PAGE_SIZE")
    sharing = ksm["pages_sharing"]
    return {
        "label": label, "unix_seconds": time.time(), "kernel": os.uname().release,
        "page_size_bytes": page_size, "clock_ticks_per_second": os.sysconf("SC_CLK_TCK"),
        "memory_bytes": mem,
        "host_used_bytes": mem["MemTotal"] - mem["MemAvailable"],
        "ksm": ksm,
        "ksm_saved_bytes": None if sharing is None else
            (sharing + (ksm["ksm_zero_pages"] or 0)) * page_size,
        "ksmd_cpu_ticks": scanner_cpu_ticks(),
    }

def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--label", required=True)
    parser.add_argument("--seconds", type=float, default=300)
    parser.add_argument("--interval", type=float, default=2)
    args = parser.parse_args()
    if not all(math.isfinite(value) and value > 0
               for value in (args.seconds, args.interval)):
        parser.error("--seconds and --interval must be positive")
    if not Path("/proc/meminfo").is_file():
        parser.error("run this sampler on the Linux lab host")
    deadline = time.monotonic() + args.seconds
    try:
        while True:
            print(json.dumps(sample(args.label)), flush=True)
            remaining = deadline - time.monotonic()
            if remaining <= 0:
                break
            time.sleep(min(args.interval, remaining))
    except KeyboardInterrupt:
        return 0
    return 0

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