Fix terminal session: better error handling, reliable state save
- Fix asyncio.get_event_loop() → get_running_loop() in PTY reader - Add error logging for PTY spawn failures - Add POST /api/session/state endpoint for sendBeacon (beforeunload) - Use navigator.sendBeacon for reliable state save on page close - Improve frontend error reporting when terminal creation fails Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
220a5234cd
commit
68fe9b4435
4 changed files with 57 additions and 13 deletions
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.auth import get_current_user
|
||||
from backend.auth import get_current_user, decode_token
|
||||
from backend.sessions import manager
|
||||
|
||||
router = APIRouter(prefix="/api/session", tags=["session"])
|
||||
|
|
@ -41,6 +41,8 @@ async def get_session(user: str = Depends(get_current_user)):
|
|||
@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)."""
|
||||
|
|
@ -48,6 +50,25 @@ async def save_state(
|
|||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -64,7 +85,11 @@ async def create_terminal(
|
|||
user: str = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new persistent terminal."""
|
||||
terminal = manager.create_terminal(user, cols=req.cols, rows=req.rows)
|
||||
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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class ManagedTerminal:
|
|||
|
||||
async def _read_loop(self):
|
||||
"""Background task: read PTY output → scrollback + attached WebSockets."""
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
while self.alive:
|
||||
try:
|
||||
raw = await loop.run_in_executor(None, lambda: self.pty.read(4096))
|
||||
|
|
@ -237,12 +237,18 @@ class SessionManager:
|
|||
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
}
|
||||
|
||||
pty = PtyProcess.spawn(
|
||||
[shell, "-l"],
|
||||
dimensions=(rows, cols),
|
||||
env=env,
|
||||
cwd=home,
|
||||
)
|
||||
log.info("Spawning PTY for %s: shell=%s home=%s", username, shell, home)
|
||||
|
||||
try:
|
||||
pty = PtyProcess.spawn(
|
||||
[shell, "-l"],
|
||||
dimensions=(rows, cols),
|
||||
env=env,
|
||||
cwd=home,
|
||||
)
|
||||
except Exception:
|
||||
log.exception("Failed to spawn PTY for %s", username)
|
||||
raise
|
||||
|
||||
terminal_id = str(uuid.uuid4())[:8]
|
||||
terminal = ManagedTerminal(
|
||||
|
|
|
|||
|
|
@ -19,7 +19,11 @@
|
|||
method: 'POST',
|
||||
body: { cols: 120, rows: 30 },
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
if (!res || !res.ok) {
|
||||
const err = res ? await res.text().catch(() => 'unknown') : 'no response';
|
||||
console.error('Failed to create terminal:', err);
|
||||
return null;
|
||||
}
|
||||
const data = await res.json();
|
||||
return attachToTerminal(data.terminal_id, data.title);
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -522,9 +522,18 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Save state before unload
|
||||
// Save state before unload (use sendBeacon for reliability)
|
||||
window.addEventListener('beforeunload', () => {
|
||||
_doSaveState();
|
||||
const termApp = Atlus.apps.terminal;
|
||||
const terminalTabs = (termApp && termApp.getTerminalIds)
|
||||
? termApp.getTerminalIds() : [];
|
||||
const state = {
|
||||
open_apps: Atlus.openApps.slice(),
|
||||
active_app: Atlus.activeApp,
|
||||
terminal_tabs: terminalTabs,
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(state)], { type: 'application/json' });
|
||||
navigator.sendBeacon('/api/session/state?token=' + TOKEN, blob);
|
||||
});
|
||||
|
||||
// =====================================================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue