mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
fix: WS session lifecycle, reconnection, and CJK session-switch cleanup
- Close WebSocket when session exits (exit event listener) to prevent orphaned listeners and stale writes to dead PTY - Add readyState guard in onTerminal to stop buffering after socket closes - Simplify heartbeat: remove redundant alive flag, use pongTimeout only - Add exponential backoff reconnection on unexpected WS close (skip for server rejections 4004/4008/4009) - Clear CJK textarea on session switch to prevent wrong-session input Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+28
-5
@@ -2904,6 +2904,7 @@ class CodemanApp {
|
|||||||
// Only mark ready if this is still the intended session
|
// Only mark ready if this is still the intended session
|
||||||
if (this._ws === ws) {
|
if (this._ws === ws) {
|
||||||
this._wsReady = true;
|
this._wsReady = true;
|
||||||
|
this._wsReconnectAttempts = 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2924,11 +2925,24 @@ class CodemanApp {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = (event) => {
|
||||||
if (this._ws === ws) {
|
if (this._ws !== ws) return;
|
||||||
this._ws = null;
|
this._ws = null;
|
||||||
this._wsSessionId = null;
|
this._wsSessionId = null;
|
||||||
this._wsReady = false;
|
this._wsReady = false;
|
||||||
|
|
||||||
|
// Reconnect on unexpected close (server restart, network blip, ping timeout).
|
||||||
|
// Don't reconnect if we intentionally disconnected (_disconnectWs nulls onclose)
|
||||||
|
// or if the server rejected the session (4004=not found, 4008=too many, 4009=terminated).
|
||||||
|
if (event.code < 4004 && this.activeSessionId === sessionId) {
|
||||||
|
const delay = Math.min(1000 * Math.pow(2, this._wsReconnectAttempts || 0), 10000);
|
||||||
|
this._wsReconnectAttempts = (this._wsReconnectAttempts || 0) + 1;
|
||||||
|
this._wsReconnectTimer = setTimeout(() => {
|
||||||
|
this._wsReconnectTimer = null;
|
||||||
|
if (this.activeSessionId === sessionId) {
|
||||||
|
this._connectWs(sessionId);
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2939,6 +2953,11 @@ class CodemanApp {
|
|||||||
|
|
||||||
/** Close the active WebSocket connection (if any). */
|
/** Close the active WebSocket connection (if any). */
|
||||||
_disconnectWs() {
|
_disconnectWs() {
|
||||||
|
if (this._wsReconnectTimer) {
|
||||||
|
clearTimeout(this._wsReconnectTimer);
|
||||||
|
this._wsReconnectTimer = null;
|
||||||
|
}
|
||||||
|
this._wsReconnectAttempts = 0;
|
||||||
if (this._ws) {
|
if (this._ws) {
|
||||||
this._ws.onclose = null; // Prevent re-entrant cleanup
|
this._ws.onclose = null; // Prevent re-entrant cleanup
|
||||||
this._ws.close();
|
this._ws.close();
|
||||||
@@ -3763,6 +3782,10 @@ class CodemanApp {
|
|||||||
// Close WebSocket for previous session (new one opens after buffer load)
|
// Close WebSocket for previous session (new one opens after buffer load)
|
||||||
this._disconnectWs();
|
this._disconnectWs();
|
||||||
|
|
||||||
|
// Clear CJK textarea to prevent sending stale text to the wrong session
|
||||||
|
const cjkEl = document.getElementById('cjkInput');
|
||||||
|
if (cjkEl) cjkEl.value = '';
|
||||||
|
|
||||||
// Clean up flicker filter state when switching sessions
|
// Clean up flicker filter state when switching sessions
|
||||||
if (this.flickerFilterTimeout) {
|
if (this.flickerFilterTimeout) {
|
||||||
clearTimeout(this.flickerFilterTimeout);
|
clearTimeout(this.flickerFilterTimeout);
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ export function registerWsRoutes(app: FastifyInstance, ctx: SessionPort): void {
|
|||||||
|
|
||||||
// Terminal output -> micro-batched WS send
|
// Terminal output -> micro-batched WS send
|
||||||
const onTerminal = (data: string) => {
|
const onTerminal = (data: string) => {
|
||||||
|
if (socket.readyState !== 1) return;
|
||||||
batchChunks.push(data);
|
batchChunks.push(data);
|
||||||
batchSize += data.length;
|
batchSize += data.length;
|
||||||
|
|
||||||
@@ -153,17 +154,22 @@ export function registerWsRoutes(app: FastifyInstance, ctx: SessionPort): void {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Close WS when session exits (deleted, respawned, or crashed) — prevents
|
||||||
|
// orphaned listeners and stale writes to a dead PTY.
|
||||||
|
const onSessionExit = () => {
|
||||||
|
socket.close(4009, 'Session terminated');
|
||||||
|
};
|
||||||
|
|
||||||
session.on('terminal', onTerminal);
|
session.on('terminal', onTerminal);
|
||||||
session.on('clearTerminal', onClearTerminal);
|
session.on('clearTerminal', onClearTerminal);
|
||||||
session.on('needsRefresh', onNeedsRefresh);
|
session.on('needsRefresh', onNeedsRefresh);
|
||||||
|
session.on('exit', onSessionExit);
|
||||||
|
|
||||||
// Heartbeat: detect stale connections (especially through tunnels where
|
// Heartbeat: detect stale connections (especially through tunnels where
|
||||||
// TCP RST can take minutes to propagate).
|
// TCP RST can take minutes to propagate).
|
||||||
let pongTimeout: ReturnType<typeof setTimeout> | null = null;
|
let pongTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
let alive = true;
|
|
||||||
|
|
||||||
socket.on('pong', () => {
|
socket.on('pong', () => {
|
||||||
alive = true;
|
|
||||||
if (pongTimeout) {
|
if (pongTimeout) {
|
||||||
clearTimeout(pongTimeout);
|
clearTimeout(pongTimeout);
|
||||||
pongTimeout = null;
|
pongTimeout = null;
|
||||||
@@ -171,12 +177,7 @@ export function registerWsRoutes(app: FastifyInstance, ctx: SessionPort): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const pingInterval = setInterval(() => {
|
const pingInterval = setInterval(() => {
|
||||||
if (!alive) {
|
if (socket.readyState !== 1) return;
|
||||||
// Previous ping never got a pong — connection is dead
|
|
||||||
socket.terminate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
alive = false;
|
|
||||||
socket.ping();
|
socket.ping();
|
||||||
pongTimeout = setTimeout(() => {
|
pongTimeout = setTimeout(() => {
|
||||||
socket.terminate();
|
socket.terminate();
|
||||||
@@ -191,6 +192,7 @@ export function registerWsRoutes(app: FastifyInstance, ctx: SessionPort): void {
|
|||||||
session.off('terminal', onTerminal);
|
session.off('terminal', onTerminal);
|
||||||
session.off('clearTerminal', onClearTerminal);
|
session.off('clearTerminal', onClearTerminal);
|
||||||
session.off('needsRefresh', onNeedsRefresh);
|
session.off('needsRefresh', onNeedsRefresh);
|
||||||
|
session.off('exit', onSessionExit);
|
||||||
|
|
||||||
// Decrement per-session connection count
|
// Decrement per-session connection count
|
||||||
const count = sessionWsCount.get(id) ?? 1;
|
const count = sessionWsCount.get(id) ?? 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user