Spaces:
Running
Running
| from __future__ import annotations | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| from typing import Any | |
| ROOT_DIR = Path(__file__).resolve().parents[1] | |
| EVALS_SRC_DIR = ROOT_DIR / "evals" / "src" | |
| if str(EVALS_SRC_DIR) not in sys.path: | |
| sys.path.insert(0, str(EVALS_SRC_DIR)) | |
| from evals_verifier import ( # type: ignore | |
| check_bridges_structural_validity, | |
| check_galaxies_structural_validity, | |
| check_undead_structural_validity, | |
| verify_with_rlp, | |
| ) | |
| from puzzles import PUZZLES # type: ignore | |
| # Puzzle families whose "solved" check runs in the native RLP C library (loaded | |
| # in-process via ctypes). That library can segfault or hang when its puzzle | |
| # objects are reused across many requests in a long-lived server process, which | |
| # crashes the web worker and surfaces as an opaque HTTP 500 (and, sometimes, a | |
| # hung request). We run every native check in a short-lived subprocess so a | |
| # native crash/hang kills only the child and we return a clean "not solved" | |
| # result instead of taking down the worker. (flow_free already shells out to its | |
| # own compiled solver binary, so it is naturally isolated.) | |
| SUBPROCESS_RLP_PUZZLES = {"undead", "loopy", "bridges", "galaxies", "pattern"} | |
| RLP_VERIFIER_TIMEOUT_SECONDS = 20 | |
| # flow_free shells out to a compiled C solver with no internal timeout and can | |
| # raise SystemExit on a missing compiler/binary; isolate it too so it can never | |
| # hang or crash the worker. Enumerating all solutions can be slower than the | |
| # RLP solved-check, so it gets a more generous budget. | |
| FLOW_FREE_TIMEOUT_SECONDS = 30 | |
| class VerificationService: | |
| def _structural_validity( | |
| self, *, puzzle_type: str, board_ascii: str, problem_ascii: str | |
| ) -> bool: | |
| # Pure-Python structural pre-checks are safe to run in-process and also | |
| # cover the "start board was modified" case for bridges/galaxies. Puzzle | |
| # families without such a check are treated as structurally valid here; | |
| # the subprocess solved-check is the source of truth for correctness. | |
| if puzzle_type == "bridges": | |
| return bool( | |
| check_bridges_structural_validity(board_ascii, problem_ascii=problem_ascii) | |
| ) | |
| if puzzle_type == "galaxies": | |
| return bool( | |
| check_galaxies_structural_validity(board_ascii, problem_ascii=problem_ascii) | |
| ) | |
| if puzzle_type == "undead": | |
| return bool(check_undead_structural_validity(board_ascii)) | |
| return True | |
| def _verify_rlp_with_subprocess( | |
| self, | |
| *, | |
| puzzle_type: str, | |
| problem_ascii: str, | |
| board_ascii: str, | |
| args: str, | |
| ) -> dict[str, Any]: | |
| result = { | |
| "board_exists": bool(board_ascii), | |
| "board_valid": False, | |
| "board_modified": False, | |
| "correct": False, | |
| } | |
| if not board_ascii: | |
| return result | |
| result["board_valid"] = self._structural_validity( | |
| puzzle_type=puzzle_type, board_ascii=board_ascii, problem_ascii=problem_ascii | |
| ) | |
| if not result["board_valid"]: | |
| result["board_modified"] = True | |
| return result | |
| verifier_path = ROOT_DIR / "submodules" / "rlp" / "verifier.py" | |
| command = [sys.executable, str(verifier_path), puzzle_type, "--arg", args] | |
| # The RLP libraries pull in pygame, which noisily probes audio/video on | |
| # import; force the dummy drivers (and silence warnings) so a headless | |
| # verifier run does not pollute the captured stderr. | |
| env = { | |
| **os.environ, | |
| "SDL_AUDIODRIVER": "dummy", | |
| "SDL_VIDEODRIVER": "dummy", | |
| "PYTHONWARNINGS": "ignore", | |
| } | |
| try: | |
| completed = subprocess.run( | |
| command, | |
| input=board_ascii, | |
| text=True, | |
| capture_output=True, | |
| cwd=ROOT_DIR, | |
| timeout=RLP_VERIFIER_TIMEOUT_SECONDS, | |
| check=False, | |
| env=env, | |
| ) | |
| except subprocess.TimeoutExpired: | |
| result["error"] = f"{puzzle_type.capitalize()} verification timed out." | |
| return result | |
| result["correct"] = completed.returncode == 0 and "SOLVED" in completed.stdout | |
| if completed.stderr.strip(): | |
| result["stderr"] = completed.stderr.strip()[:500] | |
| return result | |
| def _verify_flow_free_with_subprocess( | |
| self, *, problem_ascii: str, board_ascii: str | |
| ) -> dict[str, Any]: | |
| result = { | |
| "board_exists": bool(board_ascii), | |
| "board_valid": False, | |
| "board_modified": False, | |
| "correct": False, | |
| } | |
| if not board_ascii: | |
| return result | |
| runner_path = Path(__file__).resolve().parent / "flow_free_runner.py" | |
| payload = json.dumps({"problem_ascii": problem_ascii, "board_ascii": board_ascii}) | |
| env = {**os.environ, "PYTHONWARNINGS": "ignore"} | |
| try: | |
| completed = subprocess.run( | |
| [sys.executable, str(runner_path)], | |
| input=payload, | |
| text=True, | |
| capture_output=True, | |
| cwd=ROOT_DIR, | |
| timeout=FLOW_FREE_TIMEOUT_SECONDS, | |
| check=False, | |
| env=env, | |
| ) | |
| except subprocess.TimeoutExpired: | |
| result["error"] = "Flow_free verification timed out." | |
| return result | |
| if completed.returncode == 0 and completed.stdout.strip(): | |
| try: | |
| parsed = json.loads(completed.stdout) | |
| if isinstance(parsed, dict): | |
| return parsed | |
| except json.JSONDecodeError: | |
| pass | |
| result["error"] = "Flow_free verification failed." | |
| if completed.stderr.strip(): | |
| result["stderr"] = completed.stderr.strip()[:500] | |
| return result | |
| def verify( | |
| self, | |
| *, | |
| puzzle_type: str, | |
| problem_ascii: str, | |
| board_ascii: str, | |
| args: str, | |
| ) -> dict[str, Any]: | |
| spec = PUZZLES[puzzle_type] | |
| if spec.verifier_type == "flow_free": | |
| return self._verify_flow_free_with_subprocess( | |
| problem_ascii=problem_ascii, board_ascii=board_ascii | |
| ) | |
| if spec.verifier_type in SUBPROCESS_RLP_PUZZLES: | |
| return self._verify_rlp_with_subprocess( | |
| puzzle_type=spec.verifier_type, | |
| problem_ascii=problem_ascii, | |
| board_ascii=board_ascii, | |
| args=args, | |
| ) | |
| return verify_with_rlp( | |
| puzzle_type=spec.verifier_type, | |
| problem_ascii=problem_ascii, | |
| extracted_board=board_ascii, | |
| args=args, | |
| ) | |