#!/usr/bin/env python3 -u
# rig236.py -- shared rig for DF-2361/DF-2362 verification against the REAL
# in-kernel gdb stub (sys/cpu/x86_64/misc/x86_64-gdbstub.c, sha256-gated
# bda2d9a4..., compiled into the stock X86_64_GENERIC kernel).
#
# Transport: guest com0 (the sio0 console UART) wired by QEMU to a unix
# socket. We speak the exact bytes a physical serial debug console would.
#
# gdb_tab attach: the stock kernel boots with gdb_tab == NULL (the boot-time
# hint hint.sio.0.flags=0x90 did not attach gdb_tab on this ACPI guest; see
# VERDICT.md). The rig attaches the documented state at runtime via DDB:
#   gdb_tab = &sio_consdev, siocniobase=0x3f8 already, siogdbunit=-1 already
# => stub I/O on com0. This mirrors exactly what sio.c:3113-3119 is supposed
# to do for flags 0x80/0x90. No code change; stock binary untouched.
import os, sys, time, socket, subprocess, json

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from rsp import SerialPeer, parse_t05, cks

SOCK = os.environ.get("DF_SOCK", "/home/maxx/dfbsd/dfbsd/dfbsd-qemu/df236x/ser.sock")
RUNNER = os.environ.get("DF_VMSH", "/home/maxx/dfbsd/dfbsd/dfbsd-qemu/run236.sh")

# nm of the RUNNING kernel (stock X86_64_GENERIC build, Jul 2 2026).
# Override via DF_ADDRS env (json) for other builds (fix validation).
ADDRS = json.loads(os.environ.get("DF_ADDRS", json.dumps({
    "gdb_tab":      0xffffffff8131dd20,
    "sio_consdev":  0xffffffff81142e60,
    "siogdbunit":   0xffffffff811435d0,
    "siogdbiobase": 0xffffffff81b256f4,
    "siocniobase":  0xffffffff81b256fc,
    "siocnunit":    0xffffffff81b256f8,
    "remcomOutBuffer": 0xffffffff817a6f20,
    "remcomInBuffer":  0xffffffff817a70c0,
    "ddb_regs":     0xffffffff817a7ac0,
    "gdb_handle_exception": 0xffffffff80b94280,
    "kdb_trap":     0xffffffff80bcc890,
    "kdb_call_ret": 0xffffffff80bcc9aa,   # retaddr of gdb_handle_exception
})))

def hx(v): return "%x" % (v & 0xffffffffffffffff)

class Rig:
    def __init__(self, name, rawlog):
        self.name = name
        self.p = SerialPeer(SOCK, rawlog, name=name)

    # ---- DDB helpers --------------------------------------------------
    def ddb(self, cmds, settle=2.0):
        """Enter DDB via sysctl (async), run cmds at db>, then continue."""
        p = self.p
        p.buf.clear()
        subprocess.Popen([RUNNER, "ddbtrigger"])
        m = p.wait(r"db> ", 45)
        if m is None:
            raise RuntimeError("no db> prompt after ddb trigger")
        out = {}
        for c in cmds:
            p.buf.clear()
            p.send(c.encode() + b"\n")
            time.sleep(settle)
            o = bytes(p.buf)
            out[c] = o.decode(errors="replace")
            print("[ddb] %-38s -> %s" % (c, " ".join(out[c].split())[:80]))
        p.buf.clear()
        p.send(b"continue\n")
        m = p.wait(r"restarted", 30)
        print("[ddb] continue -> %s" % ("RESUMED" if m else "no-restart-marker"))
        return out

    def attach_gdb_tab(self, force=False):
        """Set gdb_tab=&sio_consdev via DDB (documented flags=0x90 state)."""
        a = ADDRS
        lo = a["sio_consdev"] & 0xffffffff
        hi = a["sio_consdev"] >> 32
        out = self.ddb([
            "x/x %s" % hx(a["gdb_tab"]),
        ])
        cur = out.get("x/x %s" % hx(a["gdb_tab"]), "")
        if "81142e60" in cur and not force:
            print("[rig] gdb_tab already set")
            return True
        out = self.ddb([
            "wl %s %s" % (hx(a["gdb_tab"]), hx(lo)),
            "wl %s %s" % (hx(a["gdb_tab"] + 4), hx(hi)),
            "x/x %s" % hx(a["gdb_tab"]),
        ])
        chk = out.get("x/x %s" % hx(a["gdb_tab"]), "")
        ok = ("%x" % a["sio_consdev"]) in chk.replace("0x", "")
        print("[%s] gdb_tab attach: %s" % ("+" if ok else "-", chk.strip()))
        return ok

    # ---- stub session -------------------------------------------------
    def enter_stub(self, timeout=45):
        p = self.p
        p.buf.clear()
        for attempt in range(4):
            subprocess.call([RUNNER, "trigger"], timeout=40)
            m = p.wait(r"\$T[0-9a-fA-F]{2}[^#]*#[0-9a-fA-F]{2}", 20)
            if m:
                break
            print("[stub] trigger attempt %d: no T05 yet, retrying" % (attempt + 1))
        else:
            raise RuntimeError("no T05 from stub")
        t05 = parse_t05(m.group(0)[1:-3].decode())
        p.send(b"+")
        print("[stub] ALIVE rip=%s rbp=%s rsp=%s" % (t05.get("10"), t05.get("06"), t05.get("07")))
        return t05

    def detach(self):
        try:
            r = self.p.pkt("D", timeout=15)
            print("[stub] D -> %r" % r)
            m = self.p.wait(r"restarted", 30)
            print("[stub] %s" % ("kernel resumed" if m else "NO resume marker"))
            return r
        except Exception as e:
            print("[stub] detach failed: %s" % e)
            return None

    def close(self):
        try:
            self.p.pump(quiet=True)
            self.p.close()
        except Exception:
            pass

def read_mem(p, addr, n):
    return p.read_mem(addr, n)

def write_mem(p, addr, data):
    return p.write_mem(addr, data)

def find_registers(p, t05):
    return __import__("rsp").find_registers(p, t05)
