diff options
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/greeneye/record.py')
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/record.py | 156 |
1 files changed, 156 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/greeneye/record.py b/debug/risc-v-gsp-rm-vm/greeneye/record.py new file mode 100644 index 0000000..bf70e58 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/greeneye/record.py @@ -0,0 +1,156 @@ +"""Session recording: events.jsonl + snapshots + summary. + +A recorded session is the durable artifact green-eye produces: a complete, +symbol-annotatable map of what the firmware did (hook hits, watch hits, +memory writes, snapshots, coverage steps) that can be replayed offline, +diffed between runs (bare-metal vs VFIO-mdev, old GSP vs new GSP) and +annotated back into Ghidra. +""" +from __future__ import annotations + +import json +import os +import time +import zlib + +FORMAT = "greeneye-session/1" + + +class NullRecorder: + dir: str | None = None + + def event(self, event_type: str, **fields) -> None: + pass + + def snapshot(self, label: str, addr: int, data: bytes) -> None: + pass + + def blob(self, name: str, data: bytes) -> None: + pass + + def close(self) -> None: + pass + + +class Recorder: + def __init__(self, root: str, *, name: str = "session", meta: dict | None = None): + self.dir = root + os.makedirs(self.dir, exist_ok=True) + self.name = name + self.meta = dict(meta or {}) + self.meta.update({"format": FORMAT, "name": name, "created": time.time(), "greeneye": _version()}) + self._events_path = os.path.join(self.dir, "events.jsonl") + self._fh = open(self._events_path, "w", encoding="utf-8") + self._t0 = time.monotonic() + self._counts: dict[str, int] = {} + self._snapshots: list[dict] = [] + self._closed = False + + @classmethod + def new(cls, *, base: str | None = None, name: str = "session", meta: dict | None = None) -> "Recorder": + base = base or os.environ.get("GREENEYE_DUMPS") or os.path.join(os.getcwd(), "dumps") + stamp = time.strftime("%Y%m%d-%H%M%S") + return cls(os.path.join(base, f"{stamp}-{name}"), name=name, meta=meta) + + # -- writing ----------------------------------------------------------- + + def event(self, event_type: str, **fields) -> None: + if self._closed: + return + record = {"t": round(time.monotonic() - self._t0, 6), "type": event_type} + for key, value in fields.items(): + record[key] = _jsonable(value) + self._fh.write(json.dumps(record, sort_keys=True) + "\n") + self._counts[event_type] = self._counts.get(event_type, 0) + 1 + + def snapshot(self, label: str, addr: int, data: bytes) -> str: + rel = os.path.join("snapshots", f"{label}-{addr:#x}-{len(data)}B.bin") + path = os.path.join(self.dir, rel) + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "wb") as fh: + fh.write(zlib.compress(data)) + info = {"label": label, "addr": addr, "size": len(data), "path": rel, "sha256": _sha(data)} + self._snapshots.append(info) + self.event("snapshot_saved", **info) + return rel + + def blob(self, name: str, data: bytes) -> str: + rel = os.path.join("blobs", name) + path = os.path.join(self.dir, rel) + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "wb") as fh: + fh.write(data) + self.event("blob_saved", path=rel, size=len(data)) + return rel + + # -- closing ----------------------------------------------------------- + + def close(self, write_summary: bool = True) -> None: + if self._closed: + return + self.event("session_end") + self._fh.close() + self._closed = True + self.meta["counts"] = self._counts + self.meta["snapshots"] = self._snapshots + with open(os.path.join(self.dir, "session.json"), "w", encoding="utf-8") as fh: + json.dump(self.meta, fh, indent=1, sort_keys=True) + if write_summary: + try: + from .analyze import render_summary + + report = render_summary(self.dir) + with open(os.path.join(self.dir, "summary.md"), "w", encoding="utf-8") as fh: + fh.write(report) + except Exception as exc: # pragma: no cover - never break close + with open(os.path.join(self.dir, "summary-error.txt"), "w", encoding="utf-8") as fh: + fh.write(f"summary failed: {exc}\n") + + def __enter__(self) -> "Recorder": + return self + + def __exit__(self, *exc) -> None: + self.close() + + +def load_events(session_dir: str) -> list[dict]: + path = session_dir + if os.path.isdir(session_dir): + path = os.path.join(session_dir, "events.jsonl") + events: list[dict] = [] + with open(path, "r", encoding="utf-8") as fh: + for line in fh: + line = line.strip() + if line: + events.append(json.loads(line)) + return events + + +def load_meta(session_dir: str) -> dict: + with open(os.path.join(session_dir, "session.json"), "r", encoding="utf-8") as fh: + return json.load(fh) + + +def _jsonable(value): + if isinstance(value, bytes): + return value.hex() + if isinstance(value, (set, tuple)): + return list(value) + if isinstance(value, dict): + return {str(k): _jsonable(v) for k, v in value.items()} + return value + + +def _sha(data: bytes) -> str: + import hashlib + + return hashlib.sha256(data).hexdigest() + + +def _version() -> str: + try: + from . import __version__ + + return __version__ + except Exception: # pragma: no cover + return "unknown" |
