114 lines
3.2 KiB
Python
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 ""
|