97 lines
3.5 KiB
Python
Executable File
97 lines
3.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Run the repository's Blender/WBV2 unattended scheduler.
|
|
|
|
It is the project-local wrapper for copied launcher scripts. It does not start
|
|
an interactive agent, use a second ledger, or emulate
|
|
Blender behavior. The authoritative Node scheduler owns queue validation,
|
|
guardian/supervisor execution, evidence handoff, commit and push rules.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import signal
|
|
import subprocess
|
|
from datetime import datetime, timezone
|
|
|
|
from wbv2_runtime import ROOT, scheduler_args
|
|
|
|
|
|
STATUS_PATH = ROOT / "tests/golden/WBV2/my-tools-scheduler.json"
|
|
PID_PATH = ROOT / "tests/golden/WBV2/my-tools-scheduler.pid"
|
|
|
|
|
|
def write_status(**values: object) -> None:
|
|
STATUS_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
payload = {"schemaVersion": 1, "updatedAt": datetime.now(timezone.utc).isoformat(), **values}
|
|
temporary = STATUS_PATH.with_suffix(f".tmp-{os.getpid()}")
|
|
temporary.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
temporary.replace(STATUS_PATH)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--max-iterations", "--max-cycles", type=int, default=0)
|
|
parser.add_argument("--poll-ms", type=int, default=1000)
|
|
parser.add_argument("--push-retries", type=int, default=6)
|
|
parser.add_argument(
|
|
"--commit",
|
|
action="store_true",
|
|
default=os.getenv("WBV2_AUTO_COMMIT", "1") != "0",
|
|
help="commit the exact handoff files after a successful task (default: on)",
|
|
)
|
|
parser.add_argument(
|
|
"--push",
|
|
action="store_true",
|
|
default=os.getenv("WBV2_AUTO_PUSH", "1") != "0",
|
|
help="push and verify origin/main after committing (default: on)",
|
|
)
|
|
parser.add_argument("--preflight-only", action="store_true")
|
|
args = parser.parse_args()
|
|
if args.max_iterations < 0 or args.poll_ms < 0 or args.push_retries < 0:
|
|
parser.error("iteration, poll and retry values must be non-negative")
|
|
if args.push and not args.commit:
|
|
parser.error("--push requires --commit")
|
|
|
|
command = scheduler_args(
|
|
commit=args.commit,
|
|
push=args.push,
|
|
max_iterations=args.max_iterations,
|
|
preflight_only=args.preflight_only,
|
|
)
|
|
command.extend(["--poll-ms", str(args.poll_ms), "--push-retries", str(args.push_retries)])
|
|
print("WBV2 scheduler:", " ".join(command), flush=True)
|
|
PID_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
if PID_PATH.exists():
|
|
try:
|
|
existing = int(PID_PATH.read_text(encoding="ascii").strip())
|
|
os.kill(existing, 0)
|
|
except (OSError, ValueError):
|
|
PID_PATH.unlink(missing_ok=True)
|
|
else:
|
|
parser.error(f"scheduler wrapper already running: pid={existing}")
|
|
PID_PATH.write_text(f"{os.getpid()}\n", encoding="ascii")
|
|
write_status(status="starting", pid=os.getpid(), command=command)
|
|
process = subprocess.Popen(command, cwd=ROOT, start_new_session=True)
|
|
|
|
def forward(signum: int, _frame: object) -> None:
|
|
try:
|
|
os.killpg(process.pid, signum)
|
|
except ProcessLookupError:
|
|
pass
|
|
|
|
signal.signal(signal.SIGINT, forward)
|
|
signal.signal(signal.SIGTERM, forward)
|
|
try:
|
|
code = process.wait()
|
|
write_status(status="complete" if code == 0 else "blocked", pid=os.getpid(), childPid=process.pid, exitCode=code)
|
|
return code
|
|
finally:
|
|
PID_PATH.unlink(missing_ok=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|