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.
This commit is contained in:
Fusion
2026-05-24 15:57:29 +02:00
parent 5500552993
commit 0dba13a354
2 changed files with 25 additions and 7 deletions
+4 -4
View File
@@ -148,10 +148,10 @@ async def _write_loop(session, websocket) -> None:
msg_type = ctrl.get("type") msg_type = ctrl.get("type")
if msg_type == "resize": if msg_type == "resize":
await session.resize( cols = ctrl.get("cols", 80)
ctrl.get("cols", 80), rows = ctrl.get("rows", 24)
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": elif msg_type == "reset":
# Reset terminal session # Reset terminal session
logger.info("Resetting terminal session for instance %s", session.instance_id) logger.info("Resetting terminal session for instance %s", session.instance_id)
+21 -3
View File
@@ -1,9 +1,11 @@
"""Terminal session management for tool instances.""" """Terminal session management for tool instances."""
import asyncio import asyncio
import logging
import os import os
import pty import pty
import select import select
import signal
import struct import struct
import fcntl import fcntl
import time import time
@@ -11,6 +13,8 @@ import uuid
from collections import deque from collections import deque
from typing import Any from typing import Any
logger = logging.getLogger(__name__)
class TerminalSession: class TerminalSession:
"""Manages a single terminal session connected to a docker container. """Manages a single terminal session connected to a docker container.
@@ -55,6 +59,7 @@ class TerminalSession:
# Set the terminal size initially # Set the terminal size initially
self._set_terminal_size(self._cols, self._rows) 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 # Start docker exec with the slave fd as stdin/stdout/stderr
# Using -it because the slave fd IS a TTY # Using -it because the slave fd IS a TTY
@@ -79,16 +84,27 @@ class TerminalSession:
self.last_activity = time.time() self.last_activity = time.time()
def _set_terminal_size(self, cols: int, rows: int) -> None: 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: if self._master_fd is None:
logger.warning("Cannot resize: master_fd is None (session not started)")
return return
# TIOCSWINSZ = 0x5414 on Linux # TIOCSWINSZ = 0x5414 on Linux
TIOCSWINSZ = 0x5414 TIOCSWINSZ = 0x5414
size = struct.pack('HHHH', rows, cols, 0, 0) size = struct.pack('HHHH', rows, cols, 0, 0)
try: try:
fcntl.ioctl(self._master_fd, TIOCSWINSZ, size) fcntl.ioctl(self._master_fd, TIOCSWINSZ, size)
except (OSError, IOError): logger.info(f"Resized PTY to {cols}x{rows} (fd={self._master_fd})")
pass
# 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: async def read_output(self) -> bytes:
"""Read output from the PTY master and store in buffer.""" """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: async def resize(self, cols: int, rows: int) -> None:
"""Resize the terminal.""" """Resize the terminal."""
if self._closed: if self._closed:
logger.warning("Cannot resize: session is closed")
return return
self._cols = cols self._cols = cols
self._rows = rows self._rows = rows
logger.info(f"resize() called for session {self.session_id}: {cols}x{rows}")
self._set_terminal_size(cols, rows) self._set_terminal_size(cols, rows)
async def reset(self) -> None: async def reset(self) -> None: