atlus/backend/routers/processes.py
roberts f9743bb29a Initial commit — Atlus web desktop environment for SBCs
Full-stack implementation: FastAPI backend with PAM auth, WebSocket
stats/terminal, and vanilla JS frontend with tiling desktop shell.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 16:53:46 -05:00

89 lines
3 KiB
Python

"""Process list and signal management."""
import os
import signal
import psutil
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from backend.auth import get_current_user
router = APIRouter(prefix="/api/processes", tags=["processes"])
class SignalRequest(BaseModel):
pid: int
signal: str = "SIGTERM" # SIGTERM, SIGKILL, SIGSTOP, SIGCONT, SIGHUP
@router.get("")
async def list_processes(_user: str = Depends(get_current_user)):
"""List all running processes."""
procs = []
for p in psutil.process_iter(["pid", "name", "username", "cpu_percent",
"memory_percent", "status", "create_time",
"cmdline"]):
try:
info = p.info
procs.append({
"pid": info["pid"],
"name": info["name"],
"user": info["username"],
"cpu": info["cpu_percent"],
"mem": round(info["memory_percent"], 1) if info["memory_percent"] else 0,
"status": info["status"],
"started": info["create_time"],
"cmdline": " ".join(info["cmdline"][:5]) if info["cmdline"] else "",
})
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
return procs
@router.get("/{pid}")
async def process_detail(pid: int, _user: str = Depends(get_current_user)):
try:
p = psutil.Process(pid)
with p.oneshot():
return {
"pid": p.pid,
"name": p.name(),
"user": p.username(),
"cpu": p.cpu_percent(),
"mem": round(p.memory_percent(), 1),
"status": p.status(),
"started": p.create_time(),
"cmdline": p.cmdline(),
"cwd": p.cwd(),
"num_threads": p.num_threads(),
"open_files": len(p.open_files()),
"connections": len(p.net_connections()),
"memory_info": {
"rss": p.memory_info().rss,
"vms": p.memory_info().vms,
},
}
except psutil.NoSuchProcess:
raise HTTPException(404, f"Process {pid} not found")
except psutil.AccessDenied:
raise HTTPException(403, f"Access denied to process {pid}")
@router.post("/signal")
async def send_signal(req: SignalRequest, _user: str = Depends(get_current_user)):
sig_name = req.signal.upper()
if not sig_name.startswith("SIG"):
sig_name = "SIG" + sig_name
try:
sig = getattr(signal, sig_name)
except AttributeError:
raise HTTPException(400, f"Unknown signal: {req.signal}")
try:
os.kill(req.pid, sig)
except ProcessLookupError:
raise HTTPException(404, f"Process {req.pid} not found")
except PermissionError:
raise HTTPException(403, f"Permission denied for pid {req.pid}")
return {"pid": req.pid, "signal": sig_name, "sent": True}