fester/backend/api/debugger.py

121 lines
3.2 KiB
Python
Executable File

"""
Debugger API — pause / resume / step an in-flight pipeline.
The engine is set by main.py on startup via `set_engine(...)`. If no
engine is registered (e.g. when running the mock server), endpoints
return a structured error rather than crashing.
"""
from fastapi import APIRouter
from typing import Any, Dict, Optional
import threading
router = APIRouter(prefix="/debugger", tags=["debugger"])
# F-15: guards for module-level mutable state used from async handlers.
# Multiple WS clients can call pause/resume/step concurrently.
_engine_lock = threading.Lock()
_state_lock = threading.Lock()
# Single in-memory engine reference (set by main.py)
_ENGINE: Optional[Any] = None
_DEBUG_STATE: Dict[str, Any] = {
"paused": False,
"current_step": 0,
"history": [],
}
def set_engine(engine: Any) -> None:
global _ENGINE
with _engine_lock:
_ENGINE = engine
def get_engine() -> Optional[Any]:
with _engine_lock:
return _ENGINE
# -----------------------------
# RESUME
# -----------------------------
@router.post("/resume")
def resume() -> Dict[str, Any]:
with _state_lock:
_DEBUG_STATE["paused"] = False
eng = get_engine()
if eng and hasattr(eng, "resume"):
eng.resume()
return {"state": "running"}
# -----------------------------
# PAUSE
# -----------------------------
@router.post("/pause")
def pause() -> Dict[str, Any]:
with _state_lock:
_DEBUG_STATE["paused"] = True
eng = get_engine()
if eng and hasattr(eng, "pause"):
eng.pause()
return {"state": "paused"}
# -----------------------------
# STEP
# -----------------------------
@router.post("/step")
def step() -> Dict[str, Any]:
eng = get_engine()
if eng and hasattr(eng, "step"):
eng.step()
with _state_lock:
_DEBUG_STATE["current_step"] += 1
step_n = _DEBUG_STATE["current_step"]
return {"state": "stepped", "step": step_n}
# -----------------------------
# STEP BACK (replay-only)
# -----------------------------
@router.post("/step-back")
def step_back() -> Dict[str, Any]:
with _state_lock:
if _DEBUG_STATE["current_step"] > 0:
_DEBUG_STATE["current_step"] -= 1
step_n = _DEBUG_STATE["current_step"]
return {"state": "stepped_back", "step": step_n}
# -----------------------------
# STATE
# -----------------------------
@router.get("/state")
def state() -> Dict[str, Any]:
eng = get_engine()
active = bool(eng and hasattr(eng, "is_active") and eng.is_active())
with _state_lock:
snapshot = {
"paused": _DEBUG_STATE["paused"],
"current_step": _DEBUG_STATE["current_step"],
"engine_attached": eng is not None,
"engine_active": active,
}
return snapshot
# -----------------------------
# Legacy wrapper for api.py facade
# -----------------------------
def debugger_endpoint(session_id: str, registry: Any = None, bus: Any = None) -> Dict[str, Any]:
"""Legacy — return current debugger state for a session."""
with _state_lock:
return {
"session_id": session_id,
"paused": _DEBUG_STATE["paused"],
"current_step": _DEBUG_STATE["current_step"],
"engine_attached": _ENGINE is not None,
}