From 0dba13a354877c0703bd8aa6b0c2d67dc98a567f Mon Sep 17 00:00:00 2001 From: Fusion Date: Sun, 24 May 2026 15:57:29 +0200 Subject: [PATCH] fix: send SIGWINCH to docker exec after PTY resize When resizing the PTY, docker exec needs to be notified so it can re-read the terminal size and propagate it to the container's PTY. Without this, the container shell stays at 80x24 regardless of what the frontend sends. --- apps/api/src/api/terminal.py | 8 ++++---- apps/api/src/services/terminal_session.py | 24 ++++++++++++++++++++--- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/apps/api/src/api/terminal.py b/apps/api/src/api/terminal.py index 26d05f1..c43ed91 100644 --- a/apps/api/src/api/terminal.py +++ b/apps/api/src/api/terminal.py @@ -148,10 +148,10 @@ async def _write_loop(session, websocket) -> None: msg_type = ctrl.get("type") if msg_type == "resize": - await session.resize( - ctrl.get("cols", 80), - ctrl.get("rows", 24), - ) + cols = ctrl.get("cols", 80) + rows = ctrl.get("rows", 24) + logger.info(f"Received resize message for instance {instance_id}: {cols}x{rows}") + await session.resize(cols, rows) elif msg_type == "reset": # Reset terminal session logger.info("Resetting terminal session for instance %s", session.instance_id) diff --git a/apps/api/src/services/terminal_session.py b/apps/api/src/services/terminal_session.py index 0f6e7d0..76ec714 100644 --- a/apps/api/src/services/terminal_session.py +++ b/apps/api/src/services/terminal_session.py @@ -1,9 +1,11 @@ """Terminal session management for tool instances.""" import asyncio +import logging import os import pty import select +import signal import struct import fcntl import time @@ -11,6 +13,8 @@ import uuid from collections import deque from typing import Any +logger = logging.getLogger(__name__) + class TerminalSession: """Manages a single terminal session connected to a docker container. @@ -55,6 +59,7 @@ class TerminalSession: # Set the terminal size initially self._set_terminal_size(self._cols, self._rows) + logger.info(f"Starting terminal session {self.session_id} for container {self.container_id} with initial size {self._cols}x{self._rows}") # Start docker exec with the slave fd as stdin/stdout/stderr # Using -it because the slave fd IS a TTY @@ -79,16 +84,27 @@ class TerminalSession: self.last_activity = time.time() def _set_terminal_size(self, cols: int, rows: int) -> None: - """Set the terminal size using TIOCSWINSZ.""" + """Set the terminal size using TIOCSWINSZ and signal docker exec.""" if self._master_fd is None: + logger.warning("Cannot resize: master_fd is None (session not started)") return # TIOCSWINSZ = 0x5414 on Linux TIOCSWINSZ = 0x5414 size = struct.pack('HHHH', rows, cols, 0, 0) try: fcntl.ioctl(self._master_fd, TIOCSWINSZ, size) - except (OSError, IOError): - pass + logger.info(f"Resized PTY to {cols}x{rows} (fd={self._master_fd})") + + # Send SIGWINCH to docker exec process so it re-reads terminal + # size and propagates it to the container's PTY + if self.process and self.process.pid: + try: + os.kill(self.process.pid, signal.SIGWINCH) + logger.info(f"Sent SIGWINCH to docker exec pid={self.process.pid}") + except (OSError, ProcessLookupError) as e: + logger.warning(f"Failed to send SIGWINCH: {e}") + except (OSError, IOError) as e: + logger.error(f"Failed to resize PTY: {e}") async def read_output(self) -> bytes: """Read output from the PTY master and store in buffer.""" @@ -134,9 +150,11 @@ class TerminalSession: async def resize(self, cols: int, rows: int) -> None: """Resize the terminal.""" if self._closed: + logger.warning("Cannot resize: session is closed") return self._cols = cols self._rows = rows + logger.info(f"resize() called for session {self.session_id}: {cols}x{rows}") self._set_terminal_size(cols, rows) async def reset(self) -> None: