mirror of
https://github.com/blw1138/cross-py-builder.git
synced 2026-09-07 21:41:09 -05:00
- Add ctrl/ package: Flask app (waitress), SQLite job store, background scheduler that probes configured workers, dispatches via /upload and /checkout_git, fetches artifacts, and serves a single-page UI - Supporting API: /api/jobs (create/list/detail/cancel), /api/workers, /api/capabilities, artifact download, and SSE log stream - Workers keep the existing HTTP API; add /progress/<job_id> endpoint and per-step build tracking for live status - Retire agent_manager.py and the cross-py-builder CLI; controller UI is the primary frontend - Record design in DESIGN.md; add waitress dependency Note: /progress is exposed on the worker but the synchronous /upload and /checkout endpoints block until a build completes, so per-step worker progress is not yet relayed live in the UI. Live streaming needs an async-start worker endpoint as a follow-up.
113 lines
2.8 KiB
Python
113 lines
2.8 KiB
Python
import json
|
|
import sqlite3
|
|
import threading
|
|
|
|
import ctrl.settings as settings
|
|
|
|
|
|
_local = threading.local()
|
|
|
|
|
|
def _conn():
|
|
conn = getattr(_local, "conn", None)
|
|
if conn is None:
|
|
conn = sqlite3.connect(settings.DB_PATH, timeout=30)
|
|
conn.row_factory = sqlite3.Row
|
|
_local.conn = conn
|
|
return conn
|
|
|
|
|
|
def init_db():
|
|
settings.ensure_dirs()
|
|
conn = _conn()
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS jobs (
|
|
id TEXT PRIMARY KEY,
|
|
status TEXT NOT NULL,
|
|
source_type TEXT NOT NULL,
|
|
source TEXT,
|
|
os_req TEXT,
|
|
cpu_req TEXT,
|
|
worker_host TEXT,
|
|
worker_job_id TEXT,
|
|
worker_url TEXT,
|
|
created_at TEXT NOT NULL,
|
|
started_at TEXT,
|
|
finished_at TEXT,
|
|
log TEXT DEFAULT '',
|
|
error TEXT,
|
|
artifacts TEXT DEFAULT '[]'
|
|
)
|
|
"""
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
def create_job(source_type, source, os_req, cpu_req):
|
|
import uuid
|
|
import datetime
|
|
job_id = "JOB-" + uuid.uuid4().hex[:8]
|
|
conn = _conn()
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO jobs (id, status, source_type, source, os_req, cpu_req, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(job_id, "queued", source_type, source, os_req, cpu_req,
|
|
datetime.datetime.now().isoformat()),
|
|
)
|
|
conn.commit()
|
|
return job_id
|
|
|
|
|
|
def _row_to_dict(row):
|
|
d = dict(row)
|
|
try:
|
|
d["artifacts"] = json.loads(d.get("artifacts") or "[]")
|
|
except (TypeError, ValueError):
|
|
d["artifacts"] = []
|
|
return d
|
|
|
|
|
|
def get_job(job_id):
|
|
row = _conn().execute("SELECT * FROM jobs WHERE id = ?", (job_id,)).fetchone()
|
|
return _row_to_dict(row) if row else None
|
|
|
|
|
|
def list_jobs(limit=100):
|
|
rows = _conn().execute(
|
|
"SELECT * FROM jobs ORDER BY created_at DESC LIMIT ?", (limit,)
|
|
).fetchall()
|
|
return [_row_to_dict(r) for r in rows]
|
|
|
|
|
|
def update_job(job_id, **fields):
|
|
if not fields:
|
|
return
|
|
cols = ", ".join(f"{k} = ?" for k in fields)
|
|
values = []
|
|
for v in fields.values():
|
|
if isinstance(v, (list, dict)):
|
|
v = json.dumps(v)
|
|
values.append(v)
|
|
conn = _conn()
|
|
conn.execute(f"UPDATE jobs SET {cols} WHERE id = ?", (*values, job_id))
|
|
conn.commit()
|
|
|
|
|
|
def append_log(job_id, text):
|
|
conn = _conn()
|
|
conn.execute("UPDATE jobs SET log = log || ? WHERE id = ?", (text, job_id))
|
|
conn.commit()
|
|
|
|
|
|
def set_queued_jobs_for_worker(worker_url):
|
|
"""Reschedule any jobs currently stuck on a worker back to queued."""
|
|
conn = _conn()
|
|
conn.execute(
|
|
"UPDATE jobs SET status = 'queued', worker_url = NULL WHERE worker_url = ? AND status IN ('dispatching', 'building')",
|
|
(worker_url,),
|
|
)
|
|
conn.commit()
|