"""YakPanel - Node.js / PM2 API""" import json import re import time from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from app.core.utils import exec_shell_sync from app.api.auth import get_current_user from app.models.user import User router = APIRouter(prefix="/node", tags=["node"]) class AddProcessRequest(BaseModel): script: str name: str = "" @router.get("/processes") async def node_processes(current_user: User = Depends(get_current_user)): """List PM2 processes (pm2 jlist)""" out, err = exec_shell_sync("pm2 jlist", timeout=10) if err and "not found" in err.lower(): return {"processes": [], "error": "PM2 not installed"} try: data = json.loads(out) if out.strip() else [] processes = [] now_ms = int(time.time() * 1000) for p in data if isinstance(data, list) else []: name = p.get("name", "") pm2_env = p.get("pm2_env", {}) start_ms = pm2_env.get("pm_uptime", 0) uptime_ms = (now_ms - start_ms) if start_ms and pm2_env.get("status") == "online" else 0 processes.append({ "id": p.get("pm_id"), "name": name, "status": pm2_env.get("status", "unknown"), "pid": p.get("pid"), "uptime": uptime_ms, "restarts": pm2_env.get("restart_time", 0), "memory": p.get("monit", {}).get("memory", 0), "cpu": p.get("monit", {}).get("cpu", 0), }) return {"processes": processes} except json.JSONDecodeError: return {"processes": [], "error": "Failed to parse PM2 output"} @router.post("/add") async def node_add( body: AddProcessRequest, current_user: User = Depends(get_current_user), ): """Add and start a new PM2 process (pm2 start script --name name)""" script = (body.script or "").strip() if not script: raise HTTPException(status_code=400, detail="Script path required") if ".." in script or ";" in script or "|" in script or "`" in script: raise HTTPException(status_code=400, detail="Invalid script path") name = (body.name or "").strip() if name and ("'" in name or '"' in name or ";" in name): raise HTTPException(status_code=400, detail="Invalid process name") cmd = f"pm2 start {script}" if name: cmd += f" --name '{name}'" out, err = exec_shell_sync(cmd, timeout=15) if err and "error" in err.lower(): raise HTTPException(status_code=500, detail=err.strip() or out.strip()) return {"status": True, "msg": "Process started"} @router.get("/version") async def node_version(current_user: User = Depends(get_current_user)): """Get Node.js version""" out, err = exec_shell_sync("node -v", timeout=5) version = out.strip() if out else "" if err and "not found" in err.lower(): return {"version": None, "error": "Node.js not installed"} return {"version": version or None} @router.post("/{proc_id}/start") async def node_start( proc_id: str, current_user: User = Depends(get_current_user), ): """Start PM2 process""" if not re.match(r"^\d+$", proc_id): raise HTTPException(status_code=400, detail="Invalid process ID") out, err = exec_shell_sync(f"pm2 start {proc_id}", timeout=15) if err and "error" in err.lower(): raise HTTPException(status_code=500, detail=err.strip() or out.strip()) return {"status": True, "msg": "Started"} @router.post("/{proc_id}/stop") async def node_stop( proc_id: str, current_user: User = Depends(get_current_user), ): """Stop PM2 process""" if not re.match(r"^\d+$", proc_id): raise HTTPException(status_code=400, detail="Invalid process ID") out, err = exec_shell_sync(f"pm2 stop {proc_id}", timeout=15) if err and "error" in err.lower(): raise HTTPException(status_code=500, detail=err.strip() or out.strip()) return {"status": True, "msg": "Stopped"} @router.post("/{proc_id}/restart") async def node_restart( proc_id: str, current_user: User = Depends(get_current_user), ): """Restart PM2 process""" if not re.match(r"^\d+$", proc_id): raise HTTPException(status_code=400, detail="Invalid process ID") out, err = exec_shell_sync(f"pm2 restart {proc_id}", timeout=15) if err and "error" in err.lower(): raise HTTPException(status_code=500, detail=err.strip() or out.strip()) return {"status": True, "msg": "Restarted"} @router.delete("/{proc_id}") async def node_delete( proc_id: str, current_user: User = Depends(get_current_user), ): """Delete PM2 process""" if not re.match(r"^\d+$", proc_id): raise HTTPException(status_code=400, detail="Invalid process ID") out, err = exec_shell_sync(f"pm2 delete {proc_id}", timeout=15) if err and "error" in err.lower(): raise HTTPException(status_code=500, detail=err.strip() or out.strip()) return {"status": True, "msg": "Deleted"}