from __future__ import annotations import json import subprocess import sys from pathlib import Path from typing import Any from .config import CSHARP_SIDECAR_PATH def call_sidecar_action( action: str, payload: dict[str, Any] | None = None, *, command_fields: dict[str, Any] | None = None, timeout_seconds: int = 120, ) -> Any: request = { "action": action, "payload": payload or {}, } if command_fields: for key, value in command_fields.items(): if key in {"action", "payload"}: continue request[key] = value command, cwd = _resolve_sidecar_command() result = subprocess.run( command, input=json.dumps(request) + "\n", text=True, capture_output=True, cwd=str(cwd), timeout=timeout_seconds, check=False, ) stdout_line = _first_non_empty_line(result.stdout) if not stdout_line: stderr_line = _first_non_empty_line(result.stderr) details = stderr_line or f"sidecar exited with code {result.returncode}" raise RuntimeError(f"C# sidecar did not return JSON response: {details}") try: response = json.loads(stdout_line) except json.JSONDecodeError as exc: raise RuntimeError(f"Invalid JSON from C# sidecar: {stdout_line}") from exc if not isinstance(response, dict): raise RuntimeError("Unexpected sidecar response shape.") if bool(response.get("ok")): return response.get("data") error = str(response.get("error", "Unknown sidecar error")) raise RuntimeError(error) def _resolve_sidecar_command() -> tuple[list[str], Path]: project_root = Path(__file__).resolve().parents[2] sidecar_root = project_root / "journal-master" / "journal" override = _resolve_override_path(project_root) if override is not None: return [str(override)], sidecar_root for candidate in _candidate_sidecar_paths(sidecar_root): if candidate.exists(): return [str(candidate)], sidecar_root csproj = sidecar_root / "Journal.Sidecar" / "Journal.Sidecar.csproj" return ["dotnet", "run", "--project", str(csproj)], sidecar_root def _resolve_override_path(project_root: Path) -> Path | None: if not CSHARP_SIDECAR_PATH: return None override = Path(CSHARP_SIDECAR_PATH) if not override.is_absolute(): override = project_root / override return override def _candidate_sidecar_paths(sidecar_root: Path) -> list[Path]: sidecar_dir = sidecar_root / "Journal.Sidecar" candidates: list[Path] = [] if sys.platform == "win32": candidates.extend( [ sidecar_dir / "bin" / "Debug" / "net10.0" / "Journal.Sidecar.exe", sidecar_dir / "bin" / "Release" / "net10.0" / "Journal.Sidecar.exe", ] ) else: candidates.extend( [ sidecar_dir / "bin" / "Debug" / "net10.0" / "Journal.Sidecar", sidecar_dir / "bin" / "Release" / "net10.0" / "Journal.Sidecar", ] ) return candidates def _first_non_empty_line(text: str) -> str: for line in text.splitlines(): stripped = line.strip() if stripped: return stripped return ""