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
This commit is contained in:
@@ -124,6 +124,21 @@ async def _handle_terminal_websocket(
|
|||||||
|
|
||||||
logger.debug("Terminal auth passed for instance %s, user %s", instance_id, user_id)
|
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
|
# Fetch tool type to get startup_command
|
||||||
tool_type = await db_session.get(ToolType, instance.tool_type_id)
|
tool_type = await db_session.get(ToolType, instance.tool_type_id)
|
||||||
startup_command = tool_type.startup_command if tool_type else None
|
startup_command = tool_type.startup_command if tool_type else None
|
||||||
|
|||||||
@@ -140,7 +140,9 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
|
|
||||||
if (event.data instanceof Blob) {
|
if (event.data instanceof Blob) {
|
||||||
// eslint-disable-next-line no-console
|
// 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) => {
|
event.data.arrayBuffer().then((buffer) => {
|
||||||
const data = new Uint8Array(buffer);
|
const data = new Uint8Array(buffer);
|
||||||
termRef.current?.write(data);
|
termRef.current?.write(data);
|
||||||
@@ -311,7 +313,9 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
// Open xterm first (must happen before fit)
|
// Open xterm first (must happen before fit)
|
||||||
term.open(container);
|
term.open(container);
|
||||||
term.focus();
|
term.focus();
|
||||||
console.log(`[Terminal ${sessionId ?? "default"}] xterm opened and focused`);
|
console.log(
|
||||||
|
`[Terminal ${sessionId ?? "default"}] xterm opened and focused`,
|
||||||
|
);
|
||||||
const ws = connectWebSocket();
|
const ws = connectWebSocket();
|
||||||
|
|
||||||
// Initial fit after layout settles (terminal must be opened first)
|
// Initial fit after layout settles (terminal must be opened first)
|
||||||
@@ -337,7 +341,10 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
// Handle terminal input
|
// Handle terminal input
|
||||||
term.onData((data) => {
|
term.onData((data) => {
|
||||||
// eslint-disable-next-line no-console
|
// 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;
|
const currentWs = wsRef.current;
|
||||||
if (currentWs?.readyState !== WebSocket.OPEN) return;
|
if (currentWs?.readyState !== WebSocket.OPEN) return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user