2026-02-23 20:12:10 -06:00

114 lines
3.2 KiB
Python

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 ""