105 lines
3.4 KiB
Python
Executable File
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
|