#!/usr/bin/env python3 """Small, read-only helpers shared by the Blender Web unattended tools.""" from __future__ import annotations import json import os import re import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[1] CONTROL_ROOT = ROOT / "tests/golden/WBV2" QUEUE = ROOT / "docs/EXECUTION_QUEUE.md" INDEX = CONTROL_ROOT / "task-index.json" SCHEDULER = ROOT / "tools/web/run-wbv2-scheduler.mjs" LIVE_STATUS = ROOT / "tools/web/print-wbv2-live-status.mjs" SCHEDULER_LOCK = CONTROL_ROOT / "continuous-scheduler.lock" SCHEDULER_CHECKPOINT = CONTROL_ROOT / "continuous-scheduler.json" GUARDIAN_LOCK = CONTROL_ROOT / "continuous-guardian.lock" SUPERVISOR_LOCK = CONTROL_ROOT / "continuous-supervisor.lock" def read_json(path: Path) -> dict: try: value = json.loads(path.read_text(encoding="utf-8")) except (FileNotFoundError, json.JSONDecodeError): return {} return value if isinstance(value, dict) else {} def queue_pointer() -> dict[str, str | None]: """Read the authoritative queue pointer without inferring task order.""" text = QUEUE.read_text(encoding="utf-8") task_match = re.search(r"\| 当前任务 \| `([^`]+)` \|", text) parent_match = re.search(r"\| parent manifest \| `([^`]+)` \|", text) return { "task": task_match.group(1) if task_match else None, "parentManifest": parent_match.group(1) if parent_match else None, } def process_alive(pid: object) -> bool: if not isinstance(pid, int) or pid <= 0: return False try: os.kill(pid, 0) except ProcessLookupError: return False except PermissionError: return True return True def lock_owner(path: Path) -> dict: value = read_json(path) pid = value.get("pid") return { "path": str(path.relative_to(ROOT)), "pid": pid if isinstance(pid, int) else None, "startedAt": value.get("startedAt"), "alive": process_alive(pid), } def terminal_pointer(pointer: dict[str, str | None] | None = None) -> bool: pointer = pointer or queue_pointer() task = pointer.get("task") if not task: # A missing queue pointer is malformed/unknown, not proof of terminal # completion. Let the production scheduler record the precise failure. return False index = read_json(INDEX) manifest_path = CONTROL_ROOT.parent / task / "manifest.json" manifest = read_json(manifest_path) return ( index.get("activeTask") is None and manifest.get("task") == task and manifest.get("status") == "done" and manifest.get("nextTask") is None ) def live_status(*, json_only: bool = True, watch: bool = False, interval_ms: int = 2000) -> subprocess.CompletedProcess[str]: args = ["node", str(LIVE_STATUS.relative_to(ROOT))] if json_only: args.append("--json") if watch: args.extend(["--watch", "--interval-ms", str(interval_ms)]) return subprocess.run(args, cwd=ROOT, text=True, capture_output=True, check=False) def scheduler_args(*, commit: bool, push: bool, max_iterations: int = 0, preflight_only: bool = False) -> list[str]: if push and not commit: raise ValueError("push requires commit") args = ["node", str(SCHEDULER.relative_to(ROOT)), "--max-iterations", str(max_iterations)] if commit: args.append("--commit") if push: args.append("--push") if preflight_only: args.append("--preflight-only") return args