diff options
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/greeneye')
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/__init__.py | 4 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/analyze.py | 6 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/cli.py | 38 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/device_map.py | 2 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/elf.py | 2 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/errors.py | 4 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/gdbridge.py | 22 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/ghidra.py | 4 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/inject.py | 4 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/plugins.py | 4 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/record.py | 2 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/rsp.py | 2 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/session.py | 20 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/sim_backend.py | 2 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/symbols.py | 6 | ||||
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/greeneye/target.py | 35 |
16 files changed, 95 insertions, 62 deletions
diff --git a/debug/risc-v-gsp-rm-vm/greeneye/__init__.py b/debug/risc-v-gsp-rm-vm/greeneye/__init__.py index 717160c..4d55b89 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/__init__.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/__init__.py @@ -1,7 +1,7 @@ -"""green-eye: Frida-like runtime instrumentation for GSP/uproc firmware in a +"""risc-v-gsp-rm-debugger: Frida-like runtime instrumentation for GSP/uproc firmware in a RISC-V QEMU VM. -green-eye talks to QEMU's gdbstub directly (no GDB dependency) and layers a +risc-v-gsp-rm-debugger talks to QEMU's gdbstub directly (no GDB dependency) and layers a scriptable instrumentation API on top: breakpoints, hooks, watchpoints, memory inspection, structure decoding, session recording and post-run analysis. diff --git a/debug/risc-v-gsp-rm-vm/greeneye/analyze.py b/debug/risc-v-gsp-rm-vm/greeneye/analyze.py index 1c7e930..cc8b8f6 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/analyze.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/analyze.py @@ -143,9 +143,9 @@ def render_summary_from(meta: dict, events: list[dict], symbols=None, max_rows: if trace is None: trace = [e for e in events if _trace_kind(e) in _TRACE_KINDS] lines: list[str] = [] - lines.append(f"# green-eye session summary: {meta.get('name', '?')}") + lines.append(f"# risc-v-gsp-rm-debugger session summary: {meta.get('name', '?')}") lines.append("") - lines.append(f"* format: `{meta.get('format', '?')}` green-eye {meta.get('greeneye', '?')}") + lines.append(f"* format: `{meta.get('format', '?')}` risc-v-gsp-rm-debugger {meta.get('greeneye', '?')}") lines.append(f"* directory: `{meta.get('dir', '.')}`") lines.append(f"* events: {len(events)}") counts = Counter(event.get("type", "?") for event in events) @@ -396,7 +396,7 @@ def compare_sessions(a_dir: str, b_dir: str, symbols=None, max_rows: int = 40) - a_meta, b_meta = load_meta(a_dir), load_meta(b_dir) a_events, b_events = load_events(a_dir), load_events(b_dir) a_trace, b_trace = load_trace(a_dir, a_meta), load_trace(b_dir, b_meta) - lines = [f"# green-eye session comparison", "", f"* A: {a_meta.get('name')} ({a_dir})", f"* B: {b_meta.get('name')} ({b_dir})", ""] + lines = [f"# risc-v-gsp-rm-debugger session comparison", "", f"* A: {a_meta.get('name')} ({a_dir})", f"* B: {b_meta.get('name')} ({b_dir})", ""] def trace_kind(record: dict) -> str: kind = _trace_kind(record) diff --git a/debug/risc-v-gsp-rm-vm/greeneye/cli.py b/debug/risc-v-gsp-rm-vm/greeneye/cli.py index e7023ec..3f3c937 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/cli.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/cli.py @@ -1,4 +1,4 @@ -"""green-eye command line interface. +"""risc-v-gsp-rm-debugger command line interface. Commands: info environment/tooling check (incl. native simulator availability) @@ -14,7 +14,7 @@ Commands: ghidra serve a live session to GhidraScripts over the JSON-lines bridge analyze render a recorded session to a symbol-annotated report compare A/B two recorded sessions at the symbolic level - symbols build/inspect a green-eye symbol database from Ghidra exports + symbols build/inspect a risc-v-gsp-rm-debugger symbol database from Ghidra exports plugins list discoverable plugins """ from __future__ import annotations @@ -108,7 +108,7 @@ def _start_session( def _print_session_dir(session) -> None: directory = getattr(session.recorder, "dir", None) if directory: - print(f"[green-eye] session: {directory}") + print(f"[risc-v-gsp-rm-debugger] session: {directory}") def _add_backend_arg(parser) -> None: @@ -176,7 +176,7 @@ def _trace_spec(args): def cmd_info(args) -> int: from .target import DEFAULT_QEMU - print(f"green-eye {ge.__version__}") + print(f"risc-v-gsp-rm-debugger {ge.__version__}") print(f"tool root: {TOOL_ROOT}") qemu = os.environ.get("GREENEYE_QEMU", DEFAULT_QEMU) import shutil @@ -314,8 +314,8 @@ def cmd_run(args) -> int: class GreenEyeRepl(cmd.Cmd): - intro = "green-eye interactive instrumentation. Type 'help' for commands." - prompt = "greeneye> " + intro = "risc-v-gsp-rm-debugger interactive instrumentation. Type 'help' for commands." + prompt = "risc-v-gsp-rm-debugger> " def __init__(self, session, manager): super().__init__() @@ -519,15 +519,15 @@ def cmd_launch(args) -> int: raise GreenEyeError(f"kernel not found: {kernel}") session = ge.launch(kernel=str(kernel), record=args.record, name="launch", keep_alive=True) port = session.target.port - print(f"[green-eye] target running: {kernel}") - print(f"[green-eye] gdbstub: 127.0.0.1:{port}") - print(f"[green-eye] attach: ./greeneye.sh attach --port {port} --kernel {kernel}") - print("[green-eye] Ctrl-C stops waiting; QEMU keeps running for attach.") + print(f"[risc-v-gsp-rm-debugger] target running: {kernel}") + print(f"[risc-v-gsp-rm-debugger] gdbstub: 127.0.0.1:{port}") + print(f"[risc-v-gsp-rm-debugger] attach: ./risc-v-gsp-rm-debugger.sh attach --port {port} --kernel {kernel}") + print("[risc-v-gsp-rm-debugger] Ctrl-C stops waiting; QEMU keeps running for attach.") try: while True: time.sleep(1) except KeyboardInterrupt: - print("[green-eye] detached (QEMU still running)") + print("[risc-v-gsp-rm-debugger] detached (QEMU still running)") return 0 @@ -678,10 +678,10 @@ def cmd_ghidra(args) -> int: ) bridge = GhidraBridge(session, port=args.bridge_port) port = bridge.start() - print(f"[green-eye] ghidra bridge on 127.0.0.1:{port}") - print(f"[green-eye] session: {getattr(session.recorder, 'dir', None)}") - print("[green-eye] in Ghidra: analyzeHeadless ... -postScript GreenEyeControl.java " + str(port)) - print("[green-eye] Ctrl-C to stop the bridge") + print(f"[risc-v-gsp-rm-debugger] ghidra bridge on 127.0.0.1:{port}") + print(f"[risc-v-gsp-rm-debugger] session: {getattr(session.recorder, 'dir', None)}") + print("[risc-v-gsp-rm-debugger] in Ghidra: analyzeHeadless ... -postScript GreenEyeControl.java " + str(port)) + print("[risc-v-gsp-rm-debugger] Ctrl-C to stop the bridge") try: bridge.serve_forever() except KeyboardInterrupt: @@ -754,8 +754,8 @@ def cmd_plugins(args) -> int: def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(prog="greeneye", description=__doc__.splitlines()[0]) - parser.add_argument("--version", action="version", version=f"green-eye {ge.__version__}") + parser = argparse.ArgumentParser(prog="risc-v-gsp-rm-debugger", description=__doc__.splitlines()[0]) + parser.add_argument("--version", action="version", version=f"risc-v-gsp-rm-debugger {ge.__version__}") sub = parser.add_subparsers(dest="command", required=True) sub.add_parser("info", help="environment/tooling check").set_defaults(fn=cmd_info) @@ -798,7 +798,7 @@ def build_parser() -> argparse.ArgumentParser: probe.add_argument("--no-relocate", action="store_true") probe.add_argument("--instructions", type=int, default=0) probe.add_argument("--duration", type=float, default=1.0) - probe.add_argument("--symbols", help="green-eye symbol DB JSON to annotate the stop") + probe.add_argument("--symbols", help="risc-v-gsp-rm-debugger symbol DB JSON to annotate the stop") probe.add_argument("--record", nargs="?", const=True, default=None) probe.set_defaults(fn=cmd_probe) @@ -881,7 +881,7 @@ def main(argv: list[str] | None = None) -> int: try: return args.fn(args) except GreenEyeError as exc: - print(f"green-eye error: {exc}", file=sys.stderr) + print(f"risc-v-gsp-rm-debugger error: {exc}", file=sys.stderr) return 2 except KeyboardInterrupt: print("interrupted", file=sys.stderr) diff --git a/debug/risc-v-gsp-rm-vm/greeneye/device_map.py b/debug/risc-v-gsp-rm-vm/greeneye/device_map.py index 11ac656..2acb2a5 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/device_map.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/device_map.py @@ -515,7 +515,7 @@ def _cmd_demo(args) -> int: def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( prog="python3 -m greeneye.device_map", - description="Harvest, inspect and merge green-eye first-touch device maps.", + description="Harvest, inspect and merge risc-v-gsp-rm-debugger first-touch device maps.", ) sub = parser.add_subparsers(dest="cmd", required=True) diff --git a/debug/risc-v-gsp-rm-vm/greeneye/elf.py b/debug/risc-v-gsp-rm-vm/greeneye/elf.py index abfccd9..3b8f04a 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/elf.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/elf.py @@ -7,7 +7,7 @@ Needed for three things: * entry-point detection for the first-fault firmware probe. This is not a full ELF library; it parses headers, PT_LOAD segments and -``.symtab`` symbols, which is all green-eye needs. +``.symtab`` symbols, which is all risc-v-gsp-rm-debugger needs. """ from __future__ import annotations diff --git a/debug/risc-v-gsp-rm-vm/greeneye/errors.py b/debug/risc-v-gsp-rm-vm/greeneye/errors.py index fab5874..dc5771b 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/errors.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/errors.py @@ -1,9 +1,9 @@ -"""green-eye error types. Keep them few and meaningful.""" +"""risc-v-gsp-rm-debugger error types. Keep them few and meaningful.""" from __future__ import annotations class GreenEyeError(Exception): - """Base class for all green-eye errors.""" + """Base class for all risc-v-gsp-rm-debugger errors.""" class RspError(GreenEyeError): diff --git a/debug/risc-v-gsp-rm-vm/greeneye/gdbridge.py b/debug/risc-v-gsp-rm-vm/greeneye/gdbridge.py index cc06e43..eafac39 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/gdbridge.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/gdbridge.py @@ -1,4 +1,4 @@ -"""Ghidra control bridge: let GhidraScripts drive a green-eye session. +"""Ghidra control bridge: let GhidraScripts drive a risc-v-gsp-rm-debugger session. A tiny JSON-lines server over localhost. Each request is one line:: @@ -20,6 +20,9 @@ import threading from .errors import GreenEyeError +#: The bridge is unauthenticated and write-capable; it must never be exposed. +LOOPBACK_HOSTS = ("127.0.0.1", "localhost", "::1") + class GhidraBridge: def __init__(self, session, host: str = "127.0.0.1", port: int = 0): @@ -34,6 +37,8 @@ class GhidraBridge: # -- lifecycle --------------------------------------------------------- def start(self) -> int: + if self.host not in LOOPBACK_HOSTS: + raise ValueError(f"refusing non-loopback bind {self.host!r}; the Ghidra bridge is local-only") server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((self.host, self.port)) @@ -46,12 +51,21 @@ class GhidraBridge: def stop(self) -> None: self._stop.set() - if self._server is not None: + server = self._server + self._server = None + if server is not None: + # shutdown() wakes a thread blocked in accept(); close() alone does + # not, which left the listener up after a <close> command. + try: + server.shutdown(socket.SHUT_RDWR) + except OSError: + pass try: - self._server.close() + server.close() except OSError: pass - self._server = None + if self._thread is not None and self._thread.is_alive() and threading.current_thread() is not self._thread: + self._thread.join(timeout=2.0) def serve_forever(self) -> None: if self._thread is None: diff --git a/debug/risc-v-gsp-rm-vm/greeneye/ghidra.py b/debug/risc-v-gsp-rm-vm/greeneye/ghidra.py index 399a355..a7d547a 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/ghidra.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/ghidra.py @@ -2,14 +2,14 @@ Two flows: -*out of Ghidra*: export function names/addresses/sizes to a green-eye JSON +*out of Ghidra*: export function names/addresses/sizes to a risc-v-gsp-rm-debugger JSON symbol database (from ``names.tsv``, ``decompiled_index.tsv``, exports or ELF); *hits*: turn a recorded session into ``greeneye_hits.tsv`` (``addr<TAB>hits<TAB>symbol``) that ``MarkGreenEyeHits.java`` can annotate in the Ghidra project. This is the static/dynamic join: Ghidra owns the address space and -decompilation, green-eye owns what actually executed. +decompilation, risc-v-gsp-rm-debugger owns what actually executed. """ from __future__ import annotations diff --git a/debug/risc-v-gsp-rm-vm/greeneye/inject.py b/debug/risc-v-gsp-rm-vm/greeneye/inject.py index f9605fc..eb4b16a 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/inject.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/inject.py @@ -1,4 +1,4 @@ -"""Frida-like service injection for green-eye. +"""Frida-like service injection for risc-v-gsp-rm-debugger. Loads a position-independent RV64 service blob into a live target, registers its dispatch entry with the firmware (on the demo uproc: a handler-table slot @@ -8,7 +8,7 @@ firmware's own mailbox dispatcher. On a future GSP-RM simulator the same shape becomes "inject an RM engine and register it with the simulated RM core"; see ``docs/12-INJECTION-SDK.md`` and -``GREEN-EYE-ROADMAP.md``. +``RISC-V-GSP-RM-DEBUGGER-ROADMAP.md``. """ from __future__ import annotations diff --git a/debug/risc-v-gsp-rm-vm/greeneye/plugins.py b/debug/risc-v-gsp-rm-vm/greeneye/plugins.py index af4545f..6c597ac 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/plugins.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/plugins.py @@ -1,4 +1,4 @@ -"""green-eye plugin system. +"""risc-v-gsp-rm-debugger plugin system. A plugin is a normal Python module that exposes:: @@ -7,7 +7,7 @@ A plugin is a normal Python module that exposes:: ge.add_command("mycmd", run_mycmd, help="…") ge.hook("rpc_sum", on_enter=lambda hit: ge.log(f"sum({hit.arg(0)})")) -Plugins can extend green-eye with: +Plugins can extend risc-v-gsp-rm-debugger with: * new CLI commands (including an MCP server later - roadmap); * runtime RE tools (extra watchpoint strategies, structure probes, coverage); diff --git a/debug/risc-v-gsp-rm-vm/greeneye/record.py b/debug/risc-v-gsp-rm-vm/greeneye/record.py index 0b025b5..91c7fb5 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/record.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/record.py @@ -1,6 +1,6 @@ """Session recording: events.jsonl + snapshots + summary. -A recorded session is the durable artifact green-eye produces: a complete, +A recorded session is the durable artifact risc-v-gsp-rm-debugger 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 diff --git a/debug/risc-v-gsp-rm-vm/greeneye/rsp.py b/debug/risc-v-gsp-rm-vm/greeneye/rsp.py index 0263c63..b482f4c 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/rsp.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/rsp.py @@ -1,6 +1,6 @@ """Pure-Python GDB Remote Serial Protocol (RSP) client for QEMU's gdbstub. -green-eye deliberately avoids GDB: the whole instrumentation stack is a Python +risc-v-gsp-rm-debugger deliberately avoids GDB: the whole instrumentation stack is a Python library, so sessions, probes and plugins are scriptable end to end. This module implements the RSP subset QEMU needs: packet framing/acks, no-ack mode, registers (with layout read from ``target.xml``), memory, software/hardware diff --git a/debug/risc-v-gsp-rm-vm/greeneye/session.py b/debug/risc-v-gsp-rm-vm/greeneye/session.py index fffc60e..2ef13a4 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/session.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/session.py @@ -1,4 +1,4 @@ -"""green-eye instrumentation session: hooks, breakpoints, watchpoints, events. +"""risc-v-gsp-rm-debugger instrumentation session: hooks, breakpoints, watchpoints, events. This is the Frida-like core. It is deliberately transport-agnostic; the RSP client is the default backend, and a Session can also be constructed around a @@ -18,14 +18,22 @@ from .rsp import RspClient, StopReply from .symbols import Symbol, SymbolDB #: Registers saved and restored around ``Session.call`` (RISC-V ABI + pc). +#: ``fp`` is x8/s0 under QEMU's target.xml name; both spellings are listed so +#: the restore works whichever name the transport exposes. RESTORE_REGS = ( - ["ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1"] + ["ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "fp", "s1"] + [f"a{i}" for i in range(8)] + [f"s{i}" for i in range(2, 12)] + [f"t{i}" for i in range(3, 7)] + ["pc"] ) +#: Budget for the stop reply after a budget-interrupt. The default client +#: socket timeout can be exhausted on a heavily loaded host before QEMU gets +#: scheduled; a longer explicit budget keeps a probe from being misclassified +#: as an error just because the host was busy. +INTERRUPT_TIMEOUT = 30.0 + @dataclass class Hit: @@ -392,7 +400,7 @@ class Session: try: stop = self.client.continue_(timeout=remaining) except RspTimeout: - self.client.interrupt() + self.client.interrupt(timeout=INTERRUPT_TIMEOUT) raise RspTimeout(f"run_until {addr:#x}: not reached in {timeout}s") hit = self._dispatch_stop(stop) if hit is not None and hit.addr == addr and hit.kind == "breakpoint": @@ -433,7 +441,7 @@ class Session: try: stop = self.client.continue_(timeout=remaining) except RspTimeout: - self.client.interrupt() + self.client.interrupt(timeout=INTERRUPT_TIMEOUT) raise RspTimeout(f"call {addr:#x}: no return in {timeout}s") hit = self._dispatch_stop(stop) pc = self.client.pc @@ -483,7 +491,7 @@ class Session: except RspTimeout: if deadline is None: raise - stop = self.client.interrupt() + stop = self.client.interrupt(timeout=INTERRUPT_TIMEOUT) interrupted = True stops += 1 self._dispatch_stop(stop) @@ -496,7 +504,7 @@ class Session: return stops def stop(self) -> Hit | None: - stop = self.client.interrupt() + stop = self.client.interrupt(timeout=INTERRUPT_TIMEOUT) return self._dispatch_stop(stop) # -- stop dispatch ----------------------------------------------------- diff --git a/debug/risc-v-gsp-rm-vm/greeneye/sim_backend.py b/debug/risc-v-gsp-rm-vm/greeneye/sim_backend.py index d17076f..a2d2c6a 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/sim_backend.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/sim_backend.py @@ -1,4 +1,4 @@ -"""Native green-eye execution backend (M1): deterministic RV64IMAC in Python. +"""Native risc-v-gsp-rm-debugger execution backend (M1): deterministic RV64IMAC in Python. ``ge.launch(kernel=..., backend="native")`` builds a :class:`SimTarget` around :class:`sim.machine.Machine` and exposes a :class:`SimClient` whose method diff --git a/debug/risc-v-gsp-rm-vm/greeneye/symbols.py b/debug/risc-v-gsp-rm-vm/greeneye/symbols.py index cb11f7f..d240fc4 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/symbols.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/symbols.py @@ -1,4 +1,4 @@ -"""Symbol resolution and address translation for green-eye. +"""Symbol resolution and address translation for risc-v-gsp-rm-debugger. Inputs supported (all optional, merged into one database): @@ -8,7 +8,7 @@ Inputs supported (all optional, merged into one database): - ``decompiled_index.tsv`` / bucket TSVs ``<program_addr>\t<file_addr>\t<size>\t<name>\t<bucket>\t<line>\t<status>``, - ``exports.tsv`` ``addr\tfile_addr\tsize\tname``, -* green-eye native JSON (``greeneye-symbols.json``). +* risc-v-gsp-rm-debugger native JSON (``greeneye-symbols.json``). Address conventions (matching the project docs): * Ghidra program address = file vaddr + image base (``0x100000`` for libcuda); @@ -193,7 +193,7 @@ class SymbolDB: return count def load_json(self, path: str) -> int: - """Load a green-eye JSON symbol DB; raise GreenEyeError on bad input.""" + """Load a risc-v-gsp-rm-debugger JSON symbol DB; raise GreenEyeError on bad input.""" try: with open(path, "r", encoding="utf-8") as fh: payload = json.load(fh) diff --git a/debug/risc-v-gsp-rm-vm/greeneye/target.py b/debug/risc-v-gsp-rm-vm/greeneye/target.py index 273f216..669a55d 100644 --- a/debug/risc-v-gsp-rm-vm/greeneye/target.py +++ b/debug/risc-v-gsp-rm-vm/greeneye/target.py @@ -1,4 +1,4 @@ -"""QEMU target lifecycle for green-eye. +"""QEMU target lifecycle for risc-v-gsp-rm-debugger. Owns the ``qemu-system-riscv64`` process, the gdbstub endpoint, the serial capture and the QEMU trace log. Both firmware paths are supported: @@ -76,8 +76,10 @@ class QemuTarget: os.makedirs(self.workdir, exist_ok=True) self.uart_log = uart_log or os.path.join(self.workdir, "uart.log") self.qemu_log = qemu_log or os.path.join(self.workdir, "qemu.log") + self.stderr_log = os.path.join(self.workdir, "qemu.stderr.log") self.keep_alive = keep_alive self.process: subprocess.Popen | None = None + self._stderr_fh = None self._segment_files: list[str] = [] self._cmd: list[str] = [] @@ -118,15 +120,19 @@ class QemuTarget: def start(self, paused: bool = True, timeout: float = 15.0) -> "QemuTarget": self._cmd = self._build_command(paused) + # Never use an undrained stderr=PIPE: a chatty QEMU would block once + # the pipe buffer fills and then stop answering the gdbstub. + self._stderr_fh = open(self.stderr_log, "ab") try: self.process = subprocess.Popen( self._cmd, stdout=subprocess.DEVNULL, - stderr=subprocess.PIPE, + stderr=self._stderr_fh, cwd=self.workdir, start_new_session=True, ) except OSError as exc: + self._close_stderr() raise TargetError(f"cannot start QEMU: {exc}") from exc try: self.wait_for_port(timeout=timeout) @@ -140,11 +146,11 @@ class QemuTarget: while time.monotonic() < deadline: if self.process is not None and self.process.poll() is not None: stderr = b"" - if self.process.stderr is not None: - try: - stderr = self.process.stderr.read() or b"" - except OSError: - pass + try: + with open(self.stderr_log, "rb") as fh: + stderr = fh.read() + except OSError: + pass raise TargetError( f"QEMU exited early rc={self.process.returncode}: " f"{stderr.decode('utf-8', 'replace')[-2000:]}" @@ -159,8 +165,17 @@ class QemuTarget: def alive(self) -> bool: return self.process is not None and self.process.poll() is None + def _close_stderr(self) -> None: + if self._stderr_fh is not None: + try: + self._stderr_fh.close() + except OSError: + pass + self._stderr_fh = None + def kill(self, timeout: float = 5.0) -> None: if self.process is None: + self._close_stderr() return if self.process.poll() is None: try: @@ -181,11 +196,7 @@ class QemuTarget: except ProcessLookupError: pass self.process.wait(timeout=timeout) - if self.process.stderr is not None: - try: - self.process.stderr.close() - except OSError: - pass + self._close_stderr() self.process = None # -- observation ------------------------------------------------------- |
