"""risc-v-gsp-rm-debugger plugin system. A plugin is a normal Python module that exposes:: def register(ge): ge.log("my plugin loaded") 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 risc-v-gsp-rm-debugger with: * new CLI commands (including an MCP server later - roadmap); * runtime RE tools (extra watchpoint strategies, structure probes, coverage); * virtualisation-environment updates (e.g. record/replay harnesses); * AI-harness integration and reporting. Discovery order: 1. explicit ``--plugin PATH`` arguments; 2. colon-separated paths in ``GREENEYE_PLUGINS``; 3. ``/plugins/*.py`` (the bundled examples). """ from __future__ import annotations import importlib.util import os import sys from typing import Callable class GreenEyeAPI: """Handle passed to ``register(ge)``.""" def __init__(self, session, cli=None): self.session = session self.cli = cli self.commands: dict[str, tuple[Callable, str]] = {} self.loaded: list[str] = [] # -- convenience -------------------------------------------------------- @property def memory(self): return self.session.memory @property def symbols(self): return self.session.symbols @property def recorder(self): return self.session.recorder def log(self, message: str, **fields) -> None: self.session.recorder.event("plugin", message=message, **fields) def hook(self, target, on_enter=None, on_leave=None, **kw): return self.session.hook(target, on_enter=on_enter, on_leave=on_leave, **kw) def watch(self, target, size: int = 8, kind: str = "w", callback=None, **kw): return self.session.watch(target, size=size, kind=kind, callback=callback, **kw) def on_event(self, kind: str, callback) -> None: self.session.on_event(kind, callback) def resolve(self, target): return self.session.resolve(target) def add_command(self, name: str, fn: Callable, help: str = "") -> None: self.commands[name] = (fn, help) def run_command(self, name: str, args: list[str]) -> int: fn, _help = self.commands[name] return int(fn(self.session, args) or 0) class PluginManager: def __init__(self, session, cli=None): self.session = session self.api = GreenEyeAPI(session, cli=cli) self.loaded: list[str] = [] def default_paths(self) -> list[str]: here = os.path.dirname(os.path.abspath(__file__)) bundled = os.path.join(os.path.dirname(here), "plugins") paths = [] env = os.environ.get("GREENEYE_PLUGINS", "") paths += [p for p in env.split(os.pathsep) if p] if os.path.isdir(bundled): paths.append(bundled) return paths def load_all(self, explicit: list[str] | None = None) -> list[str]: for path in (explicit or []) + self.default_paths(): if os.path.isdir(path): for entry in sorted(os.listdir(path)): if entry.endswith(".py") and not entry.startswith("_"): self._load_file(os.path.join(path, entry)) elif path.endswith(".py") and os.path.isfile(path): self._load_file(path) return self.loaded def _load_file(self, path: str) -> None: name = os.path.splitext(os.path.basename(path))[0] mod_name = f"greeneye_plugin_{name}" try: spec = importlib.util.spec_from_file_location(mod_name, path) if spec is None or spec.loader is None: return module = importlib.util.module_from_spec(spec) sys.modules[mod_name] = module spec.loader.exec_module(module) register = getattr(module, "register", None) if register is None: self.session.recorder.event("plugin_error", plugin=name, error="no register(ge)") return register(self.api) self.loaded.append(name) self.session.recorder.event("plugin_loaded", plugin=name) except Exception as exc: # plugins must never break the session self.session.recorder.event("plugin_error", plugin=name, error=repr(exc))