Files
workinf_Blender_Wasm/my-tools/wbv2_runtime.py
2026-09-02 18:35:58 +08:00

105 lines
3.4 KiB
Python
Executable File

#!/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