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
|
import logging
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, Request, Query
|
||||||
from pydantic import BaseModel
|
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
|
from backend.sessions import manager
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/session", tags=["session"])
|
router = APIRouter(prefix="/api/session", tags=["session"])
|
||||||
|
|
@ -41,6 +41,8 @@ async def get_session(user: str = Depends(get_current_user)):
|
||||||
@router.put("/state")
|
@router.put("/state")
|
||||||
async def save_state(
|
async def save_state(
|
||||||
state: DesktopState,
|
state: DesktopState,
|
||||||
|
request: Request,
|
||||||
|
token: str = Query(default=None),
|
||||||
user: str = Depends(get_current_user),
|
user: str = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""Save desktop state (open apps, active app, terminal tab info)."""
|
"""Save desktop state (open apps, active app, terminal tab info)."""
|
||||||
|
|
@ -48,6 +50,25 @@ async def save_state(
|
||||||
return {"ok": True}
|
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
|
# Terminal endpoints
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -64,7 +85,11 @@ async def create_terminal(
|
||||||
user: str = Depends(get_current_user),
|
user: str = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""Create a new persistent terminal."""
|
"""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()
|
return terminal.to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class ManagedTerminal:
|
||||||
|
|
||||||
async def _read_loop(self):
|
async def _read_loop(self):
|
||||||
"""Background task: read PTY output → scrollback + attached WebSockets."""
|
"""Background task: read PTY output → scrollback + attached WebSockets."""
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_running_loop()
|
||||||
while self.alive:
|
while self.alive:
|
||||||
try:
|
try:
|
||||||
raw = await loop.run_in_executor(None, lambda: self.pty.read(4096))
|
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",
|
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||||
}
|
}
|
||||||
|
|
||||||
pty = PtyProcess.spawn(
|
log.info("Spawning PTY for %s: shell=%s home=%s", username, shell, home)
|
||||||
[shell, "-l"],
|
|
||||||
dimensions=(rows, cols),
|
try:
|
||||||
env=env,
|
pty = PtyProcess.spawn(
|
||||||
cwd=home,
|
[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_id = str(uuid.uuid4())[:8]
|
||||||
terminal = ManagedTerminal(
|
terminal = ManagedTerminal(
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,11 @@
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { cols: 120, rows: 30 },
|
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();
|
const data = await res.json();
|
||||||
return attachToTerminal(data.terminal_id, data.title);
|
return attachToTerminal(data.terminal_id, data.title);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -522,9 +522,18 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save state before unload
|
// Save state before unload (use sendBeacon for reliability)
|
||||||
window.addEventListener('beforeunload', () => {
|
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