From e0f432bc39b559ea364f2adfc60355e22ec0f944 Mon Sep 17 00:00:00 2001 From: "Gan, Jimmy" Date: Wed, 11 Mar 2026 23:55:51 +0800 Subject: [PATCH] fix: add pong/timeout detection to prevent silent connection hangs - Backend now responds to __ping__ with __pong__ - Frontend detects missing pong within 5s and closes connection - This triggers proper reconnect/auth recovery instead of hanging - Fixes 'keepalive ping timeout' issue where connection appears alive but is dead --- dashboard/backend/routers/terminal.py | 1 + dashboard/frontend/src/routes/Terminal.svelte | 26 ++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/dashboard/backend/routers/terminal.py b/dashboard/backend/routers/terminal.py index 8750a02..90ea7b6 100644 --- a/dashboard/backend/routers/terminal.py +++ b/dashboard/backend/routers/terminal.py @@ -180,6 +180,7 @@ async def ws_endpoint(websocket: WebSocket, host: str = Query("nas"), token: str process.stdin.write(data) elif "text" in msg and msg["text"]: if msg["text"] == "__ping__": + await websocket.send_text("__pong__") continue process.stdin.write(msg["text"].encode()) except Exception as e: diff --git a/dashboard/frontend/src/routes/Terminal.svelte b/dashboard/frontend/src/routes/Terminal.svelte index 273147c..d297558 100644 --- a/dashboard/frontend/src/routes/Terminal.svelte +++ b/dashboard/frontend/src/routes/Terminal.svelte @@ -190,18 +190,24 @@ const proto = location.protocol === "https:" ? "wss:" : "ws:"; let heartbeat = null; + let pongTimeout = null; let reconnectTimer = null; let closedManually = false; let reconnecting = false; let authRecoveryInFlight = false; let reconnectAttempts = 0; const MAX_RECONNECT_DELAY = 30000; + const PONG_TIMEOUT = 5000; // 5s to receive pong function clearHeartbeat() { if (heartbeat) { clearInterval(heartbeat); heartbeat = null; } + if (pongTimeout) { + clearTimeout(pongTimeout); + pongTimeout = null; + } } function handleAuthExpired(reason = "Session expired — please log in again") { @@ -280,7 +286,15 @@ sendSize(ws); clearHeartbeat(); heartbeat = setInterval(() => { - if (ws.readyState === 1) ws.send("__ping__"); + if (ws.readyState === 1) { + ws.send("__ping__"); + // Set timeout to detect missing pong + pongTimeout = setTimeout(() => { + if (ws.readyState === 1) { + ws.close(1000, "keepalive ping timeout"); + } + }, PONG_TIMEOUT); + } }, 12000); const d = tabData.get(tabId); if (d) { @@ -293,6 +307,16 @@ } }; ws.onmessage = (e) => { + // Check for text pong response + if (typeof e.data === 'string' && e.data === '__pong__') { + // Clear the pong timeout - connection is alive + if (pongTimeout) { + clearTimeout(pongTimeout); + pongTimeout = null; + } + return; + } + const data = new Uint8Array(e.data); const isPersistenceMessage = tab.persistent && data.length >= PERSISTENCE_STATUS_PREFIX_BYTES.length