SWMM_MCP_Server_Claude / swmm_core.py
razaali10's picture
Upload 20 files
e545bf5 verified
Raw
History Blame Contribute Delete
3.63 kB
"""Crash-isolated deterministic OpenSWMM simulation core.
The OpenSWMM engine is a native extension. It is never loaded into the
long-lived Streamlit process. Each simulation runs in a short-lived worker
process and returns only serialisable Python data.
"""
from __future__ import annotations
import os
import pickle
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any
class SwmmWorkerError(RuntimeError):
"""Raised when the isolated OpenSWMM worker cannot complete."""
def run_swmm(
inp_path: str | Path,
rpt_path: str | Path | None = None,
out_path: str | Path | None = None,
timeout_s: int = 900,
) -> dict[str, Any]:
"""Run OpenSWMM in an isolated subprocess.
Isolation prevents a native OpenSWMM segmentation fault from terminating
Streamlit. The worker deliberately uses ``os._exit`` after serialising its
result so native-library finalisers cannot crash the parent application.
"""
inp = Path(inp_path).resolve()
if not inp.is_file():
raise FileNotFoundError(f"SWMM input file not found: {inp}")
rpt = Path(rpt_path).resolve() if rpt_path else inp.with_suffix(".rpt")
out = Path(out_path).resolve() if out_path else inp.with_suffix(".out")
worker = Path(__file__).with_name("swmm_worker.py")
if not worker.is_file():
raise FileNotFoundError(f"OpenSWMM worker not found: {worker}")
fd, result_name = tempfile.mkstemp(prefix="swmm_result_", suffix=".pkl")
os.close(fd)
result_file = Path(result_name)
worker_python = os.environ.get("SWMM_WORKER_PYTHON", "/opt/swmm-venv/bin/python")
if not Path(worker_python).is_file():
raise FileNotFoundError(
f"Isolated OpenSWMM interpreter not found: {worker_python}"
)
cmd = [
worker_python,
"-u",
str(worker),
"--inp", str(inp),
"--rpt", str(rpt),
"--out", str(out),
"--result", str(result_file),
]
try:
completed = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=timeout_s,
check=False,
env={**os.environ, "PYTHONUNBUFFERED": "1"},
)
payload = None
if result_file.exists() and result_file.stat().st_size:
with result_file.open("rb") as f:
payload = pickle.load(f)
if isinstance(payload, dict) and payload.get("ok"):
results = payload["results"]
results.setdefault("metadata", {})["worker_stdout"] = completed.stdout[-4000:]
results["metadata"]["worker_stderr"] = completed.stderr[-4000:]
results["metadata"]["worker_exit_code"] = completed.returncode
return results
detail = ""
if isinstance(payload, dict):
detail = payload.get("error", "")
if not detail:
detail = completed.stderr.strip() or completed.stdout.strip()
if completed.returncode in (-11, 139):
detail = (
"The OpenSWMM worker encountered a native segmentation fault. "
"The Streamlit process remained protected. " + detail
).strip()
raise SwmmWorkerError(
f"OpenSWMM worker failed with exit code {completed.returncode}. {detail}".strip()
)
except subprocess.TimeoutExpired as exc:
raise SwmmWorkerError(
f"OpenSWMM simulation exceeded the {timeout_s}-second timeout."
) from exc
finally:
result_file.unlink(missing_ok=True)