From c7c4cb45a7b9ff6d8c864b4000e55b2a48ab250d Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Thu, 28 May 2026 21:18:47 +0200 Subject: [PATCH] fix(terminal): verify container exists before creating terminal session The instance status may say 'running' but the actual Docker container may have been removed (e.g. docker prune, host restart). The old code created a terminal session which immediately died because docker exec failed with 'No such container'. - Add get_container_status check in WebSocket handler before session creation - Return 4004 with clear message if container is missing - This prevents spawning zombie terminal sessions --- apps/api/src/api/terminal.py | 15 +++++++++++++++ apps/web/src/components/terminal.tsx | 13 ++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/apps/api/src/api/terminal.py b/apps/api/src/api/terminal.py index 7f11773..a7d4daa 100644 --- a/apps/api/src/api/terminal.py +++ b/apps/api/src/api/terminal.py @@ -124,6 +124,21 @@ async def _handle_terminal_websocket( logger.debug("Terminal auth passed for instance %s, user %s", instance_id, user_id) + # Verify the container actually exists (may have been removed/recreated) + from src.services.docker import get_container_status + + container_status = get_container_status(instance.container_id) + if container_status["status"] == "not_found": + logger.error( + "Container %s for instance %s not found (may have been removed)", + instance.container_id, + instance_id, + ) + await websocket.close( + code=4004, reason="Container not found — restart the tool instance" + ) + return + # Fetch tool type to get startup_command tool_type = await db_session.get(ToolType, instance.tool_type_id) startup_command = tool_type.startup_command if tool_type else None diff --git a/apps/web/src/components/terminal.tsx b/apps/web/src/components/terminal.tsx index ab2a82b..73aee06 100644 --- a/apps/web/src/components/terminal.tsx +++ b/apps/web/src/components/terminal.tsx @@ -140,7 +140,9 @@ export const TerminalComponent = React.forwardRef( if (event.data instanceof Blob) { // eslint-disable-next-line no-console - console.log(`[Terminal ${sessionId ?? "default"}] received ${(event.data as Blob).size} bytes`); + console.log( + `[Terminal ${sessionId ?? "default"}] received ${(event.data as Blob).size} bytes`, + ); event.data.arrayBuffer().then((buffer) => { const data = new Uint8Array(buffer); termRef.current?.write(data); @@ -311,7 +313,9 @@ export const TerminalComponent = React.forwardRef( // Open xterm first (must happen before fit) term.open(container); term.focus(); - console.log(`[Terminal ${sessionId ?? "default"}] xterm opened and focused`); + console.log( + `[Terminal ${sessionId ?? "default"}] xterm opened and focused`, + ); const ws = connectWebSocket(); // Initial fit after layout settles (terminal must be opened first) @@ -337,7 +341,10 @@ export const TerminalComponent = React.forwardRef( // Handle terminal input term.onData((data) => { // eslint-disable-next-line no-console - console.log(`[Terminal ${sessionId ?? "default"}] sending:`, JSON.stringify(data)); + console.log( + `[Terminal ${sessionId ?? "default"}] sending:`, + JSON.stringify(data), + ); const currentWs = wsRef.current; if (currentWs?.readyState !== WebSocket.OPEN) return;