from dataclasses import dataclass, field from datetime import UTC, datetime from sqlalchemy import func, select from sqlalchemy.orm import Session from l4d2web.db import session_scope from l4d2web.models import Job, JobLog, Server @dataclass class SchedulerState: install_running: bool = False running_servers: set[int] = field(default_factory=set) def can_start(job, state: SchedulerState) -> bool: if job.operation == "install": return (not state.install_running) and (len(state.running_servers) == 0) if state.install_running: return False if job.server_id is None: return False return job.server_id not in state.running_servers def recover_stale_jobs() -> int: now = datetime.now(UTC) with session_scope() as db: jobs = db.scalars(select(Job).where(Job.state == "running")).all() for job in jobs: job.state = "failed" job.finished_at = now job.updated_at = now return len(jobs) def append_job_log( session: Session, job_id: int, stream: str, line: str, max_chars: int = 4096, ) -> int: last_seq = session.scalar(select(func.max(JobLog.seq)).where(JobLog.job_id == job_id)) or 0 next_seq = int(last_seq) + 1 session.add(JobLog(job_id=job_id, seq=next_seq, stream=stream, line=line[:max_chars])) session.flush() return next_seq def refresh_server_actual_state(server_id: int) -> str: from l4d2web.services import l4d2_facade now = datetime.now(UTC) with session_scope() as db: server = db.scalar(select(Server).where(Server.id == server_id)) if server is None: return "unknown" status = l4d2_facade.server_status(server.name) server.actual_state = status.state server.actual_state_updated_at = now server.updated_at = now return server.actual_state