Spaces:
Sleeping
Sleeping
| """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) | |