from __future__ import annotations import os import subprocess import sys from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel from app.core.config import ROOT_DIR from app.core.security import get_current_user from app.models.user import User from app.services.system_control import ( build_task_id, clear_active_task_id, get_active_task_id, get_allowed_command, get_runner_script_path, is_task_stale, get_task_logs, require_super_admin, serialize_task, set_active_task_id, upsert_task_state, ) router = APIRouter() class RestartTaskCreate(BaseModel): action: str class RestartTaskResponse(BaseModel): task_id: str action: str status: str stage: str message: str created_at: str updated_at: str requested_by: dict[str, object] | None = None class RestartTaskLogsResponse(BaseModel): task_id: str lines: list[str] def ensure_super_admin(current_user: User) -> None: if not require_super_admin(current_user.role): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Only super_admin can restart services", ) @router.post("/restart-tasks", response_model=RestartTaskResponse) async def create_restart_task( payload: RestartTaskCreate, current_user: User = Depends(get_current_user), ): ensure_super_admin(current_user) command = get_allowed_command(payload.action) if command is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Unsupported system action", ) active_task_id = get_active_task_id() if active_task_id: active_task = serialize_task(active_task_id) if active_task and is_task_stale(active_task): upsert_task_state( active_task_id, status="failed", stage="failed", message="Previous restart task became stale and was released", ) clear_active_task_id(active_task_id) elif active_task and active_task.get("status") in {"queued", "running"}: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Another restart task is already in progress", ) task_id = build_task_id() requested_by = {"id": current_user.id, "username": current_user.username} task_state = upsert_task_state( task_id, action=payload.action, status="queued", stage="accepted", message="Restart task accepted", requested_by=requested_by, ) set_active_task_id(task_id) env = os.environ.copy() backend_path = str(ROOT_DIR / "backend") existing_pythonpath = env.get("PYTHONPATH", "") env["PYTHONPATH"] = ( f"{backend_path}{os.pathsep}{existing_pythonpath}" if existing_pythonpath else backend_path ) try: subprocess.Popen( [ sys.executable, str(get_runner_script_path()), "--task-id", task_id, "--action", payload.action, ], cwd=str(ROOT_DIR), env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True, ) except OSError as exc: task_state = upsert_task_state( task_id, action=payload.action, status="failed", stage="failed", message=f"Unable to start restart runner: {exc}", requested_by=requested_by, ) clear_active_task_id(task_id) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=task_state["message"], ) from exc return task_state @router.get("/restart-tasks/{task_id}", response_model=RestartTaskResponse) async def get_restart_task( task_id: str, current_user: User = Depends(get_current_user), ): ensure_super_admin(current_user) task = serialize_task(task_id) if task is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Restart task not found") return task @router.get("/restart-tasks/{task_id}/logs", response_model=RestartTaskLogsResponse) async def get_restart_task_logs( task_id: str, current_user: User = Depends(get_current_user), ): ensure_super_admin(current_user) task = serialize_task(task_id) if task is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Restart task not found") return {"task_id": task_id, "lines": get_task_logs(task_id)}