Atlus runs as root (systemd) but user-facing processes must run under the authenticated user's identity. Added privilege-dropping via preexec_fn (os.setgid + os.initgroups + os.setuid) to both terminal PTY spawning and GUI app launching. System admin operations (services, packages, network, updates) intentionally remain root. Autostart apps now support a configurable default_user; without one set, autostart defers until the first user logs in and runs as that user. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
107 lines
3.2 KiB
Python
107 lines
3.2 KiB
Python
"""Desktop session — persistent state + terminal management."""
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, Query
|
|
from pydantic import BaseModel
|
|
|
|
from backend.auth import get_current_user, decode_token
|
|
from backend.display import display_manager
|
|
from backend.sessions import manager
|
|
|
|
router = APIRouter(prefix="/api/session", tags=["session"])
|
|
log = logging.getLogger("atlus.session")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Models
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class DesktopState(BaseModel):
|
|
open_apps: list[str] = []
|
|
active_app: str | None = None
|
|
terminal_tabs: list[dict] = [] # [{terminal_id, title}]
|
|
|
|
|
|
class TerminalCreate(BaseModel):
|
|
cols: int = 120
|
|
rows: int = 30
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Session endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("")
|
|
async def get_session(user: str = Depends(get_current_user)):
|
|
"""Get or create the user's desktop session."""
|
|
session = manager.get_or_create(user)
|
|
# Trigger deferred autostart apps on first login
|
|
await display_manager.trigger_deferred_autostart(user)
|
|
return session.to_dict()
|
|
|
|
|
|
@router.put("/state")
|
|
async def save_state(
|
|
state: DesktopState,
|
|
request: Request,
|
|
token: str = Query(default=None),
|
|
user: str = Depends(get_current_user),
|
|
):
|
|
"""Save desktop state (open apps, active app, terminal tab info)."""
|
|
manager.save_state(user, state.model_dump())
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/state")
|
|
async def save_state_beacon(
|
|
request: Request,
|
|
token: str = Query(default=None),
|
|
):
|
|
"""Save desktop state via sendBeacon (POST with token in query param)."""
|
|
if not token:
|
|
raise HTTPException(401, "Missing token")
|
|
try:
|
|
payload = decode_token(token)
|
|
except Exception:
|
|
raise HTTPException(401, "Invalid token")
|
|
user = payload["sub"]
|
|
body = await request.json()
|
|
state = DesktopState(**body)
|
|
manager.save_state(user, state.model_dump())
|
|
return {"ok": True}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Terminal endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/terminals")
|
|
async def list_terminals(user: str = Depends(get_current_user)):
|
|
"""List all alive terminals for the user."""
|
|
return manager.list_terminals(user)
|
|
|
|
|
|
@router.post("/terminals")
|
|
async def create_terminal(
|
|
req: TerminalCreate,
|
|
user: str = Depends(get_current_user),
|
|
):
|
|
"""Create a new persistent terminal."""
|
|
try:
|
|
terminal = manager.create_terminal(user, cols=req.cols, rows=req.rows)
|
|
except Exception as e:
|
|
log.exception("Failed to create terminal for %s", user)
|
|
raise HTTPException(500, f"Failed to spawn terminal: {e}")
|
|
return terminal.to_dict()
|
|
|
|
|
|
@router.delete("/terminals/{terminal_id}")
|
|
async def close_terminal(
|
|
terminal_id: str,
|
|
user: str = Depends(get_current_user),
|
|
):
|
|
"""Close and kill a terminal."""
|
|
if not manager.close_terminal(user, terminal_id):
|
|
raise HTTPException(404, "Terminal not found")
|
|
return {"ok": True}
|